python 爬虫学习笔记
前言
网络爬虫(又称为网页蜘蛛,网络机器人,在 FOAF 社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。
爬虫,说白了其实就是一段自动抓取互联网信息的程序,它不需要我们自己手动一个一个地打开网站搜索信息,我们只需要制定规则,就可以让程序按照规则自动获取信息。
在学习如何使用爬虫前,你仍需要具备一定的基础知识:
- python 基本功
- HTML 知识
- HTTP 请求 GET、POST
- 正则表达式
- F12 开发者工具
掌握上面的这些知识能够帮助你快速理解与掌握,当然,上面的这些除了第一点 python 基本功外,只需要先了解,然后通过具体项目来不断实践即可。
使用 requests 库请求网站
尽管有许多类似的工具,但在 python
中 requests
往往是绝大多数人的第一选择。类似的,你可以使用如下命令安装 requests
库:
pip install requests
基本请求
requests
库能够方便的让我们进行所有 HTTP 请求,在 中文文档 中有这样一个示例:
import requests
# 发送一个 get 请求并返回一个 Response 对象
r = requests.get('https://api.github.com/events')
print(r.text)
上面的这段代码可以获取 Github 的公共时间线,并打印内容。
除了 get 请求之外,你也可以按照类似的方法使用其他 HTTP 请求:
代码语言:javascript复制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/
为例,我们试着打印它返回的内容:
import requests
r = requests.get("http://httpbin.org/get")
print(r.text)
下面展示了这个 get
请求所返回的内容,其中包含了请求地址和本机的一些信息,关于 headers
会在下面进行说明,这里暂且忽视。
{
"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
关键字参数很方便地进行参数的传递:
import requests
params = {'key1': 'value1', 'key2': ['value2', 'value3']}
# 发送一个 get 请求并返回一个 Response 对象
r = requests.get("http://httpbin.org/get")
print(r.text)
在上面的例子中,我们传递了一个值以及一个列表,下面打印了详细的信息,与之前对比你会发现,网站确实收到了我们传递的参数,你也可以从 "url"
中发现这点。
{
"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
参数。
import requests
params = {'key1': 'value1', 'key2': ['value2', 'value3']}
# 发送一个 get 请求并返回一个 Response 对象
r = requests.post("http://httpbin.org/post", data=params)
print(r.text)
我们可以看到,数据被正确地传递了。
代码语言:javascript复制...
"form": {
"key1": "value1",
"key2": [
"value2",
"value3"
]
},
...
另外一方面,我们可以通过 post
来传输文件,直接用 file
参数即可。
首先我们创建一个 txt
文件,写入 hello world!
。然后通过以下方式进行文件的发送。
import requests
# 使用二进制模式打开文件
files = {'files': open('test.txt', 'rb')}
# 发送一个 get 请求并返回一个 Response 对象
r = requests.post("http://httpbin.org/post", files=files)
print(r.text)
通过返回的内容可以看到,文件确实被接收了。
代码语言:javascript复制...
"data": "",
"files": {
"files": "hello world!"
},
"form": {},
...
状态响应码
HTTP 状态码
分类 | 描述 |
---|---|
1×× | 信息,服务器收到请求,需要请求者继续执行操作 |
2×× | 成功,操作被成功接收并处理 |
3×× | 重定向,需要进一步的操作以完成请求 |
4×× | 客户端错误,请求包含语法错误或无法完成请求 |
5×× | 服务器错误,服务器在处理请求的过程中发生了错误 |
我们可以使用 status_code
查看响应状态码。
import requests
params = {'key1': 'value1', 'key2': ['value2', 'value3']}
# 发送一个 get 请求并返回一个 Response 对象
r = requests.get("http://httpbin.org/get")
print(r.status_code)
超时重时
你可以告诉 requests
在经过以 timeout
参数设定的秒数时间之后停止等待响应。
requests.get('http://github.com', timeout=1)
一个简单爬虫的示例
代码语言:javascript复制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
请求头示例:
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"
字段。
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
很多时候等于需要登录的站点我们可能需要保持一个会话,不然每次请求都先登录一遍效率太低
代码语言:javascript复制# 新建一个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
。
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
。那么我们如何正确定位到电影名呢?直接搜索类为 title
的 span
明显是不可行的,因为我们看到电影的英文名也是同样的包装,并不唯一确定。
一个比较好的做法是找到所有类型为 hd
的 div
,接着向下定位,找到 span
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 行代码中,正如我们前面讨论的,找到所有类型为 hd
的 div
。其中返回值 movies
仍然是 html
文本,只不过是筛选之后的。
在第 7 - 10 行代码中,我们写了一个简单的 for
循环,通过 movie.a.span.text
的方式逐级提取出电影名,相信理解起来并不困难。
完整代码如下:
代码语言:javascript复制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
的结构即可。
第一页 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
关键字参数很方便地进行参数的传递。
...
for i in range(10):
params = {'start': str(i * 25)}
r = requests.get(url, headers=headers, params=params)
...
通过 post 进行登录
接下来,我们以登录力扣为例,说明如何使用 post
进行登录,毕竟许多网站只有在登录之后你才可以进行各种操作。
值得注意的是,进行网站登录的时候要知道表单的字段是什么,有的是 email password
有的是 username password
而表单字段的设置不一定有规律。只有获取到表单的字段才可以模拟传入值进行登录。
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 上寻找更多的爬虫示例/教程,通过更多的实战更上一层楼。
参考资料
- Requests: 让 HTTP 服务人类