python 爬虫学习笔记

前言

网络爬虫(又称为网页蜘蛛,网络机器人,在 FOAF 社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。

爬虫,说白了其实就是一段自动抓取互联网信息的程序,它不需要我们自己手动一个一个地打开网站搜索信息,我们只需要制定规则,就可以让程序按照规则自动获取信息。

在学习如何使用爬虫前,你仍需要具备一定的基础知识:

  • python 基本功
  • HTML 知识
  • HTTP 请求 GET、POST
  • 正则表达式
  • F12 开发者工具

掌握上面的这些知识能够帮助你快速理解与掌握,当然,上面的这些除了第一点 python 基本功外,只需要先了解,然后通过具体项目来不断实践即可。

使用 requests 库请求网站

尽管有许多类似的工具,但在 pythonrequests 往往是绝大多数人的第一选择。类似的,你可以使用如下命令安装 requests 库:

1
pip install requests

基本请求

requests 库能够方便的让我们进行所有 HTTP 请求,在 中文文档 中有这样一个示例:

1
2
3
4
import requests
# 发送一个 get 请求并返回一个 Response 对象
r = requests.get('https://api.github.com/events')
print(r.text)

上面的这段代码可以获取 Github 的公共时间线,并打印内容。

除了 get 请求之外,你也可以按照类似的方法使用其他 HTTP 请求:

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

httpbin 是一个 HTTP Request & Response Service,你可以向他发送请求,然后他会按照指定的规则将你的请求返回。这个类似于 echo 服务器,但是功能又比它要更强大一些。 httpbin 支持 HTTP/HTTPS,支持所有的 HTTP 动词,能模拟 302 跳转乃至 302 跳转的次数,还可以返回一个 HTML 文件或一个 XML 文件或一个图片文件(还支持指定返回图片的格式)。

get 请求

接下来,让我们仔细来看看实现的一些细节,同样以 http://httpbin.org/ 为例,我们试着打印它返回的内容:

1
2
3
import requests
r = requests.get("http://httpbin.org/get")
print(r.text)

下面展示了这个 get 请求所返回的内容,其中包含了请求地址和本机的一些信息,关于 headers 会在下面进行说明,这里暂且忽视。

1
2
3
4
5
6
7
8
9
10
11
12
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.22.0",
"X-Amzn-Trace-Id": "Root=xxxxxxxxxxxxxxxxxxxxxx"
},
"origin": "xxx.xxx.xxx.xxx",
"url": "http://httpbin.org/get"
}

现在,我们尝试传递某种数据,如果你是手工构建 URL,那么数据会以键/值对的形式置于 URL 中,跟在一个问号的后面。例如, httpbin.org/get?key=val

Requests 允许你使用 params 关键字参数很方便地进行参数的传递:

1
2
3
4
5
6
import requests

params = {'key1': 'value1', 'key2': ['value2', 'value3']}
# 发送一个 get 请求并返回一个 Response 对象
r = requests.get("http://httpbin.org/get")
print(r.text)

在上面的例子中,我们传递了一个值以及一个列表,下面打印了详细的信息,与之前对比你会发现,网站确实收到了我们传递的参数,你也可以从 "url" 中发现这点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"args": {
"key1": "value1",
"key2": ["value2", "value3"]
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.22.0",
"X-Amzn-Trace-Id": "Root=xxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"origin": "xxx.xxx.xxx.xxx",
"url": "http://httpbin.org/get?key1=value1&key2=value2&key2=value3"
}

post 请求

post 请求中,我们往往需要传递一些参数,这与我们之前讨论的是类似的,只需要简单地传递一个字典给 data 参数。

1
2
3
4
5
6
import requests

params = {'key1': 'value1', 'key2': ['value2', 'value3']}
# 发送一个 get 请求并返回一个 Response 对象
r = requests.post("http://httpbin.org/post", data=params)
print(r.text)

