一、什么是网络爬虫

网络爬虫(又称为网页蜘蛛,网络机器人,在 FOAF 社区中,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。

网络爬虫的规模:

  1. 小规模爬虫:爬取的数据量小,爬取速度不敏感。爬取一个或几个网页。
  2. 中规模爬虫:爬取数据规模较大,爬取速度敏感。爬取整个网站甚至多个系列网站。
  3. 大规模爬虫:全网爬取,只能定制开发。主要是搜索引擎。

二、爬虫的编写思路

网页的渲染方式一般有两种:后端渲染和前端渲染。

早期的网页大多使用后端渲染的方式,即在服务器上生成 HTML 然后发送给客户端。而现在越来越多的网站开始使用前端渲染,服务器只把最基本的 HTML 发送给客户端,然后客户端通过 ajax 技术向服务器请求数据并用 JavaScript 动态生成网页。

编写爬虫程序时针对不同的渲染方式有不同的编写方法:

如果是后端渲染,要爬取的数据一般就包含在 HTML 中。所以要先向指定 URL 发送请求,得到 HTML 文档,然后从 HTML 文档中提取所需的信息

如果是前端渲染,数据一般包含在某个 API 的响应包中。所以先抓包找到该 API 的地址,然后分析请求方式、请求参数和请求头格式等,用 Python 模拟发送请求包,得到响应包后分析其结构并从中解析出所需的信息

在 Python 中,发送网络请求可以用标准库中的 urllib3,还可以用第三方库 requests,后者使用起来更加简单方便。

从 HTML 文档中提取信息一般有两种方式:

  1. 直接通过正则表达式搜索整个文档,获取信息。Python 中的正则表达式库:re
  2. 将 HTML 文档解析成一颗 DOM 树,然后通过特定语法搜索 DOM 树,获取信息。常用解析库:lxmlBeautifulSoup4 (bs4)

两种方式各有优缺点,需要结合实际情况合理搭配使用。

如果要编写中规模爬虫,爬虫框架 Scrapy 是个不错的选择。Scrapy 里自带了发送网络请求和解析网页的功能,无需再使用其他库。

如果要爬取的网站很复杂(主要体现在 API 或者身份验证方式很复杂),可以使用 selenium。参见另一篇文章:Python + selenium 操作浏览器

三、使用 requests 请求网页

requests 的运行流程:构造一个 Request 对象发送给服务器,将服务器返回的内容构造成一个 Response 对象返回。

requests 库的 7 个方法:request(), get(), post(), head(), patch(), put(), delete(),其中后六个分别对应 HTTP 对 URL 位置的资源的六种操作。

requests模块中实际上只有一个方法 request(),其他六个方法都是基于该方法。

3.1 requests 库的根本:request 方法

函数原型:requests.request(method, url[, **kwargs])

method 为请求方式,有 GET, POST, HEAD, PATCH, PUT, delete, OPTIONS。将 method 设置为 GET,那么这个函数的作用就相当于 requests.get(),其他同理。OPTIONS 为获取一些服务器和客户端打交道的参数,很少用。

**kwargs 包含 13 个访问控制参数:

参数 含义
headers 一个字典。定制请求头。
timeout 设置请求超时时间,单位 s。如果在请求时间内没有请求成功,抛出一个 Timeout 异常
params 一个字典,将字典中的数据自动转换为 key1=value1&key2=value2 的形式向指定 url 发送请求
proixes 设置代理,值是一个字典,字典格式:字典名 = {'http': 'http://10.10.1.10:3128', 'https': 'https://10.10.1.10:1080'}。可以只写 http 或 https。有 https 时默认先使用 https
data 字典或字节序列或文件,一般在向服务器提交数据时使用。一般用于 POST 方法。
json 一个字典,将数据以 json 格式提交。
files 一个字典,字典格式为 file = {'file': open('report.xls', 'rb')}。用于上传文件。
stream 值为 True / False。表示是否开启流模式,默认为 False
allow_redirects 值为 True / False。表示是否允许重定向,默认为 True
verify 值为 True / False。是否认证 SSL 证书,默认为 True
cookies 字典或 CookieJar。从 HTTP 协议中解析 cookie。
auth 一个元组。用于身份认证功能。
cert 本地 SSL 证书路径。

基于 request() 的其他方法也都是这 13 个参数,只不过有些函数的有些访问控制参数不再作为访问控制参数,而是作为默认参数。如 requests.post() 的基本参数有两个,urldatadata 不再作为访问控制参数

3.2 get 方法

函数原型:requests.get(url[, **kwargs])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
rsp = requests.get(url)		# 请求 url

rsp = requests.get(url, headers = 字典)
# 自定义请求头,如 headers = {'user-agent': 'my-app/0.0.1'} rsp = requests.get(url, headers=headers)

rsp = requests.get(url, timeout = 时间) # 设置超时时间

rsp = requests.get(url, params = 字典)
# 将字典中的数据自动转换为 key1=value1&key2=value2 的形式向指定 url 发送请求

rsp = requests.get(url, proxies = 字典) # 设置代理

rsp = requests.get(url, stream=True)
# 启用流下载。默认为 False
# 若关闭流下载,数据会在请求时就下载并保存到内容中
# 若开启,只有响应头被立即下载,数据不会立即下载,而是在打印或写入文件时再一块一块地下载。在下载大文件时,最好用这种方式,占用内存少

下载网页等字符数据:直接将 rsp.text 的内容保存到文件中

下载图片、音乐、视频等二进制数据:

  1. 直接将 rsp.content 以二进制方式写入到文件中

  2. 用流模式下载

    1
    2
    3
    4
    rsp = requests.get(url, stream = True)		//打开流下载模式
    with open('file.mp3', 'wb') as file:
    for data in rsp.iter_content(1024 * 10): //指定每块为 10240 字节,一块一块的下载,并写入文件
    file.write(data)

3.3 post 方法

函数原型:requests.post(url[, **kwargs])

基本用法:rsp = requests.post(url, data = 字典)

上传文件:

  1. 直接上传

    1
    2
    files = {'file': open('report.xls', 'rb')}
    rsp = requests.post(url, files = files)
  2. 流上传

    1
    2
    with open('massive-body', 'rb') as f:
    requests.post('http://some.url/streamed', data=f)

3.4 Response 对象的常用属性和方法

属性、方法 含义
rsp.url 服务器返回的 url (重定向后的 url)
rsp.content 服务器返回的 原始数据(即 二进制数据)
rsp.text 将服务器返回的二进制数据 根据 rsp.encoding 的值 编码后的数据
rsp.encoding 指定用哪种编码类型来将 rsp.text 编码(默认值是从 HTTP header 中猜测出来的)
rsp.apparent_encoding 从内容中分析出的响应内容编码方式(一般用作备选编码方式)
rsp.status_code 状态码
rsp.headers 服务器响应的 headers
rsp.requests.headers 发送到服务器的 headers
rsp.history 可用于追踪重定向
rsp.raise_for_status() 如果状态码是 200,不执行操作;如果不是 200,会产生一个 requests.HTTPError 异常

3.5 requests 库的异常

异常 含义
requests.ConnectionError 网络连接错误异常,如 DNS 查询失败、拒绝连接等
requests.HTTPError HTTP 错误异常
requests.ConnectTimeout 连接服务器超时(仅指与服务器连接过程产生的超时异常)
requests.Timeout 请求超时(发出 URL 请求到获得内容整个过程的超时异常)
requests.URLRequired URL 缺失异常
requests.TooManyRedirects 超过 requests 内定的最大重定向次数,产生重定向异常

3.6 session 的使用

编写爬虫时经常会遇到需要登录的情况,目前来说大多数网站还是采用 session + cookies 的方式来验证用户身份。在用户登录后,网站服务器会返回一个 SESSIONID 并以 cookie 形式保存在本地,后续访问该网站时每个请求都必须携带这个 cookie,否则会重定向到登录页面。

requests 库提供了一个 session 类用来管理会话,用 session 对象发送请求时,requests 会自动将服务器返回的 cookies 保存在 session 对象中,并在以后每次请求时携带,简化了爬虫的编写。

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

s = requests.session() # 创建 session 对象
s.post("https://xxx.xxx/login", data={ # 假设这是登录
"username": "xxx",
"password": "xxx"
})
# 以后发任何请求时将 requests 换成 s 即可

print(s.cookies) # s.cookies 是一个 cookie对象 列表

s.cookies["cookie_name"] = "cookie_value" # 有时候可能需要手动添加或修改 cookie

四、lxml 解析网页

lxml 是一个 C 语言写的 xml/html 解析库,速度快,容错能力强。

下面只对怎样解析 html 做一个简单记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from lxml import etree, html
import requests

rsp = requests.get("https://www.baidu.com/")
html_doc = html.fromstring(rsp.content) # 解析 html 文档,得到一个 <lxml.html.HtmlElement> 类型的对象

# 获取元素
els = html_doc.xpath(xpath) # 通过 xpath 语法获取元素。结果是一个列表
els = html_doc.cssselect(cssselect) # 通过 css 选择器语法获取元素。结果是一个列表

# 删除元素
el.drop_tree() # 获取到元素后,可以使用该方法将其删除

# 添加元素
parent_el.append(el) # 在父元素的最后插入一个元素,el 必须是 <lxml.html.HtmlElement> 类型
parent_el.insert(index, el) # 在父元素的指定位置插入元素

# 转换为 string
etree.tostring(html_doc) # 注意:结果是 bytes 类型

尽可能使用 xpathlxml + xpath 非常强大。

但有时候 lxml 会将 DOCTYPE 给删掉,不知道为什么。

五、BeautifulSoup4 (bs4) 解析网页

5.1 创建 BeautifulSoup 对象

  • 从字符串创建:

    1
    soup = bs4.BeautifulSoup(字符串,解析器)
  • 从 文件 创建:

    1
    soup = bs4.BeautifulSoup(open('index.html', 'r'), 'html.parser')

    1
    2
    with open('index.html', 'r') as file:
    soup = bs4.BeautifulSoup(file)

5.2 常用解析器及其优缺点

解析器 使用方法 优势 劣势
Python标准库 BeautifulSoup(markup, "html.parser") Python 的内置标准库;执行速度适中;容错能力强 Python 2.7.3 or 3.2.2)前 的版本中容错能力差
lxml HTML 解析器 BeautifulSoup(markup, "lxml") 速度快;容错能力强 需要安装C语言库
lxml XML 解析器 BeautifulSoup(markup, ["lxml", "xml"]);BeautifulSoup(markup, "xml") 速度快;唯一支持XML的解析器 需要安装 C 语言库
html5lib BeautifulSoup(markup, "html5lib") 最好的容错性;不依赖外部扩展;生成 HTML5 格式的文档 速度慢

