Mrli
别装作很努力,
因为结局不会陪你演戏。
Contacts:
QQ博客园

2022年3月19~20日-爬虫项目记录

2022/04/04 爬虫
Word count: 8,179 | Reading time: 36min

2022年3月20日——公告存网站页面到数据库

文件存储

  1. 文件夹不允许出现/\:*?|<>"
  2. a标签中href不能有, 因为通过etree.tostring会被转义成%5C===>进行了str.replace("\", “/”)
  3. windows下路径分隔符是\\, 所以save_path中会有\\, 因此使用save_path.replace("\\", "/")可以解决
  4. 多级创建文件夹os.makedirs()、单层创建os.mkdir()
    5文件名称问题: 乱码太长报错, 将其取a标签中内容解决
1
2
3
4
5
# 乱码太长报错
s = ".\\attaches\\11963218-2021年09月22日华能能源交通产业控股有限公司集团物资供应中心(甘肃区域)07月份集中物资供应--八零三电厂阀门询价采购(包093)询价书询价公告\\������������������������������������������07������������������������--���������������������������������������093���.wps"

with open(s, "wb") as f:
f.write(b"ggg")

数据库表

varchar能存多少汉字、数字?

具体还是要看版本的,一个字符占用3个字节 ,一个汉字(包括数字)占用3个字节=一个字符

  • 4.0版本以下,varchar(100),指的是100字节,如果存放UTF8汉字时,只能存33个(每个汉字3字节)
  • 5.0版本以上**,varchar(100),指的是100字符,⭐️无论存放的是数字、字母还是UTF8汉字(每个汉字3字节),都可以存放100个。
  • UTF8编码中一个汉字(包括数字)占用3个字节
  • GBK编码中一个汉字(包括数字)占用2个字节*

varchar的最大长度是多少呢?

mysql的vachar字段的类型虽然最大长度是65535,但是并不是能存这么多数据,最大可以到65533,其中需要1到2个字节来存储数据长度(如果列声明的长度超过255,则使用两个字节来存储长度,否则1个)字节,当不允许非空字段的时候(因为要用一个字节来存储不可为空的标识),当允许非空字段的时候只能到65532(省下了存储非空的那个字节)。

mysql字段类型存储需要多少字节?

数字类型

列类型 需要的存储量
TINYINT 1 字节
SMALLINT 2 个字节
MEDIUMINT 3 个字节
INT 4 个字节
INTEGER 4 个字节
BIGINT 8 个字节
FLOAT(X) 4 如果 X < = 24 或 8 如果 25 < = X < = 53
FLOAT 4 个字节
DOUBLE 8 个字节
DOUBLE PRECISION 8 个字节
REAL 8 个字节
DECIMAL(M,D) M字节(D+2 , 如果M < D)
NUMERIC(M,D) M字节(D+2 , 如果M < D)

日期和时间类型

列类型 需要的存储量
DATE 3 个字节
DATETIME 8 个字节
TIMESTAMP 4 个字节
TIME 3 个字节
YEAR 1 字节

串类型

列类型 需要的存储量
CHAR(M) M字节,1 <= M <= 255
VARCHAR(M) L+1 字节, 在此L <= M和1 <= M <= 255
TINYBLOB, TINYTEXT L+1 字节, 在此L< 2 ^ 8
BLOB, TEXT L+2 字节, 在此L< 2 ^ 16
MEDIUMBLOB, MEDIUMTEXT L+3 字节, 在此L< 2 ^ 24
LONGBLOB, LONGTEXT L+4 字节, 在此L< 2 ^ 32
ENUM(‘value1’,‘value2’,…) 1 或 2 个字节, 取决于枚举值的数目(最大值65535)
SET(‘value1’,‘value2’,…) 1,2,3,4或8个字节, 取决于集合成员的数量(最多64个成员)

MySQL中类型后面的数字含义

形式:类型(m)

  1. 整数型的数值类型已经限制了取值范围,有符号整型和无符号整型都有,而M值并不代表可以存储的数值字符长度,它代表的是数据在显示时显示的最小长度,当存储的字符长度超过M值时,没有任何的影响,只要不超过数值类型限制的范围。当存储的字符长度小于M值时,只有在设置了zerofill用0来填充,才能够看到效果,换句话就是说,没有zerofill,M值就是无用的
  2. 字符型如varchar(50) 可以储存50个字符,表示的是可变不定长的。

mySQL默认字符集

MySQL对于字符集的指定可以细化到一个数据库,一张表,一列,应该用什么字符集。 但是,传统的程序在创建数据库和数据表时并没有使用那么复杂的配置,它们用的是默认的配置,那么,默认的配置从何而来呢?

  1. 编译MySQL 时,指定了一个默认的字符集,这个字符集是 latin1;
  2. 安装MySQL 时,可以在配置文件 (my.ini) 中指定一个默认的的字符集,如果没指定,这个值继承自编译时指定的;
  3. 启动mysqld 时,可以在命令行参数中指定一个默认的的字符集,如果没指定,这个值继承自配置文件中的配置,此时 character_set_server 被设定为这个默认的字符集;
  4. 当创建一个新的数据库时,除非明确指定,这个数据库的字符集被缺省设定为character_set_server;
  5. 当选定了一个数据库时,character_set_database被设定为这个数据库默认的字符集;
  6. 在这个数据库里创建一张表时,表默认的字符集被设定为 character_set_database,也就是这个数据库默认的字符集;
  7. 当在表内设置一栏时,除非明确指定,否则此栏缺省的字符集就是表默认的字符集;

最终create.sql文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

CREATE DATABASE if not EXISTS `huaneng` ;
use `huaneng`;

-- ----------------------------
-- Table structure for quotation_inone
-- ----------------------------
DROP TABLE IF EXISTS `quotation_inone`;
CREATE TABLE `quotation_inone` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`date` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`content` LONGTEXT NULL DEFAULT NULL,
`proclamation_url` VARCHAR(512) NULL DEFAULT NULL,
UNIQUE INDEX (`proclamation_url`),

PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

2022年3月20日-护士题目下载存Word

索引报错

  • MySQL 添加索引报错:BLOB/TEXT column used in key specification without a key length
    1. 当我们对一个名称为platform的字段,类型为 text 添加unique唯一性约束和索引约束时,会报错。 原因:MySQL只能将BLOB/TEXT类型字段设置索引数据的前N个字符,因此,只需要通过sql在增加索引时指定对应字段的长度即可,如:
      ALTER TABLE hello_world ADD INDEX key1(platform(250), platform2(250), type);, 其中,platform 和 platform2 就是 text 类型的数据
    2. 根本原因: 错误发生的原因是因为MySQL只能将BLOB/TEXT类型字段设置索引为BLOB/TEXT数据的前N个字符,因此错误常常发生在字段被定义为TEXT/BLOB类型或者和TEXT/BLOB同质的数据类型,如TINYTEXT,MEDIUMTEXT,LONGTEXT ,TINYBLOB,MEDIUMBLOB 和LONGBLOB,并且当前操作是将这个字段设置成主键或者是索引的操作。在未指定TEXT/BLOB‘键长’的情况下,字段是变动的并且是动态的大小所以MySQL不能够保证字段的唯一性。因此当使用TEXT/BLOB类型字段做为索引时,N的值必须提供出来才可以让MySQL决定键长,但是MySQL不支持在TEXT/BLOB限制,TEXT(88)是不行的。
      • 解决方案是将unique限制和索引从TEXT/BLOB字段中移除,或者是设置另一个字段为主键,如果你不愿意这样做并且想在TEXT/BLOB上加限制,那么你可以尝试将这个字段更改为VARCHAR类型,同时给他一个限制长度,默认VARCHAR最多可以限定在255个字符,并且限制要在声明类型的右边指明,如VARCHAR(200)将会限制仅仅200个字符.(注: 但是mysql不支持对TEXT/BLOB长度的限制。)
      • https://blog.csdn.net/u012069924/article/details/28858337
        ▲. 在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据 实际文本区分度决定索引长度即可。Java 开发手册 33/44 说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90%以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。

数据库插入数据

1
2
3
-- \\表示一个\
use xian_nurse;
insert into `question` (`path`) VALUES ('书籍\\C-《传染病护理学习指导与习题集》-选择题'), ('书籍\\C-《传染病护理技术学习指导与习题集》-选择题'), ('书籍\\C-《传染病护理技术学习指导与习题集》-题干题'), ('书籍\\C-《成人护理学学习指导与习题集-人民卫生出版社》-选择题'), ('书籍\\C-《成人护理学学习指导与习题集-人民卫生出版社》-题干题'), ('书籍\\E -《儿科护理学实践与学习指导(十三五)》—选择题'), ('书籍\\E -《儿科护理学实践与学习指导(十三五)》—题干题');

操作word

使用python-docx: python -m pip install python-docxhttps://python-docx.readthedocs.io/en/latest/

  1. 添加1-9级标题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from datetime import datetime

from docx import Document

# 创建新的docx文件
document = Document()
document.add_heading('1级标题', 1) # 添加1级标题
document.add_heading('2级标题', 2) # 添加2级标题
document.add_heading('3级标题', 3) # 添加3级标题
document.add_heading('4级标题', 4) # 添加4级标题
document.add_heading('5级标题', 5) # 添加5级标题
document.add_heading('6级标题', 6) # 添加6级标题
document.add_heading('7级标题', 7) # 添加7级标题
document.add_heading('8级标题', 8) # 添加8级标题
document.add_heading('9级标题', 9) # 添加9级标题
document.save('{}.docx'.format(datetime.now().strftime('%Y%m%d%H%M%S')))
  1. 添加段落
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from datetime import datetime

from docx import Document

# 创建新的docx文件
document = Document()
paragraph = """这是一个段落
"""
paragraph2 = """这是一个新的段落"""
paragraph3 = """这是一个新的段落。
"""
document.add_paragraph(paragraph)
document.add_paragraph(paragraph2)
document.add_paragraph(paragraph3)
document.save('{}.docx'.format(datetime.now().strftime('%Y%m%d%H%M%S')))
  1. 设置字体大小和样式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from datetime import datetime

from docx import Document
# 创建新的docx文件
from docx.shared import Pt

document = Document()
document.add_paragraph("这是一个段落") # 添加段落
paragraph = document.add_paragraph("这是一个段落,") # 添加段落
run = paragraph.add_run('设置了字体的段落') # 在同一段添加内容, 即为了操作段落或单词的子字符串. 概念上讲,您需要为段落/文本的run每个部分创建一个实例。
"""
Append a run to this paragraph containing *text* and having character
style identified by style ID *style*. *text* can contain tab
(``\\t``) characters, which are converted to the appropriate XML form
for a tab. *text* can also include newline (``\\n``) or carriage
return (``\\r``) characters, each of which is converted to a line
break.
大概意思就是追加一个段落, 包含text, 且设置了格式, 我感觉是这样
"""
run.font.name = u'宋体' # 设置字体
run.font.size = Pt(20) # 设置字号
# run.font.color.rgb = RGBColor(255, 0, 0) # 设置红色
run.font.underline = True # 设置下划线

run1 = paragraph.add_run('\t粗体')
run1.bold = True

run2 = paragraph.add_run('\t斜体')
run2.italic = True
#图片居中设置
paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
1
2
3
4
5
6
7
8
9
10
11
12
13
Doc = Document() 
Doc.styles['Normal'].font.name = u'宋体'
Doc.styles['Normal']._element.rPr.rFonts.set(qn('w:eastAsia'), u'宋体')
Doc.styles['Normal'].font.size = Pt(10.5)
Doc.styles['Normal'].font.color.rgb = RGBColor(0,0,0)
Head = Doc.add_heading("",level=1)# 这里不填标题内容
run = Head.add_run("刚来csdn,这就是博客么,I了")
run.font.name=u'Cambria'
run.font.color.rgb = RGBColor(0,0,0)
run._element.rPr.rFonts.set(qn('w:eastAsia'), u'Cambria')
Doc.add_paragraph("Python ")
Doc.add_paragraph("Python 对word进行操作")
Doc.save("Python_word.docx")

from : https://www.pythonheidong.com/blog/article/692569/99875f167810b45f17e8/

  1. 有序(无序)列表和引用
1
2
3
4
5
6
7
8
9
10
# 增加引用
document.add_paragraph('123', style='Intense Quote')

# 增加有序列表
document.add_paragraph(u'有序列表元素1', style='List Number')
document.add_paragraph(u'有序列别元素2', style='List Number')

# 增加无序列表
document.add_paragraph(u'无序列表元素1', style='List Bullet')
document.add_paragraph(u'无序列表元素2', style='List Bullet')
  1. 表格和分页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 增加图片(此处使用相对位置)
# document.add_picture('jdb.jpg', width=Inches(1.25))

# 增加表格
table = document.add_table(rows=3, cols=3) # 3行3列
hdr_cells1 = table.rows[0].cells # 第一行
hdr_cells1[0].text = "第一行,第一列"
hdr_cells1[1].text = "第一行,第二列"
hdr_cells1[2].text = "第一行,第三列"

hdr_cells2 = table.rows[1].cells # 第二行
hdr_cells2[0].text = "第二行,第一列"
hdr_cells2[1].text = "第二行,第二列"
hdr_cells2[2].text = "第二行,第三列"

hdr_cells3 = table.rows[2].cells # 第三行
hdr_cells3[0].text = "第三行,第一列"
hdr_cells3[1].text = "第三行,第二列"
hdr_cells3[2].text = "第三行,第三列"

# 增加分页
document.add_page_break()

提取文字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from docx import Document

path = '/media/bobo/自动化办公/wordOperation/wordDemo/test2.docx'

doc = Document(path)
print(doc.paragraphs)

# 输出的是列表,列表中一共有4份内容
# [<docx.text.paragraph.Paragraph object at 0x7fca95f0aba8>,
# <docx.text.paragraph.Paragraph object at 0x7fca95f0abe0>,
# <docx.text.paragraph.Paragraph object at 0x7fca95f0ab70>,
#<docx.text.paragraph.Paragraph object at 0x7fca95f0ac50>,]

for paragraph in doc.paragraphs:
print(paragraph.text)

from :

共同点处理

文件保存

由于文件夹、文件名不允许有/\:*?|<>",因此需要保存时如果出现这些字符得特别处理。

  1. 项目一:xxxx阻垢剂\反渗透阻垢剂\25KG询价书询价公告\zxc.doc ==> xxx阻垢剂、反渗透阻垢剂、25KG询价书询价公告\zxc.doc,存储在attaches\xxx阻垢剂、反渗透阻垢剂、25KG询价书询价公告\zxc.doc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@staticmethod
def format_name(name: str) -> [bool, str]:
"""
文件和文件夹名规范化
:param name:
:return:
"""
invalid_char = ["/", "\\", ":", "*", "?", "|", "<", ">", "\""]
rename = name
valid = True
for ch in invalid_char:
if ch in rename:
if ch == "?":
rename = rename.replace("?", "?")
elif ch == "<":
rename = rename.replace("<", "[")
elif ch == ">":
rename = rename.replace(">", "]")
else:
rename = rename.replace(ch, "、")
valid = False
return valid, rename
  1. 项目二:'.\\docx/专科题库\\儿科\\[书籍]\\S-《实用临床护理三基-"应知应会"》—名词解释、简答(规02)\\something.docx'===>'.\\docx/专科题库\\儿科\\[书籍]\\S-《实用临床护理三基-'应知应会'》—名词解释、简答(规02)\\something.docx'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def format_file_path(path: str) -> [bool, str]:
"""
文件和文件夹名规范化
:param name:
:return:
"""
invalid_char = ["/", "\\", ":", "*", "?", "|", "<", ">", "\""]
rename = path
valid = True
for ch in invalid_char:
if ch in rename:
if ch == "?":
rename = rename.replace("?", "?")
elif ch == "<":
rename = rename.replace("<", "[")
elif ch == ">":
rename = rename.replace(">", "]")
elif ch == '"':
rename = rename.replace("\"", "\'")
elif ch == "\\" or ch == "/":
# 注意跟 上一个的区别, 由于上一个项目中的/和\都是分隔符,而不是真实的分隔目录,因此需要替换,但本项目是真实路径分隔符,所以不需要转换
pass
else:
rename = rename.replace(ch, "、")
valid = False
return valid, rename

附录

lxml库的使用

  • 提取Element中最近标签中的文本(不包含标签本身): ele.xpath('//*[@id="xxx"]/text()')

  • 提取Element中所有标签(标签嵌套)中的文本(不包含标签本身): ele.xpath('//*[@id="xxx"]/string(.)')

    • 注:跟//text()区别在于,string(.)的结果为合并后的str,//text()为未合并的list
  • 提取Element中所有内容(标签本身): etree.tostring(ele, method="html")

  • 修改Element某一属性:

    1
    2
    3
    4
    5
    6
    appendixs = html_content.xpath('/html/body/div[4]/div/div[4]/div/div/div[1]/div[4]/p/a')
    for a in appendixs:
    href = a.xpath("@href")[0]
    save_path = self.download_file(ReUtil.extra_announcement_id(detail_article_url), href, title)
    # html中会对\进行转义
    a.attrib["href"] = save_path

    注: 修改子element的某属性后,root节点tostring的结果中element也会被修改。(root抱有element的引用)

  • Element的XML标记名通过对象的属性访问

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    a = '<a href="http://baidu.com">hh<span>gg</span></a>'
    res = {_Element: 1} <Element html at 0x1de3a6b9800>
    attrib = {_Attrib: 0} {}
    base = {NoneType} None
    nsmap = {dict: 0} {}
    prefix = {NoneType} None
    sourceline = {int} 1
    tag = {str} 'html'
    tail = {NoneType} None
    text = {NoneType} None
  • Element被组织在XML树结构中。增加子Element并指定它们的父Element,使用append方法。

    1
    2
    3
    4
    5
    # 法一
    root = etree.Element("root")
    root.append(etree.Element("child1"))
    # 法二
    childNode = etree.SubElement(root, "child")
  • 删除Element下某一节点内容,与添加相同,也有实例方法和工厂方法两种

    • parentnode.remove(node)

    • etree.strip_elements(html, 'element_name', with_tag=True/False)

timeit模块使用

1
2
3
4
5
spider = HuaNengSpider(ifend="in")
# 记得setup导入
run_time = timeit.timeit("spider.run_parallelly()", "from __main__ import spider", number=1)
# 输出的单位为s
print("耗时: {}s".format(run_time))
1
2
run_time = timeit.timeit('HuaNengSpider(ifend="notin", is_in_one=True).run_with_concurrent()',
"from __main__ import HuaNengSpider", number=1)

Python中操作SQLAlchemy,SQLAlchemy中文技术文档

参考:https://www.jianshu.com/p/0ad18fdd7eed、python3 SQLAlchemy模块使用

ArgumentParser使用

  • parser.add_argument("-o", "--is_in_one", action="store_true", help="数据存放在一张表中"), 其中action的store_true表示,如果出现–is_in_one(action)则设置未true, 所以默认为False

    • 参数设置为True、False的最好不使用choice参数:因为
    • parser.add_argument("-o", "--is_in_one", choices=[True, False], help="数据存放在一张表中"), choice中为True、False时, 可以选择不填,此时is_in_one为None, 如果必须要这两个则选一个则加上required=True选项
    • 加上required效果===>爬虫脚本: error: argument -o/--is_in_one: invalid choice: 'q' (choose from True, False), 但是如果输入True同样会报错: 爬虫脚本: error: argument -o/–is_in_one: invalid choice: ‘True’ (choose from True, False)

    所以只有choices=["True", "False"]才能生效, 好在的是Python中对true判断比较宽容,下面三种都可以,因此使得choice传str类型的True也可以成功判定,即能实现相同效果,但是还是存在些歧义的,得谨慎使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    a = "true"
    b = True
    c = "True"
    if a:
    print("a yes")
    if b:
    print("b yes")
    if c:
    print("c yes")

其他SQL文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
CREATE TABLE `auto_bilibili` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`userid` int(11) NOT NULL,
`name` varchar(255) NOT NULL COMMENT '创建的任务名',
`sessdata` longtext NOT NULL,
`bili_jct` longtext NOT NULL,
`dedeuserid` longtext NOT NULL,
`taskIntervalTime` int(11) NOT NULL DEFAULT '10' COMMENT '任务之间的执行间隔',
`numberOfCoins` int(11) NOT NULL DEFAULT '5' COMMENT '每日投币数量',
`reserveCoins` int(11) NOT NULL DEFAULT '50' COMMENT '预留的硬币数',
`selectLike` int(11) NOT NULL DEFAULT '0' COMMENT '投币时是否点赞,默认 0, 0:否 1:是',
`monthEndAutoCharge` varchar(10) NOT NULL DEFAULT 'true' COMMENT '年度大会员月底是否用 B 币券给自己充电,默认 true,即充电对象是你本人。',
`giveGift` varchar(10) NOT NULL DEFAULT 'true' COMMENT '直播送出即将过期的礼物,默认开启',
`upLive` varchar(255) NOT NULL DEFAULT '0' COMMENT '直播送出即将过期的礼物,指定 up 主,为 0 时则随随机选取一个 up 主',
`chargeForLove` varchar(255) NOT NULL DEFAULT '0' COMMENT '给指定 up 主充电,值为 0 或者充电对象的 uid,默认为 0,即给自己充电',
`devicePlatform` varchar(10) NOT NULL DEFAULT 'ios' COMMENT '手机端漫画签到时的平台,建议选择你设备的平台 ,默认 ios',
`coinAddPriority` int(11) NOT NULL DEFAULT '1' COMMENT '0:优先给热榜视频投币,1:优先给关注的 up 投币',
`userAgent` varchar(255) NOT NULL DEFAULT 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 Edg/86.0.622.69' COMMENT '浏览器 UA',
`skipDailyTask` varchar(10) NOT NULL DEFAULT 'false' COMMENT '是否跳过每日任务',
`webhook` varchar(255) DEFAULT NULL COMMENT '推送地址',
`enddate` datetime DEFAULT NULL,
`match_enable` varchar(10) NOT NULL DEFAULT 'false' COMMENT '预测是否开启',
`match_predictNumberOfCoins` int(11) NOT NULL DEFAULT '10' COMMENT '单次预测投注硬币',
`match_minimumNumberOfCoins` int(11) NOT NULL DEFAULT '200' COMMENT '预测保留硬币',
`match_showHandModel` varchar(255) NOT NULL DEFAULT 'false' COMMENT '押注形式',
`other` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `userid` (`userid`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC

2022年3月26日——小鸡词典

抓取指定词条信息

https://jikipedia.com/search?phrase=a&category=definition

需求:将包含英文的词条信息保存到excel文件中

难点:

  • 由于不断下滑都会产生数据, 因此难以评估数据量

思路:通过搜索a-z英文字母,从而找出所有包含英文字母的单词,由于搜索结果的数量是有限的,因此可以确定是否完成。 随着而来的问题是,如果词条为apple,那么搜a/p/l/e都会重复抓取==>防重思路: 通过词条id来判断是否已经爬取过

Lookup:

▲. 由于不需要对excel中样式进行修改,所以为了省事,最终直接采用了入库导出Excel的方式,而没用使用xlwings库

日志输出两次

原因是多个Py文件中引入了LOG, 但引入方式不一样,如main.py中是from logger import LOG, 而spider.py中是from logger import LOG,
saver.oy中是from jikipedia.logger import LOG

问题就出在main中导入了spider.py, 而spider又引用了saver.py,但是saver中引用的LOG由于导入方式不同,使得跟前两个的使用的LOG不是同一个,但LOG配置都是一样的,所以导致了输出多次。

pytest库使用

pip install pytest

  • 测试文件以test_开头(以_test结尾也可以)
  • 测试类以Test开头,并且不能带有 init 方法
  • 测试函数以test_开头
  • 断言使用基本的assert即可

pytest 夹具(fixture)

测试需要在一组已知对象的背景下进行。 这组对象称为测试夹具(fixture)。

1
2
3
4
5
6
7
8
9
10
11
12
13

import algo
import pytest


@pytest.fixture
def data():
return [3, 2, 1, 5, -3, 2, 0, -2, 11, 9]


def test_sel_sort(data):
sorted_vals = algo.sel_sort(data)
assert sorted_vals == sorted(data)

综合测试

接下来,我们展示如何在 Python 包中运行测试。

  1. 首先把将所有的测试文件全部放在./tests/文件夹下
1
2
3
4
5
6
7
8
9
10
setup.py
utils
│ algo.py
│ srel.py
│ __init__.py

└───tests
algo_test.py
srel_test.py
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# !/usr/bin/env python3
# tests/algo_test.py
import utils.algo
import pytest


@pytest.fixture
def data():
return [3, 2, 1, 5, -3, 2, 0, -2, 11, 9]


def test_sel_sort(data):
sorted_vals = utils.algo.sel_sort(data)
assert sorted_vals == sorted(data)


def test_min():
values = (2, 3, 1, 4, 6)

val = utils.algo.min(values)
assert val == 1


def test_max():
values = (2, 3, 1, 4, 6)

val = utils.algo.max(values)
assert val == 6
  1. pytest tests/

执行文件、文件夹

  • pytest test_mod.py 执行该模块下的测试类测试方法
  • pytest testing 执行该文件夹下的所有模块
  • pytest test_cgi.py::test_answer,执行test_answer方法
  • pytest test_cgi.py::TestMyClass::test_one,执行TestMyClass类下的test_one方法

more: 在 PyCharm 里使用 Pytest 测试框架

requests异常

ReadTimeout: 意思是已经建立连接,并开始读取服务端资源。如果到了指定的时间,没有可能的数据被客户端读取,则报异常。
出现的情况是在设置了timeout设置为3秒,服务器在3秒内未给出响应,出现报错requests.exceptions.ReadTimeout。 ConnectTimeout:
意思是用来建立连接的时间。如果到了指定的时间,还没建立连接,则报异常。

附录

导入问题:

在tests/中如果要引工程根目录的文件比较麻烦,得通过相对路径from ..utils import xxx的方式, 但是这样必须把再加上sys.path.append("..")
,针对这种情况,一个解决方案是所有模块都从根工程为绝对路径开始引,比如main.py中引settings为from jikipedia.settings import xxx,
而tests中引也是同样from jikipedia.settings import xxx==>pycharm中使用alt+enter提示后使用的import也是从根目录导入from jikipedia.xxx import yyy

注: 这样需要把工程根目录作为包,所以得在根目录加上__init__.py文件

脚本中相对路径问题

由于settings.py会直接在同级的根目录下创建attaches文件夹,但是如果直接是mkdir("./attaches")的话,tests/中只要间接导入了settings.py都会在tests/下创建attaches,因为此时的脚本路径是在tests/下,所以settings中的可执行mkdir(.)就变成了在tests/.所以这边应该指定绝对路径, 下面是一个通过__file__获得脚本绝对路径,然后再获得其所在文件夹的做法:

DOWNLOAD_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "attaches")

DOWNLOAD_PATH的值被赋值成了F:\aDevelopment\Python\ShowYourCode\jikipedia从而解决了这个问题

redis使用

特点:

1.基于内存的key-value数据库
2.基于c语言编写的,可以支持多种语言的api //set每秒11万次,取get 81000次
3.支持数据持久化
4.value可以是string,hash, list, set, sorted set

连接redis服务器

  • 本地命令行进入redis: redis-cli
  • 远程连接:redis-cli -h host -p port -a password

命令参数:

key

  • keys * 获取所有的key
  • select 0 选择第一个库
  • move myString 1 将当前的数据库key移动到某个数据库,目标库有,则不能移动
  • flush db 清除指定库
  • randomkey 随机key
  • type key 类型
  • set key1 value1 设置key
  • get key1 获取key
  • mset key1 value1 key2 value2 key3 value3
  • mget key1 key2 key3
  • del key1 删除key
  • exists key 判断是否存在key
  • expire key 10 10过期
  • pexpire key 1000 毫秒
  • persist key 删除过期时间

string

  • set name cxx
  • get name
  • getrange name 0 -1 字符串分段
  • getset name new_cxx 设置值,返回旧值
  • mset key1 key2 批量设置
  • mget key1 key2 批量获取
  • setnx key value 不存在就插入(not exists)
  • setex key time value 过期时间(expire)
  • setrange key index value 从index开始替换value
  • incr age 递增
  • incrby age 10 递增
  • decr age 递减
  • decrby age 10 递减
  • incrbyfloat 增减浮点数
  • append 追加
  • strlen 长度
  • getbit/setbit/bitcount/bitop 位操作

hash

  • hset myhash name cxx
  • hget myhash name
  • hmset myhash name cxx age 25 note “i am notes”
  • hmget myhash name age note
  • hgetall myhash 获取所有的
  • hexists myhash name 是否存在
  • hsetnx myhash score 100 设置不存在的
  • hincrby myhash id 1 递增
  • hdel myhash name 删除
  • hkeys myhash 只取key
  • hvals myhash 只取value
  • hlen myhash 长度

list

  • lpush mylist a b c 左插入
  • rpush mylist x y z 右插入
  • lrange mylist 0 -1 数据集合
  • lpop mylist 弹出元素
  • rpop mylist 弹出元素
  • llen mylist 长度
  • lrem mylist count value 删除
  • lindex mylist 2 指定索引的值
  • lset mylist 2 n 索引设值
  • ltrim mylist 0 4 删除key
  • linsert mylist before a 插入
  • linsert mylist after a 插入
  • rpoplpush list list2 转移列表的数据

set

  • sadd myset redis
  • smembers myset 数据集合
  • srem myset set1 删除
  • sismember myset set1 判断元素是否在集合中
  • scard key_name 个数
  • sdiff | sinter | sunion 操作:集合间运算:差集 | 交集 | 并集
  • srandmember 随机获取集合中的元素
  • spop 从集合中弹出一个元素

zset

  • zadd zset 1 one
  • zadd zset 2 two
  • zadd zset 3 three
  • zincrby zset 1 one 增长分数
  • zscore zset two 获取分数
  • zrange zset 0 -1 withscores 范围值
  • zrangebyscore zset 10 25 withscores 指定范围的值
  • zrangebyscore zset 10 25 withscores limit 1 2 分页
  • Zrevrangebyscore zset 10 25 withscores 指定范围的值
  • zcard zset 元素数量
  • Zcount zset 获得指定分数范围内的元素个数
  • Zrem zset one two 删除一个或多个元素
  • Zremrangebyrank zset 0 1 按照排名范围删除元素
  • Zremrangebyscore zset 0 1 按照分数范围删除元素
  • Zrank zset 0 -1 分数最小的元素排名为0
  • Zrevrank zset 0 -1 分数最大的元素排名为0
  • Zinterstore
  • zunionstore rank:last_week 7 rank:20150323 rank:20150324 rank:20150325 weights 1 1 1 1 1 1 1

排序:

  • sort mylist 排序
  • sort mylist alpha desc limit 0 2 字母排序
  • sort list by it:* desc by命令
  • sort list by it:* desc get it:* get参数
  • sort list by it:* desc get it:* store sorc:result sort命令之store参数:表示把sort查询的结果集保存起来

订阅与发布:

  • 订阅频道:subscribe chat1
  • 发布消息:publish chat1 “hell0 ni hao”
  • 查看频道:pubsub channels
  • 查看某个频道的订阅者数量: pubsub numsub chat1
  • 退订指定频道: unsubscrible chat1 , punsubscribe java.*
  • 订阅一组频道: psubscribe java.*

redis事物:

  • 隔离性,原子性,
  • 步骤: 开始事务,执行命令,提交事务
  •      multi  //开启事务
    
  •      sadd myset a b c
    
  •      sadd myset e f g
    
  •      lpush mylist aa bb cc
    
  •      lpush mylist dd ff gg
    

服务器管理

  • dump.rdb
  • appendonly.aof
  • //BgRewriteAof 异步执行一个aop(appendOnly file)文件重写, 会创建当前一个AOF文件体积的优化版本
  • //BgSave 后台异步保存数据到磁盘,会在当前目录下创建文件dump.rdb
  • //save同步保存数据到磁盘,会阻塞主进程,别的客户端无法连接
  • //client kill 关闭客户端连接
  • //client list 列出所有的客户端
  • client setname myclient1 //给客户端设置一个名称
  • client getname
  • config get port
  • //configRewrite 对redis的配置文件进行改写

rdb

  • save 900 1
  • save 300 10
  • save 60 10000

aop备份处理

  • appendonly yes 开启持久化
  • appendfsync everysec 每秒备份一次

命令:

  • bgsave异步保存数据到磁盘(快照保存)
  • lastsave返回上次成功保存到磁盘的unix的时间戳
  • shutdown同步保存到服务器并关闭redis服务器
  • bgrewriteaof文件压缩处理(命令)

使用场景

  1. 去最新n个数据的操作
  2. 排行榜,取top n个数据 //最佳人气前10条
  3. 精确的设置过期时间
  4. 计数器
  5. 实时系统, 反垃圾系统
  6. pub, sub发布订阅构建实时消息系统
  7. 构建消息队列
  8. 缓存

Redis在互联网公司一般有以下应用:

  • String:缓存、限流、计数器、分布式锁、分布式Session
  • Hash:存储用户信息、用户主页访问量、组合查询
  • List:微博关注人时间轴列表、简单队列
  • Set:赞、踩、标签、好友关系
  • Zset:排行榜

MYSQL中时间类型设置

  • create_time:设置数据类型为:TIMESTAMP,默认值为:CURRENT_TIMESTAMP()
  • modify_time:设置数据类型为:TIMESTAMP,默认值为:CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP()
1
2
3
CREATE TABLE `base_name`.`table_name` (
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP(),
`modify_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP());

除了TIMESTAMP以外,也可以设置为DATETIME(0),对于在SQLAlchemy中为TIMESTAMP和DateTime类型

IP代理池

爬虫和反爬虫是一对矛和盾,反爬虫很常见的一个方法就是封IP,一个IP短时间内频繁访问,可以做限流或者是加入黑名单。针对这种情况,我们可以采用IP代理池+随机UA的方式来规避。

此外爬虫是一种IO密集型程序,如果全程单线程执行那会很慢,因此可以用多线程来提高数据采集效率,不过自己管理多线程太麻烦,所以可以选择线程池。

代理池

一个完善的代理池,应该可以实现以下功能

  • 批量采集代理(或者通过接口导入我们购买的代理,不过偶尔用一用还是免费的就好)
  • 采集到之后自动验证代理有效性
  • 将有效代理存储起来
  • 提供获取随机代理的接口、提供管理(删除、增加)代理的接口

使用代理池

之前在就看到过崔庆才书中介绍的开源代理池,但没仔细研究,本次搜索过后又发现了一个在GitHub上有14k+ Stars的代理池来用,名字叫ProxyPool

官方文档提供了两种部署方式,包括下载代码运行和docker,既然有docker那肯定选最方便的docker啦!

不过官方的docker命令还不够方便,因为这个代理池还需要依赖Redis服务,因此可以用docker-compose配置来用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
version: "3"
services:
redis:
image: redis
expose:
- 6379

web:
restart: always
image: jhao104/proxy_pool
environment:
- DB_CONN=redis://redis:6379/0
ports:
- "5010:5010"
depends_on:
- redis

接口信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"url": [
{
"desc": "get a proxy",
"params": "type: ''https'|''",
"url": "/get"
},
{
"desc": "get and delete a proxy",
"params": "",
"url": "/pop"
},
{
"desc": "delete an unable proxy",
"params": "proxy: 'e.g. 127.0.0.1:8080'",
"url": "/delete"
},
{
"desc": "get all proxy from proxy pool",
"params": "type: ''https'|''",
"url": "/all"
},
{
"desc": "return proxy count",
"params": "",
"url": "/count"
}
]
}

获取随机代理

封装获取随机代理和删除代理的操作后就可以使用了

1
2
3
4
5
6
7
8
9
10
import requests
# IP换成自己服务器的IP
PROXY_POOL_URL = 'http://127.0.0.1:5010'

def get_proxy():
proxy = requests.get(f"{PROXY_POOL_URL}/get/").json().get("proxy")
return {'http': proxy, 'https': proxy}

def delete_proxy(proxy):
requests.get(f"{PROXY_POOL_URL}/delete/?proxy={proxy}")

from: https://www.cnblogs.com/deali/p/15890678.html

添加代理装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# @author: Mrli
def reqCheck(func):
"""
目前只支持http代理,https大多无效
:param func:
:return:
"""
@wraps(func)
def wrapper(*args, **kwargs):
rand_proxy = get_proxy()
# 给请求加上proxy参数
kwargs["proxy"] = rand_proxy
tryTimes = 0
resp = None
while tryTimes < MAX_RETRY_COUNT:
try:
resp = func(*args, **kwargs)
print(resp.status_code)
if 100 < resp.status_code < 400:
break
except RequestException as e:
tryTimes += 1
LOG.warn("{}请求失败: e~{}".format(rand_proxy, e))
rand_proxy = get_proxy()
delete_proxy(rand_proxy)
return resp

return wrapper

Author: Mrli

Link: https://nymrli.top/2022/03/19/2022年3月19日-20日爬虫项目记录/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
JS逆向-webpack打包网站实战
NextPost >
深入学习使用Spring
CATALOG
  1. 1. 2022年3月20日——公告存网站页面到数据库
    1. 1.1. 文件存储
    2. 1.2. 数据库表
      1. 1.2.1. mysql字段类型存储需要多少字节?
      2. 1.2.2. MySQL中类型后面的数字含义
      3. 1.2.3. mySQL默认字符集
      4. 1.2.4. 最终create.sql文件
  2. 2. 2022年3月20日-护士题目下载存Word
    1. 2.0.1. 索引报错
    2. 2.0.2. 数据库插入数据
  3. 2.1. 操作word
    1. 2.1.1. 提取文字
  • 3. 共同点处理
    1. 3.1. 文件保存
  • 4. 附录
    1. 4.0.1. lxml库的使用
    2. 4.0.2. timeit模块使用
    3. 4.0.3. Python中操作SQLAlchemy,SQLAlchemy中文技术文档
    4. 4.0.4. ArgumentParser使用
    5. 4.0.5. 其他SQL文件
  • 5. 2022年3月26日——小鸡词典
  • 6. 抓取指定词条信息
  • 7. Lookup:
    1. 7.1. 日志输出两次
    2. 7.2. pytest库使用
      1. 7.2.1. 综合测试
    3. 7.3. requests异常
    4. 7.4. 附录
      1. 7.4.1. 导入问题:
      2. 7.4.2. 脚本中相对路径问题
      3. 7.4.3. redis使用
        1. 7.4.3.1. 连接redis服务器
        2. 7.4.3.2. 命令参数:
        3. 7.4.3.3. 使用场景
    5. 7.5. MYSQL中时间类型设置
    6. 7.6. IP代理池
      1. 7.6.1. 代理池
      2. 7.6.2. 使用代理池
        1. 7.6.2.1. 获取随机代理
    7. 7.7. 添加代理装饰器