Python与爬虫

简介

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

场景分类

  • 通用爬虫:抓取系统重要组成部分。抓取的是一整张页面数据。
  • 聚焦爬虫:建立在通用爬虫基础上。抓取的是页面中特定的局部内容。
  • 增量式爬虫:检测网站中数据更新的情况。只会抓取最新更新出来的数据。

协议

robots.txt协议

robots是告诉搜索引擎,你可以爬取收录我的什么页面,你不可以爬取和收录我的那些页面。robots很好的控制网站那些页面可以被爬取,那些页面不可以被爬取。

主流的搜索引擎都会遵守robots协议。并且robots协议是爬虫爬取网站第一个需要爬取的文件。爬虫爬取robots文件后,会读取上面的协议,并准守协议爬取网站,收录网站。

robots文件是一个纯文本文件,也就是常见的.txt文件。在这个文件中网站管理者可以声明该网站中不想被robots访问的部分,或者指定搜索引擎只收录指定的内容。因此,robots的优化会直接影响到搜索引擎对网站的收录情况。

1
Crawl-delay: 5 指定爬虫访问间隔为5

注意事项

  1. 不要禁止爬虫爬取网站的所有,因为从经验来看,如果屏蔽一次,解封后好一段时间爬虫都不会来你网站,收录成为问题。
  2. 代码后需要【冒号+空格+斜杆】 ,比如“Disallow: /?
  3. 当网站为静态路径时,需要屏蔽掉所有动态链接。网站中存在一种链接被收录即可,避免一个页面2个链接。代码如下“Disallow: /? ”表示禁止所有带 ?号的网址被爬取。通常动态网址带有“?”“=”等。
  4. 根据自己网站情况定,屏蔽不需要收录的网址。

伪装策略

我们知道即使是一些规模很小的网站通常也会对来访者的身份做一下检查,如验证请求 Headers,而对于那些上了一定规模的网站就更不用说了。因此,为了让我们的爬虫能够成功爬取所需数据信息,我们需要让爬虫进行伪装,简单来说就是让爬虫的行为变得像普通用户访问一样。

Request Headers问题

在上图中,我们可以看到 Request Headers 中包含 Referer 和 User-Agent 两个属性信息,Referer 的作用是告诉服务器该网页是从哪个页面链接过来的,User-Agent 中文是用户代理,它是一个特殊字符串头,作用是让服务器能够识别用户使用的操作系统、CPU 类型、浏览器等信息。通常的处理策略是:1)对于要检查 Referer 的网站就加上;2)对于每个 request 都添加 User-Agent。

IP限制问题

有时我们可能会对一些网站进行长期或大规模的爬取,而我们在爬取时基本不会变换 IP,有的网站可能会监控一个 IP 的访问频率和次数,一但超过这个阈值,就可能认作是爬虫,从而对其进行了屏蔽,对于这种情况,我们要采取间歇性访问的策略。

通常我们爬取是不会变换 IP 的,但有时可能会有一些特殊情况,要长时间不间断对某网站进行爬取,这时我们就可能需要采用 IP 代理的方式,但这种方式一般会增加我们开销,也就是可能要多花钱。

Requests库

所谓爬虫就是模拟客户端发送网络请求,获取网络响应,并按照一定的规则解析获取的数据并保存的程序。要说 Python 的爬虫必然绕不过 Requests 库。

简介

1
2
Requests 唯一的一个非转基因的 Python HTTP 库,人类可以安全享用。警告:非专业使用其他 HTTP 库会导致危险的副作用,包括:安全缺陷症、冗余代码症、重新发明轮子症、啃文档症、抑郁、头疼、甚至死亡。
这个介绍还是比较生动形象的,便不再多说。安装使用终端命令 `pip install requests` 。

快速上手

发送请求

导入 Requests 模块:

1
import requests

获取网页:

1
r = requests.get('http://xxx.xxx')

此时,我们获取了 Response 对象 r,我们可以通过 r 获取所需信息。Requests 简便的 API 意味着所有 HTTP 请求类型都是显而易见的,我们来看一下使用常见 HTTP 请求类型 get、post、put、delete 的示例:

1
2
3
4
r = requests.head('http://xxx.xxx/get')
r = requests.post('http://xxx.xxx/post', data = {'key':'value'})
r = requests.put('http://xxx.xxx/put', data = {'key':'value'})
r = requests.delete('http://xxx.xxx/delete')