5.3 bs4 中的四种对象

bs4 中有四种对象:TagNavigableStringBeautifulSoupComment

  • Tag:通俗来讲就是 HTML 中的标签

  • NavigableString:标签中间的字符内容,其子标签为 None。该对象也可以当做特殊的 Tag 对象

  • BeautifulSoup:用 BeautifulSoup 函数处理 HTML / XML 代码后生成的对象,使用时可以当做特殊的 Tag 对象,其 .name 属性为 [document].attrs 属性为 None

  • CommentTag.string 会将注释内容也显示出来,且去掉了注释符号,所以区分 标签内容 和 注释内容 只能通过判断它们的类型,注释内容的类型为 Comment

5.4 Tag 的方法和属性

基本属性:

  • Tag.tag:该 Tag 的子标签

  • Tag.name:标签名

  • Tag.attrs:以字典形式存储的标签属性(多值属性会用列表处理)

    可以用三种方法获取某个标签属性:

    1. Tag['class']

    2. Tag.get('class')

    3. Tag.attrs['class']

    前两种是 Tag 对象自带用法,最后一种是字典的用法

    标签的属性可以被添加、删除或修改,操作方法与字典是一样的。如:

    • Tag['class'] = 'red'

    • Tag.get('class') = 'red'

    • Tag.attrs['class'] = 'red'

  • Tag.string:标签中间的文本内容,该文本内容是 NavigableString 对象

    • 如果一个标签里面没有标签了,那么 .string 就会返回标签里面的内容。

    • 如果标签里面只有唯一的一个标签,那么 .string 也会返回最里面的内容。

    • 如果 Tag 中包含多个子节点,无法确定 .string 方法应该调用哪个子节点的内容, 输出结果则为 None

