Python 爬虫

网络爬虫是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。
本文介绍基本的网络爬虫的方法,并给出一个具体实例。
Reference:公开课

爬虫许可

大部分网站都会在其网站根网址上发布 robots 协议
如豆瓣网:https://www.douban.com/robots.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
User-agent: *
Disallow: /subject_search
Disallow: /amazon_search
Disallow: /search
Disallow: /group/search
Disallow: /event/search
Disallow: /celebrities/search
Disallow: /location/drama/search
Disallow: /forum/
Disallow: /new_subject
Disallow: /service/iframe
Disallow: /j/
Disallow: /link2/
Disallow: /recommend/
Disallow: /trailer/
Disallow: /doubanapp/card
Sitemap: https://www.douban.com/sitemap_index.xml
Sitemap: https://www.douban.com/sitemap_updated_index.xml
# Crawl-delay: 5

User-agent: Wandoujia Spider
Disallow: /

Requests 库

Reference:Python 网络爬虫与信息提取 (一):网络爬虫之规则
安装:pip install requests

Requests 库的主要方法

方法 说明
requests.request() 构造一个请求,支撑以下各方法的基础方法
requests.get() 获取 HTML 网页的主要方法,对应于 HTTP 的 GET,请求获取 url 位置的资源
requests.head() 获取 HTML 网页头信息的方法,对应于 HTTP 的 HEAD,请求获取 url 位置资源的响应消息报告,获得该资源的头部信息
requests.post() 向 HTML 网页提交 post 请求的方法,对应于 HTTP 的 POST,请求向 url 位置的资源后附加新的数据
requests.put() 向 HTML 网页提交 put 请求的方法,对应于 HTTP 的 PUT,请求向 url 位置存储一个资源,覆盖原 url 位置的资源
requests.patch() 向 HTML 网页提交局部修改请求的方法,对应于 HTTP 的 PATCH,请求局部更新 url 位置的资源,即改变该处资源的部分内容
requests.delete() 向 HTML 网页提交删除请求的方法,对应于 HTTP 的 DELETE,请求删除 url 位置存储的资源

get 方法

在爬虫中主要使用 get 方法,其他方法参数与 get 方法类似。
r = requests.get(url): 右边构造一个向服务器请求资源的 Requests 对象,左边返回一个包含服务器资源的 Response 对象给 r。
完整参数:requests.get(url,params=None,**kwargs), 实则由 request 方法封装。
Resonse 对象的五个属性:

属性 说明
r.status_code HTTP 请求的返回状态,200 表示连接成功,404 表示失败
r.text HTTP 响应内容的字符串形式,即 url 对应的页面内容
r.encoding HTTP header 中猜测的响应内容编码方式
r.apparent_encoding 从内容中分析出的响应内容编码方式(备选编码方式)
r.content HTTP 响应内容的二进制形式

异常处理

异常 说明
requests.ConnectionError 网络连接错误异常,如 DNS 查询失败、拒绝连接等
requests.HTTPError HTTP 错误异常
requests.URLRequired url 缺失异常
requests.TooManyRedirects 超过最大重定向次数,产生重定向异常
requests.ConnectTimeout 连接远程服务器超时异常
requests.Timeout 请求 url 超时,产生超时异常

使用 r.raise_for_status () 方法构建通用代码框架:

1
2
3
4
5
6
7
8
def getHTMLText(url)
try:
r = requests.get(url,timeout = 30)
r.raise_for_status()
r.encoding = r.apparent_encoding
return r.text
except:
return "Error"

Requests 库主要方法

requests.request(method,url,**kwargs)
- method (请求方式) 包括:GET/HEAD/POST/PUT/PATCH/DELETE/OPTIONS。
- **kwargs (控制访问参数) 包括:params (添加键值到 url 后)/data (字典 / 字节序列等作为 Request 的内容)/json/headers (HTTP 定制头)/cookies (Request 中的 cookie)/auth (元祖,支持 HTTP 认证)/files (传输文件)/timeout/proxies (设定访问代理服务器)/allow_redirects (重定向开关)/stream (获取内容立即下载开关)/verify (认证 SSL 证书开关)/cert (本地 SSL 证书路径)。

Beautiful Soup 库