通常我们会设置请求的超时时间,Requests 使用 timeout 参数来设置,单位是秒,示例如下:

1
r = requests.head('http://xxx.xxx/get', timeout=1)

参数传递

在使用 get 方式发送请求时,我们会将键值对形式参数放在 URL 中问号的后面,如:http://xxx.xxx/get?key=val ,Requests 通过 params 关键字,以一个字符串字典来提供这些参数。比如要传 key1=val1key2=val2http://xxx.xxx/get,示例如下:

1
2
pms= {'key1': 'val1', 'key2': 'val2'}
r = requests.get("http://xxx.xxx/get", params=pms)

Requests 还允许将一个列表作为值传入:

1
pms= {'key1': 'val1', 'key2': ['val2', 'val3']}

:字典里值为 None 的键都不会被添加到 URL 的查询字符串里。

响应内容

我们来获取一下服务器的响应内容,这里地址 https://api.github.com 为例:

1
2
3
4
5
6
import requests
r = requests.get('https://api.github.com')
print(r.text)

# 输出结果
# {"current_user_url":"https://api.github.com/user","current_user...

当访问 r.text 之时,Requests 会使用其推测的文本编码,我们可以使用 r.encoding 查看其编码,也可以修改编码,如:r.encoding = 'GBK',当改变了编码,再次访问 r.text 时,Request 都将会使用 r.encoding 的新值。

  1. 二进制响应内容 比如当我们要获取一张图片的数据,会以二进制的方式获取响应数据,示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import requests
    from PIL import Image
    from io import BytesIO
    r = requests.get('https://mp.weixin.qq.com/mp/qrcode?scene=10000004&size=102&__biz=MzU3Mzk1ODA5OQ==&mid=2247483907&idx=1&sn=0e2bcd8811313327582623246795a26e&send_time=')
    i = Image.open(BytesIO(r.content))
    print(i)

    #输出结果
    #<PIL.BmpImagePlugin.BmpImageFile image mode=L size=129x129 at 0x22B7A7A8510>
  2. JSON响应内容 Requests 中已经内置了 JSON 解码器,因此我们可以很容易的对 JSON 数据进行解析,示例如下:

    1
    2
    3
    4
    5
    6
    7
    import requests
    r = requests.get('https://api.github.com')
    r.json()

    #输出结果
    #{'current_user_url': 'https://api.github.com/user', 'current_user_authorizations_html_url': 'https://github.com/settings/connections/applications{/client_id}', 'authorizations_url': 'https://api.github.com/authorizations', 'code_search_url': 'https://api.github.com/search/code?q={query}{&page,per_page,sort,order}', 'commit_search_url': 'https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}',
    ......

    :成功调用 r.json() 并不一定响应成功,有的服务器会在失败的响应中包含一个 JSON 对象(比如 HTTP 500 的错误细节),这时我们就需要查看响应的状态码了 r.status_code 或 r.raise_for_status(),成功调用时 r.status_code 为 200,r.raise_for_status() 为 None。

自定义请求头

当我们要给请求添加 headers 时,只需给 headers 参数传递一个字典即可,示例如下:

1
2
3
url = 'http://xxx.xxx'
hds= {'user-agent': 'xxx'}
r = requests.get(url, headers=hds)

:自定义 headers 优先级是低于一些特定的信息的,如:在 .netrc 中设置了用户认证信息,使用 headers 设置的授权就不会生效,而当设置了 auth 参数,.netrc 的设置会无效。所有的 headers 值必须是 string、bytestring 或者 unicode,通常不建议使用 unicode。

重定向与历史

默认情况下,Requests 会自动处理除了 HEAD 以外的所有重定向,可以使用响应对象的 history 属性来追踪重定向,其返回为响应对象列表,这个列表是按照请求由晚到早进行排序的,看一下示例:

1
2
3
4
5
6
import requests
r = requests.get('http://github.com')
print(r.history)

# 输出结果
# [<Response [301]>]

如果使用的是get、post、put、delete、options、patch 可以使用 allow_redirects 参数禁用重定向。示例如下:

1
r = requests.get('http://xxx.xxx', allow_redirects=False)

错误与异常

当遇到网络问题(如:DNS 查询失败、拒绝连接等)时,Requests 会抛出 ConnectionError 异常;在 HTTP 请求返回了不成功的状态码时, Response.raise_for_status() 会抛出 HTTPError 异常;请求超时,会抛出 Timeout 异常;请求超过了设定的最大重定向次数,会抛出 TooManyRedirects 异常。所有 Requests 显式抛出的异常都继承自 requests.exceptions.RequestException。