5.5 遍历文档树

节点内容:

  • Tag.strings:如果一个标签中包含多个子节点,可以使用 .strings 生成器获取所有内容

    1
    2
    for string in Tag.strings:
    print(repr(string))
  • Tag.stripped_strings.strings 产生的内容可能包含很多 空格和空行,使用该方法可以去除多余空白

    1
    2
    for string in Tag.stripped_strings:
    print(repr(string))

子节点:

  • Tag.contents:以 列表形式 返回当前 Tag 的 直接子节点

  • Tag.children:以 list 生成器对象的形式返回当前 Tag 的 直接子节点

  • Tag.descendants:返回一个包含当前 Tag 所有子孙节点的对象(遍历方式为深度优先)

父节点:

  • Tag.parent:输出 直接父节点

  • Tag.parents:输出 包含所有父节点的可迭代对象,迭代时会 从里到外 输出所有父节点

兄弟节点(同级节点):

  • Tag.next_sibling:输出下一个兄弟节点

  • Tag.previous_sibling:输出上一个兄弟节点

  • Tag.next_siblings:返回 包含所有后面的兄弟节点 的可迭代对象,迭代时要先用 repr 转化

  • Tag.previous_siblings:返回 包含所有前面的兄弟节点 的可迭代对象,迭代时要先用 repr 转化