Beautiful Soup 库可对 HTML/XML 格式进行解析并提取相关信息。
Beautiful Soup 库是解析 / 遍历 / 维护 "标签" 的功能库,引用方式:from bs4 import BeautifulSoup
Reference:Python 网络爬虫与信息提取 (二):网络爬虫之提取
安装:pip install beautifulsoup4

Beautiful Soup 库的解析器

解析器 使用方法 条件
HTML 解析器 BeautifulSoup(mk,'html.parser') 安装 bs4 库
lxml 的 HTML 解析器 BeautifulSoup(mk,'lxml') pip install lxml
lxml 的 XML 解析器 BeautifulSoup(mk,'xml') pip install lxml
html5lib 的解析器 BeautifulSoup(mk,'html5lib') pip install html5lib

Beautiful Soup 类的基本元素

基本元素 说明
Tag 标签,最基本的信息组织单元,分别用 <></> 标明开头和结尾
Name 标签的名字,<p>...</p> 的名字是 'p',格式:<tag>.name
Attributes 标签的属性,字典形式组织,格式:<tag>.attrs 或者 <tag>['attrs']
NavigableString 标签内非属性字符串,<>...</> 中的字符串,格式:<tag>.string
Comment 标签内字符串的注释部分,一种特殊的 Comment 类型
  • 任何存在于 HTML 语法中的标签都可用 soup.<tag> 访问获得,存在多个取第一个。
  • 每个 <tag> 有自己的名字,通过 <tag>.name 获取,字符串类型。
  • 每个 <tag> 有 0 或多个属性,字典类型。
  • 每个 <tag> 内有字符串,可以跨越多个标签层次,如果字符串不是注释,则 NavigableString 类型。
  • 如果 <tag> 内的字符串是注释,则返回 Comment 类型。

HTML 内容遍历方法

属性 说明 遍历方式
.contents |子节点的列表,将 <tag> 所有儿子节点存入列表 下行遍历 .children |子节点的迭代类型,与.contents类似,用于循环遍历儿子节点
.descendants |子孙节点的迭代类型,用于循环遍历子孙节点
.parent |节点的父标签 上行遍历 .parents |节点先辈标签的迭代类型,用于循环遍历先辈节点
.next_sibling |返回按照HTML文本顺序的下一个平行节点标签 平行遍历

.previous_sibling | 返回按照 HTML 文本顺序的上一个平行节点标签
.next_siblings | 迭代类型,返回按照 HTML 文本顺序的后续所有平行节点标签
.previous_siblings | 迭代类型,返回按照 HTML 文本顺序的前续所有平行节点标签

1
2
3
4
5
6
7
8
9
10
11
12
13
for child in soup.body.children:
print(child) #遍历儿子节点
for descendant in soup.body.descendants:
print(descendant) #遍历子孙节点
for parent in soup.body.parents:
if parent is None:
print(parent)
else:
print(parent.name)
for sibling in soup.a.next_siblings:
print(sibling)
for sibling in soup.a.previous_siblings:
print(sibling)

HTML 格式输出

使用 prettify() 方法,为 HTML 文本 <> 及其内容增加 '' 并且可用于标签 / 方法。

HTML 内容查找方法

<>.find_all(name,attrs,recursive,text='',**kwargs)
返回一个列表类型,存储查找的结果
name: 对标签名称的检索字符串
attrs: 对标签属性值的检索字符串,可标注属性检索,如果 attrs 是 class,则替换为 class_。
recursive: 是否对子孙全部搜索,默认 True
text='': 对字符串域进行检索,其中可以用 re.compile () 来进行正则表达式检索
.find_all() 扩展的七个方法:

方法 说明
<>.find() 搜索且只返回一个结果,同.find_a11() 参数
<>.find_parents() 在先辈节点中搜索,返回列表类型,同.find_all() 参数
<>.find_parent() 在先辈节点中返回一个结果,同.find() 参数
<>.find_next_siblings() 在后续平行节点中搜索,返回列表类型,同.find_a11() 参数
<>.find_next_sibling() 在后续平行节点中返回一个结果,同.find() 参数
<>.find_previous_siblings() 在前续平行节点中搜索,返回列表类型,同.find_a11() 参数
<>.find_previous_sibling() 在前续平行节点中返回一个结果,同.find() 参数

信息组织与提取方法