BeautifulSoup库

BeautifulSoup 是一个可以从 HTML 或 XML 文件中提取数据的 Python 库,它能够将 HTML 或 XML 转化为可定位的树形结构,并提供了导航、查找、修改功能,它会自动将输入文档转换为 Unicode 编码,输出文档转换为 UTF-8 编码。

BeautifulSoup 支持 Python 标准库中的 HTML 解析器和一些第三方的解析器,默认使用 Python 标准库中的 HTML 解析器,默认解析器效率相对比较低,如果需要解析的数据量比较大或比较频繁,推荐使用更强、更快的 lxml 解析器。

安装

BeautifulSoup 安装

如果使用 Debain 或 ubuntu 系统,可以通过系统的软件包管理来安装:apt-get install Python-bs4,如果无法使用系统包管理安装,可以使用 pip install beautifulsoup4 来安装。

第三方解析器安装

如果需要使用第三方解释器 lxml 或 html5lib,可是使用如下命令进行安装:apt-get install Python-lxml(html5lib)pip install lxml(html5lib)

解析器 使用方法 优势 劣势
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格式的文档。 速度慢;不依赖外部扩展。

快速上手

将一段文档传入 BeautifulSoup 的构造方法,就能得到一个文档的对象,可以传入一段字符串或一个文件句柄,示例如下:

index.html文件