我们可以看到,数据被正确地传递了。

1
2
3
4
5
6
7
8
9
...
"form": {
"key1": "value1",
"key2": [
"value2",
"value3"
]
},
...

另外一方面,我们可以通过 post 来传输文件,直接用 file 参数即可。

首先我们创建一个 txt 文件,写入 hello world!。然后通过以下方式进行文件的发送。

1
2
3
4
5
6
7
import requests

# 使用二进制模式打开文件
files = {'files': open('test.txt', 'rb')}
# 发送一个 get 请求并返回一个 Response 对象
r = requests.post("http://httpbin.org/post", files=files)
print(r.text)

通过返回的内容可以看到,文件确实被接收了。

1
2
3
4
5
6
7
...
"data": "",
"files": {
"files": "hello world!"
},
"form": {},
...

状态响应码

HTTP 状态码

分类 描述
1×× 信息,服务器收到请求,需要请求者继续执行操作
2×× 成功,操作被成功接收并处理
3×× 重定向,需要进一步的操作以完成请求
4×× 客户端错误,请求包含语法错误或无法完成请求
5×× 服务器错误,服务器在处理请求的过程中发生了错误

我们可以使用 status_code 查看响应状态码。

1
2
3
4
5
6
import requests

params = {'key1': 'value1', 'key2': ['value2', 'value3']}
# 发送一个 get 请求并返回一个 Response 对象
r = requests.get("http://httpbin.org/get")
print(r.status_code)

超时重时

你可以告诉 requests 在经过以 timeout 参数设定的秒数时间之后停止等待响应。

1
requests.get('http://github.com', timeout=1)

一个简单爬虫的示例

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

url = 'https://www.baidu.com/'
# get方式获取网页数据
# 通过向网页发起请求,我们获得了一个 response 的对象
r = requests.get(url)

# 返回请求状态码
print(r.status_code)

# 返回页面代码
print(r.text)

上面的这段代码实现了一个简单的爬虫,我们可以获取网页的 html 代码,然后再通过解析 html 获得我们想要的数据。

http 请求头

然而,我们需要知道的是,由于许多网站都有反爬虫的措施,在我们登录网站时,大部分网站都会需要你表明你的身份,因此在我们正常访问网站时都会附带一个请求头(headers)信息,里面包含了你的浏览器,编码等内容,网站会通过这部分信息来判断你的身份,所以我们一般写爬虫时也加上一个 headers

下面我们列举了一些常见的 http 请求头参数:

  • "Accept":指定客户端可以接受的内容类型,比如文本,图片,应用等等,内容的先后排序表示客户端接收的先后次序,每种类型之间用逗号隔开
  • "Accept-Charset":指的是规定好服务器处理表单数据所接受的字符集
  • "Accept-Encoding":客户端接收编码类型
  • "Accept-Language":客户端可以接受的语言类型
  • "Cache-Control":指定请求和响应遵循的缓存机制
  • "Connection":表示是否需要持久连接
    • "close":在完成本次请求的响应后,断开连接
    • "keep-alive":在完成本次请求的响应后,保持连接,等待本次连接的后续请求
  • "Cookie":HTTP 请求发送时,会把保存在该请求域名下的 cookie 值一起发送给 web 服务器
  • "Pragma":用来包含实现特定的指令,最常用的是 "Pragma": "no-cache"
  • "Referer":先前网页的地址
  • "User-Agent":中文名用户代理,服务器从此处知道客户端的 操作系统类型和版本

下面展示了一个典型的 headers 请求头示例:

1
2
3
4
5
6
7
8
9
10
11
headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Charset": "gb2312,gbk;q=0.7,utf-8;q=0.7,*;q=0.7",
"Accept-Encoding": "gzip, deflate, sdch",
"Accept-Language": "zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Pragma": "no-cache",
"Referer": "https://leetcode-cn.com/accounts/login/",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}