信息标记的三种形式及比较:
XML (Extensible Markup Language) 是最早的通用信息标记语言,可扩展性好,但繁琐;标签由名字和属性构成,形式有:

1
2
3
<name>...</name>
<name/>
<!-- -->

JSON(JavaScript Objection Notation)适合程序处理,较XML简洁;有类型的键值对,形式有:
1
2
3
"key":"value"
"key":["value1","value2"]
"key":{"subkey":"subvalue"}

YAML(YAML Ain't Markup Language)文本信息比例最高,可读性好;无类型的键值对,形式有:
1
2
3
4
5
6
key:value
key:#Comment
-value1
-value2
key:
subkey:subvalue

Re (正则表达式) 库

regular expression = regex = RE
是一种通用的字符串表达框架,用来简洁表达一组字符串的表达式,也可用来判断某字符串的特征归属。
Reference:Python 网络爬虫与信息提取 (三):网络爬虫之实战 ;Python 正则表达式指南

正则表达式的语法

普通字符

普通字符包括没有显式指定为元字符的所有可打印和不可打印字符。这包括所有大写和小写字母、所有数字、所有标点符号和一些其他符号。

非打印字符

非打印字符也可以是正则表达式的组成部分。下表列出了表示非打印字符的转义序列:

字符 描述
\cx 匹配由 x 指明的控制字符。例如,\cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 'c' 字符。
\f 匹配一个换页符。等价于 \x0c\cL
\n 匹配一个换行符。等价于 \x0a\cJ
\r 匹配一个回车符。等价于 \x0d\cM
\d 匹配任何数字。等价于 [0-9]
\D 匹配任何非数字。等价于 [^\d]
\w 匹配任何单词字符。等价于 [a-zA-Z0-9_]
\W 匹配任何非单词字符。等价于 [^\w]
\s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [\f\n\r\t\v]
\S 匹配任何非空白字符。等价于 [^\f\n\r\t\v]
\t 匹配一个制表符。等价于 \x09\cI
\v 匹配一个垂直制表符。等价于 \x0b\cK

特殊字符

所谓特殊字符,就是一些有特殊含义的字符。
若要匹配这些特殊字符,必须首先使字符 "转义",即,将反斜杠字符。下表列出了正则表达式中的特殊字符:

特别字符 描述
^ 匹配输入字符串的开始位置,在多行模式中匹配每一行的结尾。
除非在方括号表达式中使用,此时它表示不接受该字符集合。
$ 匹配输入字符串的结尾位置,在多行模式中匹配每一行的结尾。
(...) 标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。从表达式左边开始每遇到一个分组,则编号 + 1。分组表达式作为一个整体可以后接数量词。表达式中的 | 仅在分组中有效。
(?P<name>...) 分组,除了原有编号外再指定额外别名。
\<number>
(<?P=name)
引用编号为 number 或者别名为 name 的分组。
[...] 字符集,对应的位置可以是字符集中任意字符。 字符集中的字符可以逐个列出,也可以给出范围,如 [abc][a-c]。第一个字符如果是 ^ 则表示取反,如 [^abc] 表示不是 a 或 b 或 c 的其他字符。
所有的特殊字符在字符集中都失去其原有的特殊含义。在字符集中如果要使用]-^,可以在前面加上反斜杠,或者把]- 放在第一位,把 ^ 放在非第一位。
{n} n 是一个非负整数。匹配确定的 n 次。
{n,} n 是一个非负整数。至少匹配 n 次。{1,} 等价于 +{0,} 则等价于 *
{n,m} m 和 n 均为非负整数,其中 n<=m。最少匹配 n 次且最多匹配 m 次。{0,1} 等价于 ?
请注意在逗号和两个数之间不能有空格。
* 匹配前面的子表达式零次或多次。
+ 匹配前面的子表达式一次或多次。
. 匹配除换行符 \n 之外的任何单字符。在 DOTALL 模式中也能匹配换行符。
? 匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。
\ 将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符。
| 左右表达式任意一个。

经典实例

正则表达式 匹配字符串
^[A-Za-z]+$ 由 26 个字母组成的字符串
^[A-Za-z0-9]+$ 由 26 个字母和数字组成的字符串
^-?\d+$ 整数形式的字符串
^[1-9][0-9]*$ 正整数形式的字符串
[\u4e00-\u9fa5] 匹配中文字符