1
2
3
4
5
6
7
8
9
10
11
12
html = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BeautifulSoup学习</title>
</head>
<body>
Hello BeautifulSoup
</body>
</html>
'''

使用示例

1
2
3
4
5
from bs4 import BeautifulSoup
#使用默认解析器
soup = BeautifulSoup(html,'html.parser')
#使用 lxml 解析器
soup = BeautifulSoup(html,'lxml')

举例本地文件

1
2
3
4
5
from bs4 import BeautifulSoup
#使用默认解析器
soup = BeautifulSoup(html,'html.parser')
#使用 lxml 解析器
soup = BeautifulSoup(html,'lxml')

对象的种类

BeautifulSoup 将 HTML 文档转换成一个树形结构,每个节点都是 Python 对象,所有对象可以归纳为4种:TagNavigableStringBeautifulSoupComment

Tag 对象

Tag 对象与 HTML 或 XML 原生文档中的 tag 相同,示例如下:

1
2
3
4
5
6
7
8
9
10
11
soup = BeautifulSoup('<title>BeautifulSoup学习</title>','lxml')
tag = soup.title
tp =type(tag)
print(tag)
print(tp)

#输出结果
'''
<title>BeautifulSoup学习</title>
<class 'bs4.element.Tag'>
'''

Tag 有很多方法和属性,这里先看一下它的的两种常用属性:nameattributes

我们可以通过 .name 来获取 tag 的名字,示例如下:

1
2
3
4
5
6
soup = BeautifulSoup('<title>BeautifulSoup学习</title>','lxml')
tag = soup.title
print(tag.name)

#输出结果
#title

我们还可以修改 tag 的 name,示例如下:

1
2
3
4
5
tag.name = 'title1'
print(tag)

#输出结果
#<title1>BeautifulSoup学习</title1>

一个 tag 可能有很多个属性,先看一它的 class 属性,其属性的操作方法与字典相同,示例如下:

1
2
3
4
5
6
7
soup = BeautifulSoup('<title class="tl">BeautifulSoup学习</title>','lxml')
tag = soup.title
cls = tag['class']
print(cls)

#输出结果
#['tl']

我们还可以使用 .attrs 来获取,示例如下:

1
2
3
4
5
ats = tag.attrs
print(ats)

#输出结果
#{'class': ['tl']}

tag 的属性可以被添加、修改和删除,示例如下:

1
2
3
4
5
6
7
8
#添加 id 属性
tag['id'] = 1

#修改 class 属性
tag['class'] = 'tl1'

#删除 class 属性
del tag['class']

NavigableString 类是用来包装 tag 中的字符串内容的,使用 .string 来获取字符串内容,示例如下:

1
str = tag.string

可以使用 replace_with() 方法将原有字符串内容替换成其它内容 ,示例如下:

1
tag.string.replace_with('BeautifulSoup')

BeautifulSoup 对象

BeautifulSoup 对象表示的是一个文档的全部内容,它并不是真正的 HTML 或 XML 的 tag,因此它没有 nameattribute 属性,为方便查看它的 name 属性,BeautifulSoup 对象包含了一个值为 [document] 的特殊属性 .name,示例如下:

1
2
3
4
5
soup = BeautifulSoup('<title class="tl">BeautifulSoup学习</title>','lxml')
print(soup.name)

#输出结果
#[document]

Comment 对象

Comment 对象是一个特殊类型的 NavigableString 对象,它会使用特殊的格式输出,看一下例子:

1
2
3
4
5
6
7
8
9
10
soup = BeautifulSoup('<title class="tl">Hello BeautifulSoup</title>','html.parser')
comment = soup.title.prettify()
print(comment)

#输出结果
'''
<title class="tl">
Hello BeautifulSoup
</title>
'''

我们前面看的例子中 tag 中的字符串内容都不是注释内容,现在将字符串内容换成注释内容,我们来看一下效果:

1
2
3
4
5
6
soup = BeautifulSoup('<title class="tl"><!--Hello BeautifulSoup--></title>','html.parser')
str = soup.title.string
print(str)

#输出结果
#Hello BeautifulSoup

通过结果我们发现注释符号 <!----> 被自动去除了,这一点我们要注意一下。

搜索文档树

BeautifulSoup 定义了很多搜索方法。

find_all()

find_all() 方法搜索当前 tag 的所有 tag 子节点,方法详细如下:find_all(name=None, attrs={}, recursive=True, text=None,limit=None, **kwargs),来具体看一下各个参数。

name 参数可以查找所有名字为 name 的 tag,字符串对象会被自动忽略掉,示例如下:

1
2
3
4
5
soup = BeautifulSoup('<title class="tl">Hello BeautifulSoup</title>','html.parser')
print(soup.find_all('title'))

#输出结果
#[<title class="tl">Hello BeautifulSoup</title>]

attrs 参数定义一个字典参数来搜索包含特殊属性的 tag,示例如下:

1
2
soup = BeautifulSoup('<title class="tl">Hello BeautifulSoup</title>','html.parser')
soup.find_all(attrs={"class": "tl"})

调用 find_all() 方法时,默认会检索当前 tag 的所有子孙节点,通过设置参数 recursive=False,可以只搜索 tag 的直接子节点,示例如下:

1
2
3
4
5
soup = BeautifulSoup('<html><head><title>Hello BeautifulSoup</title></head></html>','html.parser')
print(soup.find_all('title',recursive=False))

#输出结果
#[]

通过 text 参数可以搜搜文档中的字符串内容,它接受字符串、正则表达式、列表、True,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from bs4 import BeautifulSoup
import re

soup = BeautifulSoup('<head>myHead</head><title>BeautifulSoup</title>','html.parser')
#字符串
soup.find_all(text='BeautifulSoup')
#['BeautifulSoup']

#正则表达式
soup.find_all(soup.find_all(text=re.compile('title')))
#[<head>myHead</head>, <title>BeautifulSoup</title>]

#列表
soup.find_all(soup.find_all(text=['head','title']))
#[<head>myHead</head>, <title>BeautifulSoup</title>]

#True
soup.find_all(text=True)
#['myHead', 'BeautifulSoup']

limit 参数与 SQL 中的 limit 关键字类似,用来限制搜索的数据,示例如下:

1
2
3
soup = BeautifulSoup('<a id="link1" href="http://example.com/elsie">Elsie</a><a id="link2" href="http://example.com/elsie">Elsie</a>','html.parser')
soup.find_all('a', limit=1)
#[<a href="http://example.com/elsie" id="link1">Elsie</a>]

我们经常见到 Python 中 *arg**kwargs 这两种可变参数,*arg 表示非键值对的可变数量的参数,将参数打包为 tuple 传递给函数;**kwargs 表示关键字参数,参数是键值对形式的,将参数打包为 dict 传递给函数。

使用多个指定名字的参数可以同时过滤 tag 的多个属性,如:

1
2
3
soup = BeautifulSoup('<a id="link1" href="http://example.com/elsie">Elsie</a><a id="link2" href="http://example.com/elsie">Elsie</a>','html.parser')
soup.find_all(href=re.compile("elsie"),id='link1')
#[<a href="http://example.com/elsie" id="link1">Elsie</a>]

有些 tag 属性在搜索不能使用,如 HTML5 中的 data-* 属性,示例如下:

1
2
soup = BeautifulSoup('<div data-foo="value">foo!</div>')
soup.find_all(data-foo='value')

首先当我在 Pycharm 中输入 data-foo='value' 便提示语法错误了,然后我不管提示直接执行提示 SyntaxError: keyword can't be an expression 这个结果也验证了 data-* 属性在搜索中不能使用。我们可以通过 find_all() 方法的 attrs 参数定义一个字典参数来搜索包含特殊属性的 tag,示例如下:

1
2
print(soup.find_all(attrs={'data-foo': 'value'}))
# [<div data-foo="value">foo!</div>]

find()

方法详细如下:find(name=None, attrs={}, recursive=True, text=None,**kwargs),我们可以看出除了少了 limit 参数,其它参数与方法 find_all 一样,不同之处在于:find_all() 方法的返回结果是一个列表,find() 方法返回的是第一个节点,find_all() 方法没有找到目标是返回空列表,find() 方法找不到目标时,返回 None。来看个例子:

1
2
3
4
5
6
7
8
9
soup = BeautifulSoup('<a id="link1" href="http://example.com/elsie">Elsie</a><a id="link2" href="http://example.com/elsie">Elsie</a>','html.parser')
print(soup.find_all('a', limit=1))
print(soup.find('a'))

#输出结果
'''
[<a href="http://example.com/elsie" id="link1">Elsie</a>]
<a href="http://example.com/elsie" id="link1">Elsie</a>
'''

从示例中我们也可以看出,find() 方法返回的是找到的第一个节点。

find_parents() 和 find_parent()

find_all() 和 find() 用来搜索当前节点的所有子节点,find_parents() 和 find_parent() 则用来搜索当前节点的父辈节点。

find_next_siblings() 和 find_next_sibling()

这两个方法通过 .next_siblings 属性对当前 tag 所有后面解析的兄弟 tag 节点进行迭代,find_next_siblings() 方法返回所有符合条件的后面的兄弟节点,find_next_sibling() 只返回符合条件的后面的第一个tag节点。

find_previous_siblings() 和 find_previous_sibling()

这两个方法通过 .previous_siblings 属性对当前 tag 前面解析的兄弟 tag 节点进行迭代,find_previous_siblings() 方法返回所有符合条件的前面的兄弟节点,find_previous_sibling() 方法返回第一个符合条件的前面的兄弟节点。

find_all_next() 和 find_next()

这两个方法通过 .next_elements 属性对当前 tag 之后的 tag 和字符串进行迭代,find_all_next() 方法返回所有符合条件的节点,find_next() 方法返回第一个符合条件的节点。

find_all_previous() 和 find_previous()

这两个方法通过 .previous_elements 属性对当前节点前面的 tag 和字符串进行迭代,find_all_previous() 方法返回所有符合条件的节点,find_previous() 方法返回第一个符合条件的节点。

CSS选择器

BeautifulSoup 支持大部分的 CSS 选择器,在 Tag 或 BeautifulSoup 对象的 .select() 方法中传入字符串参数,即可使用 CSS 选择器的语法找到 tag,返回类型为列表。示例如下:

1
2
3
4
5
soup = BeautifulSoup('<body><a id="link1" class="elsie">Elsie</a><a id="link2" class="elsie">Elsie</a></body>','html.parser')
print(soup.select('a'))

#输出结果
#[<a clss="elsie" id="link1">Elsie</a>, <a clss="elsie" id="link2">Elsie</a>]

通过标签逐层查找

1
soup.select('body a')

找到某个 tag 标签下的直接子标签

1
soup.select('body > a')

通过类名查找

1
2
soup.select('.elsie')
soup.select('[class~=elsie]')

通过 id 查找

1
soup.select('#link1')

使用多个选择器

1
soup.select('#link1,#link2')

通过属性查找

1
soup.select('a[class]')

通过属性的值来查找

1
soup.select('a[class="elsie"]')

查找元素的第一个

1
soup.select_one('.elsie')

查找兄弟节点标签

1
2
3
4
#查找所有
soup.select('#link1 ~ .elsie')
#查找第一个
soup.select('#link1 + .elsie')

Python与爬虫
http://example.com/2024/04/22/20240422_crawler/
作者
XuanYa
发布于
2024年4月22日
许可协议