如果你只是简单的进行爬虫的学习,不想了解那么多字段,那么你可以关注 User-Agent,我们写爬虫时,User-Agent 总是必不可少的,你可以通过它来伪装成浏览器在访问。它是爬虫当中最重要的一个请求头参数,所以一定要伪造,甚至多个。

其中,对于每一种内容类型,分号 ; 后面会加一个 q=0.6 这样的 q 值,表示该种类型被客户端喜欢接受的程度,如果没有表示 q=1,数值越高,客户端越喜欢这种类型。

添加请求头

在了解了请求头之后,我们对之前的代码进行修改,在这里,我们只在请求头中添加了 "User-Agent" 字段。

1
2
3
4
5
6
7
8
9
import requests

url = 'https://www.baidu.com/'
headers = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
# 通过向网页发起请求
r = requests.get(url, headers=headers)

如果你像我一样使用 chrome,那么你可以在地址栏输入 chrome://version/ 查看你当前的用户代理,也可在网上搜索常用的用户代理。

会话对象

在之前的请求中,每次请求其实都相当于发起了一个新的请求。也就是相当于我们每个请求都用了不同的浏览器单独打开的效果。

在一些站点中,我们需要保持一个持久的会话怎么办呢?会话对象让你能够跨请求保持某些参数。它也会在同一个 Session 实例发出的所有请求之间保持 cookie

很多时候等于需要登录的站点我们可能需要保持一个会话,不然每次请求都先登录一遍效率太低

1
2
3
4
5
6
7
8
# 新建一个Session对象,保持会话
session = requests.Session()
session.get('http://httpbin.org/cookies/set/sessioncookie/123456789')
# 然后在这个会话下再去访问
r = session.get("http://httpbin.org/cookies")

print(r.text)
# '{"cookies": {"sessioncookie": "123456789"}}'

爬虫豆瓣电影名

在了解了那么多内容之后,我相信你也已经掌握了大半,现在让我们从一个具体案例开始分析,爬取豆瓣电影名。

首先,我们需要做好准备工作,准备网址,构建请求头,接着进行 get

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

# 准备网址
url = 'https://movie.douban.com/top250'

# 构建请求头
headers = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}

# 发送一个 get 请求并返回一个 Response 对象
r = requests.get(url, headers=headers)

到此为止,我们已经获得了 html 信息,接下来,我们需要对其进行解析,并提取出我们想要的内容。

通过 BeautifulSoup 解析网页

现在我们有了 html 信息,那么一个最朴素的想法就是通过正则表达式进行匹配。虽然可能写一个匹配模式可能有些难度,但基本的思想总是没问题的。

Beautiful Soup 是一个可以从 HTML 或 XML 文件中提取数据的 Python 库。你可以在 中文文档 中了解其用法。

在解析网页之前,先让我们看看豆瓣电影网站的结构是怎么样的,打开开发者工具,你可以看见网页源码,然后定位到电影名所在的位置。

现在,让我们来分析一下网页的结构

从里到外进行分析,包装电影名肖申克的救赎的是一个 span,向外一层是一个 a 标签,接着是一个 div 类型为 hd。那么我们如何正确定位到电影名呢?直接搜索类为 titlespan 明显是不可行的,因为我们看到电影的英文名也是同样的包装,并不唯一确定。

一个比较好的做法是找到所有类型为 hddiv,接着向下定位,找到 span

1
2
3
4
5
6
7
8
9
10
11
12
from bs4 import BeautifulSoup

# 对网址进行解析
soup = BeautifulSoup(r.text, 'lxml')

# 匹配所有电影名所在的标签
movies = soup.find_all('div', class_='hd')

movie_list = []
for movie in movies:
movie_list.append(movie.a.span.text)
print(movie_list)

在第 2 行代码中,我们使用 BeautifulSoup 对网址进行解析,第一个参数是网站的 html 文本,第二个参数是解析器。接着返回一个 BeautifulSoup 类型的对象。