正则表达式相关注解

  1. 数量词的贪婪模式与非贪婪模式
    正则表达式通常用于在文本中查找匹配的字符串。Python 里数量词默认是贪婪的(在少数语言里也可能是默认非贪婪),总是尝试匹配尽可能多的字符;非贪婪的则相反,总是尝试匹配尽可能少的字符。实际我们一般使用非贪婪模式来提取。
  2. 反斜杠问题
    与大多数编程语言相同,正则表达式里使用 \ 作为转义字符,这就可能造成反斜杠困扰。假如你需要匹配文本中的字符 \,那么使用编程语言表示的正则表达式里将需要 4 个反斜杠 \\\\:前两个和后两个分别用于在编程语言里转义成反斜杠,转换成两个反斜杠后再在正则表达式里转义成一个反斜杠。
    Python 里的原生字符串很好地解决了这个问题,这个例子中的正则表达式可以使用 r'\\' 表示。同样,匹配一个数字的 \\d 可以写成 r'\d'。使用原生字符串,就不用担心漏写了反斜杠,写出来的表达式也更直观。

Re 库的基本使用

正则表达式的表示类型为 raw string 类型 (原生字符串类型), 表示为 r'text'

Re 库主要功能函数

函数 说明
re.search() 在一个字符串中搜索匹配正则表达式的第一个位置,返回 match 对象
re.match() 从一个字符串的开始位置起匹配正则表达式,返回 match 对象
re.findall() 搜索字符串,以列表类型返回全部能匹配的子串
re.split() 将一个字符串按照正则表达式匹配结果进行分割,返回列表类型
re.finditer() 搜索字符串,返回一个匹配结果的迭代类型,每个迭代元素是 match 对象
re.sub() 在一个字符串中替换所有匹配正则表达式的子串,返回替换后的字符串

search 方法

re.search(pattern, string, flags = 0)
参数 flag 是匹配模式,取值可以使用按位或运算符’|’表示同时生效。
其他方法的参数与 search 类似。sub 方法多一个参数 repl,re.sub(pattern, repl, string, flags = 0)

flag 说明
re.I(IGNORECASE) 忽略大小写
re.M(MULTILINE) 多行模式,改变 '^' 和 '$' 的行为
re.S(DOTALL) 点任意匹配模式,改变 '.' 的行为
re.L(LOCALE) 使预定字符类
re.U(UNICODE) 使预定字符类
re.X(VERBOSE) 详细模式。这个模式下正则表达式可以是多行,忽略空白字符,并可以加入注释。

Match 对象

一次匹配的结果,包含了很多关于此次匹配的信息,可以使用 Match 提供的可读属性或方法来获取这些信息。

属性

属性 说明
string 匹配时使用的文本。
re 匹配时使用的 Pattern 对象。
pos 文本中正则表达式开始搜索的索引。值与 Pattern.match () 和 Pattern.seach () 方法的同名参数相同。
endpos 文本中正则表达式结束搜索的索引。值与 Pattern.match () 和 Pattern.seach () 方法的同名参数相同。
lastindex 最后一个被捕获的分组在文本中的索引。如果没有被捕获的分组,将为 None。
lastgroup 最后一个被捕获的分组的别名。如果这个分组没有别名或者没有被捕获的分组,将为 None。

方法

方法 说明
group([group1, …]) 获得一个或多个分组截获的字符串;指定多个参数时将以元组形式返回。可以使用编号也可以使用别名。
编号 0 代表整个匹配的子串,不填写参数时,返回 group(0),没有截获字符串的组返回 None,截获了多次的组返回最后一次截获的子串。
groups([default]) 以元组形式返回全部分组截获的字符串。相当于调用 group(1,2,…last)
default 表示没有截获字符串的组以这个值替代,默认为 None。
groupdict([default]) 返回以有别名的组的别名为键、以该组截获的子串为值的字典,没有别名的组不包含在内。default 含义同上。
start([group]) 返回指定的组截获的子串在 string 中的起始索引(子串第一个字符的索引)。group 默认值为 0。
end([group]) 返回指定的组截获的子串在 string 中的结束索引(子串最后一个字符的索引 + 1)。group 默认值为 0。
span([group]) 返回 (start(group), end(group))
expand(template) 将匹配到的分组代入 template 中然后返回。template 中可以使用 \id\g<id>\g<name> 引用分组,但不能使用编号 0。\id\g<id> 是等价的,但 \10 将被认为是第 10 个分组,如果你想表达 \1 之后是字符'0',只能使用 \g<1>0

