一、什么是网络爬虫
网络爬虫(又称为网页蜘蛛,网络机器人,在 FOAF 社区中,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。
网络爬虫的规模:
- 小规模爬虫:爬取的数据量小,爬取速度不敏感。爬取一个或几个网页。
- 中规模爬虫:爬取数据规模较大,爬取速度敏感。爬取整个网站甚至多个系列网站。
- 大规模爬虫:全网爬取,只能定制开发。主要是搜索引擎。
二、爬虫的编写思路
网页的渲染方式一般有两种:后端渲染和前端渲染。
早期的网页大多使用后端渲染的方式,即在服务器上生成 HTML 然后发送给客户端。而现在越来越多的网站开始使用前端渲染,服务器只把最基本的 HTML 发送给客户端,然后客户端通过 ajax 技术向服务器请求数据并用 JavaScript 动态生成网页。
编写爬虫程序时针对不同的渲染方式有不同的编写方法:
如果是后端渲染,要爬取的数据一般就包含在 HTML 中。所以要先向指定 URL 发送请求,得到 HTML 文档,然后从 HTML 文档中提取所需的信息。
如果是前端渲染,数据一般包含在某个 API 的响应包中。所以先抓包找到该 API 的地址,然后分析请求方式、请求参数和请求头格式等,用 Python 模拟发送请求包,得到响应包后分析其结构并从中解析出所需的信息。
在 Python 中,发送网络请求可以用标准库中的 urllib3
,还可以用第三方库 requests
,后者使用起来更加简单方便。
从 HTML 文档中提取信息一般有两种方式:
- 直接通过正则表达式搜索整个文档,获取信息。Python 中的正则表达式库:
re
- 将 HTML 文档解析成一颗 DOM 树,然后通过特定语法搜索 DOM 树,获取信息。常用解析库:
lxml
、BeautifulSoup4 (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()
的基本参数有两个,url
和 data
,data
不再作为访问控制参数
3.2 get 方法
函数原型:requests.get(url[, **kwargs])
1 | rsp = requests.get(url) # 请求 url |
下载网页等字符数据:直接将 rsp.text
的内容保存到文件中
下载图片、音乐、视频等二进制数据:
直接将
rsp.content
以二进制方式写入到文件中用流模式下载
1
2
3
4rsp = 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
2files = {'file': open('report.xls', 'rb')}
rsp = requests.post(url, files = files)流上传
1
2with 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 | import requests |
四、lxml 解析网页
lxml
是一个 C 语言写的 xml/html 解析库,速度快,容错能力强。
下面只对怎样解析 html 做一个简单记录:
1 | from lxml import etree, html |
尽可能使用 xpath
,lxml
+ xpath
非常强大。
但有时候 lxml
会将 DOCTYPE 给删掉,不知道为什么。
五、BeautifulSoup4 (bs4) 解析网页
5.1 创建 BeautifulSoup 对象
从字符串创建:
1
soup = bs4.BeautifulSoup(字符串,解析器)
从 文件 创建:
1
soup = bs4.BeautifulSoup(open('index.html', 'r'), 'html.parser')
或
1
2with 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 中有四种对象:Tag
,NavigableString
,BeautifulSoup
,Comment
Tag
:通俗来讲就是 HTML 中的标签NavigableString
:标签中间的字符内容,其子标签为None
。该对象也可以当做特殊的Tag
对象BeautifulSoup
:用BeautifulSoup
函数处理HTML
/XML
代码后生成的对象,使用时可以当做特殊的Tag
对象,其.name
属性为[document]
,.attrs
属性为None
Comment
:Tag.string
会将注释内容也显示出来,且去掉了注释符号,所以区分 标签内容 和 注释内容 只能通过判断它们的类型,注释内容的类型为Comment
5.4 Tag 的方法和属性
基本属性:
Tag.tag
:该Tag
的子标签Tag.name
:标签名Tag.attrs
:以字典形式存储的标签属性(多值属性会用列表处理)可以用三种方法获取某个标签属性:
Tag['class']
Tag.get('class')
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
2for string in Tag.strings:
print(repr(string))Tag.stripped_strings
:.strings
产生的内容可能包含很多 空格和空行,使用该方法可以去除多余空白1
2for 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
对象。
name
参数(查找标签):传字符串(查找指定标签):
soup.find_all('a')
传正则表达式(查找符合条件的标签)(Beautiful Soup 会通过正则表达式的
match()
来匹配内容)1
2for tag in soup.find_all(re.compile("^b")):
print(tag.name) # output: body b传列表(查找多个指定的标签名):
soup.find_all(["a", "p"])
(查找所有 a 和 p 标签)传
True
(True
可以匹配任何值)传函数名(自定义查找标准)
函数只接受一个元素作为参数。
1
2
3
4def 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) # 函数名后不加括号
keyword
参数(查找标签属性):可以将属性名当做关键字参数来搜索,如:
soup.find_all(href = "http://www.baidu.com/")
,可以返回文档中href
属性的值是"http://www.baidu.com/"
的标签,构成列表。soup.find_all(href = True)
:返回文档中拥有href
属性的所有标签,因为True
为匹配任何值。使用多个
keyword
参数可以同时过滤 Tag 的多个属性:1
2soup.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_
text
参数(查找标签内容):通过
text
参数可以搜索文档中的字符串内容。与
name
参数的可选值一样,text
参数接受 字符串、正则表达式、列表、True
其他参数:
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')
:以 列表 形式获取指定属性的值。如果它是多值属性,那么列表中存在多个字符串,否则列表中就只有一个字符串。