在第 5 行代码中,正如我们前面讨论的,找到所有类型为 hddiv。其中返回值 movies 仍然是 html 文本,只不过是筛选之后的。

在第 7 - 10 行代码中,我们写了一个简单的 for 循环,通过 movie.a.span.text 的方式逐级提取出电影名,相信理解起来并不困难。

完整代码如下:

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
import requests
from bs4 import BeautifulSoup

# 准备网站
url = 'https://movie.douban.com/top250'

# 构建请求头
headers = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}

# 发送一个 get 请求并返回一个 Response 对象
r = requests.get(url, headers=headers)

# 对网址进行解析
soup = BeautifulSoup(r.text, 'lxml')

# 匹配所有电影名所在的标签
movies = soup.find_all('div', class_='hd')

movie_list = []
for movie in movies:
movie_list.append(movie.a.span.text)
print(movie_list)

打印结果如下:

[‘肖申克的救赎’, ‘霸王别姬’, ‘阿甘正传’, ‘这个杀手不太冷’, ‘泰坦尼克号’, ‘美丽人生’, ‘千与千寻’, ‘辛德勒的名单’, ‘盗梦空间’, ‘忠犬八公的故事’, ‘星际穿越’, ‘楚门的世界’, ‘海上钢琴师’, ‘三傻大闹宝莱坞’, ‘机器人总动员’, ‘放牛班的春天’, ‘无间道’, ‘疯狂动物城’, ‘大话西游之大圣娶亲’, ‘熔炉’, ‘教父’, ‘当幸福来敲门’, ‘龙猫’, ‘怦然心动’, ‘控方证人’]

翻页的问题

现在我们成功爬取了豆瓣电影名,但是又出现了一个问题,正如我们所看到的,现在只爬取了一页 25 个电影名,远远没有完成目标,当然比较笨的做法是手动翻页重复几次,修改 url。但事实上不必如此,我们只需要仔细观察 url 的结构即可。

1
2
3
第一页  https://movie.douban.com/top250?start=0&filter=
第二页 https://movie.douban.com/top250?start=25&filter=
第三页 https://movie.douban.com/top250?start=50&filter=

你会发现 url 非常有规律,一页 25 个,共 10 页,差别也仅有 start 字段而已。

那么我们就可以在外层简单的套一个 for 循环,并且传递参数即可,正如之前提到的,Requests 允许你使用 params 关键字参数很方便地进行参数的传递。

1
2
3
4
5
...
for i in range(10):
params = {'start': str(i * 25)}
r = requests.get(url, headers=headers, params=params)
...

通过 post 进行登录

接下来,我们以登录力扣为例,说明如何使用 post 进行登录,毕竟许多网站只有在登录之后你才可以进行各种操作。

值得注意的是,进行网站登录的时候要知道表单的字段是什么,有的是 email password 有的是 username password 而表单字段的设置不一定有规律。只有获取到表单的字段才可以模拟传入值进行登录。

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
import requests

# 建立会话
session = requests.Session()
session.encoding = 'utf-8'

# 输入登录信息,即用户名和密码
USERNAME = '你的用户名'
PASSWORD = '你的密码'
login_data = {'login': USERNAME, 'password': PASSWORD}

# 登陆网址
sign_in_url = 'https://leetcode-cn.com/accounts/login/'

# 构造请求头
headers = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Connection': 'keep-alive',
'Referer': sign_in_url
}

# 发送登录请求
# 提供请求头和登录信息
session.post(sign_in_url, headers=headers, data=login_data)
is_login = session.cookies.get('LEETCODE_SESSION') != None
if is_login:
print('登录成功')
else:
print('登录失败')

总结

python 爬虫相对来说入门并不算太难,但真正的实践过程中往往会遇到许多的问题。你可以在 github 上寻找更多的爬虫示例/教程,通过更多的实战更上一层楼。

参考资料