Pattern 对象

Pattern 对象是一个编译好的正则表达式,通过 Pattern 提供的一系列方法可以对文本进行匹配查找。
Pattern 不能直接实例化,必须使用 re.compile () 进行构造。
Pattern 提供了几个可读属性用于获取表达式的相关信息:

属性 说明
pattern 编译时用的表达式字符串。
flags 编译时用的匹配模式。数字形式。
groups 表达式中分组的数量。
groupindex 以表达式中有别名的组的别名为键、以该组对应的编号为值的字典,没有别名的组不包含在内。

所以 re.xxx(pattern, string, flags = 0) 可以写做 pattern.xxx(string, flag)

SQLite 库

SQLite 简介

  • 轻量级
  • 速度快
  • 无需部署
  • 没有服务器
  • 很少或不需要进行管理
  • 由于 SQLite 数据库的驱动已经在 Python 里面了,可以直接使用 import sqlite3

创建数据库

conn = sqlite3.connect("数据库名称.db")
如果数据库不存在,那么它就会被创建,最后将返回一个数据库对象。

创建游标对象

cur = conn.cursor()

创建数据库表

1
2
3
4
5
try:
cursor.execute('CREATE TABLE 表名称 (键1 键类型 PRIMARY KEY, 键2 键类型, 键3 键类型, ...)')
print("Table created successfully")
except Exception as e:
print(e)

这里 ' 键 i' 为键的名称,其中键 1 为主键,键类型有 TEXT(文本字符串,使用数据库编码 UTF-8、UTF-16BE 或 UTF-16LE 存储),REAL(浮点值,存储为 8 字节的 IEEE 浮点数字),INT(带符号的整数,根据值的大小存储在 1、2、3、4、6 或 8 字节中),NULL(空值)。

INSERT 操作

单个插入

1
2
3
4
5
try:
cursor.execute('INSERT INTO 表名称 (键1, 键2, 键3, ...) VALUES (值1, 值2, 值3, ...)')
print('data insert successfully')
except Exception as e:
print(e)

批量插入
1
2
3
4
5
6
try:
data = [("值11", "值12", "值13", ...), ("值21", "值22", "值23", ...), ("值31", "值32", "值33", ...)]
cursor.execute('INSERT INTO 表名称 (?, ?, ?, ...)', data)
print('data insert successfully')
except Exception as e:
print(e)

UPDATE 操作

1
2
3
4
5
try:
cursor.execute('UPDATE 表名称 SET 键1 = 值1, 键2 = 值2, 键3 = 值3, ... WHERE 键1 = 值1, 键2 = 值2, 键3 = 值3, ...')
print('data update successfully')
except Exception as e:
print(e)

SELECT 操作

1
2
3
4
5
6
try:
cursor.execute('SELECT 键1, 键2, 键3, ... FROM 表名称 WHERE 键1 = 值1, 键2 = 值2, 键3 = 值3, ...')
for row in cursor:
print(row)
except Exception as e:
print(e)

DELETE 操作

1
2
3
4
5
6
try:
c.execute("DELETE FROM 表名称 WHERE 键1 = 值1, 键2 = 值2, 键3 = 值3, ...")
for row in cursor:
print(row)
except Exception as e:
print(e)

关闭数据库

1
2
3
cursor.close() # 关闭Cursor
conn.commit() # 提交事务
conn.close() # 关闭Connection

将数据库中的数据导出为 csv 文件:

1
2
3
4
5
6
7
8
9
10
conn = sqlite3.connect('文件名.db')
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table'")
for i in cursor.fetchall():
tablename = i[0]
cursor.execute("SELECT * FROM '{}'".format(tablename))
with open(tablename + '.csv', 'w', newline='', encoding='utf_8_sig') as csv_file:
csv_writer = csv.writer(csv_file)
csv_writer.writerow([j[0] for j in cursor.description])
csv_writer.writerows(cursor)