前后节点(不分级别):

  • Tag.next_element:输出下一个节点

  • Tag.previous_element:输出上一个节点

  • Tag.next_elements:返回 包含所有后面的节点 的可迭代对象,迭代时要先用 repr 转化

  • Tag.previous_elements:返回 包含所有前面的节点 的可迭代对象,迭代时要先用 repr 转化

5.6 搜索文档树

find_all() 为例,find_all() 返回一个可迭代对象,对象中的每个元素都是一个 Tag 对象。

  1. name 参数(查找标签):

    • 传字符串(查找指定标签):soup.find_all('a')

    • 传正则表达式(查找符合条件的标签)(Beautiful Soup 会通过正则表达式的 match() 来匹配内容)

      1
      2
      for tag in soup.find_all(re.compile("^b")):
      print(tag.name) # output: body b
    • 传列表(查找多个指定的标签名):soup.find_all(["a", "p"])(查找所有 a 和 p 标签)

    • TrueTrue 可以匹配任何值)

    • 传函数名(自定义查找标准)

      函数只接受一个元素作为参数。

      1
      2
      3
      4
      def has_class_but_no_id(tag):				# 参数是要遍历的每个节点
      return tag.has_attr('class') and not tag.has_attr('id')

      soup.find_all(has_class_but_no_id) # 函数名后不加括号
  2. keyword 参数(查找标签属性):

    • 可以将属性名当做关键字参数来搜索,如:soup.find_all(href = "http://www.baidu.com/"),可以返回文档中 href 属性的值是 "http://www.baidu.com/" 的标签,构成列表。

    • soup.find_all(href = True):返回文档中拥有 href 属性的所有标签,因为 True 为匹配任何值。

    • 使用多个 keyword 参数可以同时过滤 Tag 的多个属性:

      1
      2
      soup.find_all(href=re.compile("elsie"), id='link1')
      # 匹配结果:[<a class="sister" href="http://example.com/elsie" id="link1">three</a>]
    • 因为 class 是 Python 中的关键字,所以以 class 作为 keyword 参数时,要写成 class_

  3. text 参数(查找标签内容):

    通过 text 参数可以搜索文档中的字符串内容。

    name 参数的可选值一样, text 参数接受 字符串、正则表达式、列表、True

  4. 其他参数:

    • limit 参数:限制查找结果个数。如:soup.find_all("a", limit=2)
  • recursive 参数:默认为 True,此时查找当前 Tag 的所有子节点。若设为 False,则只查找直接子节点。如:soup.html.find_all("title", recursive=False)

find() 方法的用法与 find_all() 相同,只不过 find() 只返回搜索到的一地个元素。

select() 方法以 css 选择器为参数,返回匹配到的所有元素。

5.7 其他方法

  • Tag.prettify():格式化输出内容。

  • Tag.get_attribute_list('class'):以 列表 形式获取指定属性的值。如果它是多值属性,那么列表中存在多个字符串,否则列表中就只有一个字符串。