Python 网络请求与爬虫基础
Python 爬虫
前置知识
- 懂得 Python 基本语法,知道如何使用 Python 第三方库
- 知道 HTML 的基本结构
- 会使用 Chrome 等现代浏览器的开发者工具来查看网页源代码和网络请求信息
环境配置
需要用到以下一些库:
- IPython:理论上没有也行,但是使用 IPython 会提高大家测试的效率
- requests:我装 Python 的时候好像自带了但是这似乎不是标准库
- BeautifulSoup4:解析 HTML 的库,注意不要尝试用正则表达式来处理 HTML,虽然小学期可能会这样做,但是正则表达式并不适于解决 HTML 这种格式
- fake_useragent:生成 User-Agent 的库
关于 fake_useragent
这个库似乎会尝试自动获取某些在线资源,但是好像需要代理,如果不能征程运行,请下载 fake_useragent.json 放在一个你能找到的地方,最好放在我们要使用的项目文件的目录下
在使用时应当添加参数指定资源地址如下:
| ua = UserAgent(path='./fake_useragent.json')
|
网络请求
终极哲学三大问(服务器视角下的网络请求)
爬虫为了(自动化地、常常还需要是高速地)获取服务器资源,首先应当了解向服务器发送请求时发生了什么。通常来说一次网络请求的主要内容包括:
- 你是谁:User-Agent | Cookies
- 你从哪里来:IP
- 你来这里干什么:Method 与 Parameter (如果是 POST 请求的话还会有更多内容,但是作为爬虫的话可以略微简化)
在这一过程中服务器可能面临一些攻击(爬虫可能无意间导致这样的问题):
- DoS (Denial of Service) attack:拒绝服务攻击,通过构造大量的(尤其是繁重的,也就是 CC 攻击)请求使得服务器“忙不过来”,让服务器无法正常服务。
- DDoS (Distributed Denial of Service) attack:分布式拒绝服务攻击,在 DoS 攻击的基础上改为多个客户端(
肉鸡 用户)同时对目标网站发起大量请求。
尽管谈论攻击有点遥远,但是不妨考虑以下两个情景:
- 当你准时到达查询高考成绩时
- 当你双十一午夜十二点下单时
所以写爬虫时应该全力避免这种情况!
网络抓包基本功
一线主播的名言
浏览网页的本质是基于 HTTP 协议的一次或者多次网络请求,这些请求用户是可以直接查看的,比如按下 F12 再看看“网络”。
网络抓包通常有如下用途:
- 查看真正的数据来源(常为 XHR)
- 查看下载视频等数据
- 模拟请求完成登录
测试网站安全性
其实对于写爬虫来说,主要用途只有一个,就是找到我们需要的数据的真正来源(当然如果使用 selenium 等方法加载网页内容可以不用分析数据来源,但是这样显然消耗更多资源且慢),稍后我们将会看到这一点。
爬虫初步
何为爬 & 为何爬
这个想必大家都是懂的,写爬虫归根结底还是为了替代人工进行重复大量的操作,如果非常不熟悉爬虫这种程序可以去看 rls 的课。
孰可爬
这部分是我们最重要的背景知识,爬网站之前请务必关注 robots.txt
(即域名后接 /robots.txt
,要注意子域名的要求未必和主域名一样,切不可看到主域名可以爬就随便搞),这是网站与爬虫之间的“君子协议”,此处规定了什么可爬什么不可爬,下面是几个例子。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | User-agent: Baiduspider
Disallow: /baidu
Disallow: /s?
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bh
User-agent: Googlebot
Disallow: /baidu
Disallow: /s?
Disallow: /shifen/
Disallow: /homepage/
Disallow: /cpro
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bh
# 中间略去若干内容
User-agent: *
Disallow: /
|
解读:除了我钦点的爬虫之外,其他拒不接待。
| User-agent: *
Disallow: /
|
解读:莫挨老子。
注意这里就出现了我们之前提到过的问题,https://www.hupu.com/robots.txt 如下:
| User-agent: *
Allow: /
Sitemap: https://bbs.hupu.com/sitemap_index.xml
Sitemap: https://bbs.hupu.com/sitemap/sitemap_boards.xml
Sitemap: https://voice.hupu.com/sitemap_index.xml
Sitemap: https://nba.hupu.com/players/index.xml
|
解读:任何人都可以任意爬,还提供了站点地图供参考。
但是 https://bbs.hupu.com/robots.txt 则是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13 | User-agent: *
Request-rate: 50/1
Disallow: /api/
Disallow: /ajax/
Disallow: /profile.php?*
Disallow: /hack/
Disallow: /template/
Disallow: /attachment/
Disallow: /gearfeedback/
Disallow: /*_*.html$
Sitemap: https://bbs.hupu.com/sitemap_index.xml
Sitemap: https://bbs.hupu.com/sitemap/sitemap_boards.xml
|
解读:法无禁止皆可为,但是速度不超过 50 次每秒(注意不同的爬虫其实对于一次爬取的理解是不同的,但是这是君子协议,所以对于我们自己写的爬虫也没有必要那么纠结,如果要仔细考虑的话,可以研究一下不同搜索引擎的爬虫对于这一问题的理解,可以参考 知乎的 robots.txt 以及 相关提问)。
虽然虎扑社区比主站多了一些限制,但是我们看到这仍然是限制最少的(虽然限制 *_*.html$
但是新闻页并不是这样,而且 50 次每秒其实也不少),加上 众所周知的原因 虎扑前两天很出圈(至少在我写文档的时候是这样),所以我们将主要爬取虎扑的体育新闻作为演示。
如何爬
理想中的高性能爬虫架构:
本文用的入门版爬虫架构:
爬虫实战
自学内容
环境配置中写了最重要的工具,下面还有些可能会用到的自学内容:
- js2py:在 Python 中执行 JavaScript 脚本
- pyppeteer:通过 headless 的方法直接渲染网页
- selenium:控制浏览器进行自动化测试的工具
- requests.Session:保存会话,适用于需要 Cookie 的场景
- requests 添加代理 IP
- scrapy:真正的高性能爬虫框架
爬取演示
以爬取 虎扑某板块新闻列表 为例:
| from requests import get, post
from bs4 import BeautifulSoup
from fake_useragent import UserAgent
from urllib.request import urljoin
from json import dump
from time import sleep # 如果网站要求以很低的频率爬,那么大概率会需要 sleep
|
如果你点开了前面那个链接(而且你最近没有看过虎扑的话),你大概率会发现报了个错,不要怀疑,我没有写错 URL,造成这样问题的原因大概是虎扑在切换新旧版面,直接点进去会莫名被转入旧版的页面,而旧版页面的 URL 大概不是这样写的。
那么这就要提到之前的一个问题,也就是“你是谁”的问题,虎扑会根据 Cookie 来判断你是否已经进入了新版页面,而这个 Cookie 显然是在首页被设置的,因此直接访问这一板块是行不通的。
从浏览器中获取 Cookie 信息如下(因为内容太长所以我们直接保存在 Python 变量中):
| Cookie = 'smidV2=20210720104859f6c99288e1bccb891e9ca9ce9d7bb6fe0055f24f4e677eec0; csrfToken=iGeSrk669wqQBcY5fKk2Diue; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%2217ac1d102691c24-05465c49cc1a3d8-3e62684b-1296000-17ac1d1026a2685%22%2C%22%24device_id%22%3A%2217ac1d102691c24-05465c49cc1a3d8-3e62684b-1296000-17ac1d1026a2685%22%2C%22props%22%3A%7B%22%24latest_referrer%22%3A%22%22%2C%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%7D%7D; acw_tc=2f624a6816268527794733860e071fa5c73b1f702b456ad494e04ecfb5dc53; bbs_2020=1'
|
我们注意到,虎扑的新闻列表页 URL 大致可以表示为:
| https://bbs.hupu.com/502-{PAGE}
|
注意到其中每一项的格式大致如下:
| <a href="/44314069.html" target="_blank" class="p-title">[流言板]字母哥:要相信!我曾经上顿不接下顿,如今我是冠军</a>
|
那么假如我们获取前 10 页的新闻详情链接:
| url = 'https://bbs.hupu.com/'
news_list = []
for i in range(1, 11):
response = get(f'https://bbs.hupu.com/502-{i}', headers={'Cookie': Cookie})
soup = BeautifulSoup(response.content.decode('utf-8'), 'lxml')
news = soup.find_all('a', class_='p-title')
for n in news:
news_list.append(urljoin(url, n.get('href')))
sleep(0.02) # 按照 robots.txt 要求休息 1/50 秒
|
更好的做法是这里写一个类,但是一个自定义类写入文件又涉及到许多问题,所以我们简单的用字典表示,类似的,我们获取并解析网页:
1
2
3
4
5
6
7
8
9
10
11
12
13 | news_content = []
for news_url in news_list:
response = get(news_url, headers={'Cookie': Cookie})
soup = BeautifulSoup(response.content.decode('utf-8'), 'lxml')
title = soup.find('h1', class_='name').text.strip()
content = soup.find('div', class_='thread-content-detail').text.strip().replace('\r', '\n')
news_content.append({
'url': news_url,
'title': title,
'content': content
})
sleep(0.02)
|
接下来我们将获取的内容保存为 JSON:
| dump(news_content, open('./hupu.json', 'w'), ensure_ascii=False)
|
关于反爬
接下来我们演示一个糟糕的反爬示例,北京卫健委 网站。
如果我们直接执行:
| response = get('http://wjw.beijing.gov.cn/')
|
我们会发现 response.text
很不对劲,而且其中比较明显能看到的是(当然我经过了格式化):
| <div id='notice-jiasule'>
<p>当前网址:<%- url %></p>
<p>客户端特征:<%- user_agent %></p>
<p>拦截时间:<%- now %> 本次事件ID <%- rule_id %></p>
</div>
|
还有下面的:
| var content = _.template(document.getElementById('content_tpl').innerHTML)({
error_403: '' || '当前访问疑似黑客攻击,已被网站管理员设置为拦截',
url: document.URL.replace(/\</g,"%3C").replace(/\>/g,"%3E"),
user_agent: navigator.userAgent,
now: new Date(new Date() - -8 * 3600000).toISOString().substr(0, 19).replace('T', ' '),
rule_id: parseInt('[80001]'.replace(/\[|\]/g, '')) || '',
from: encodeURIComponent(document.referrer.substr(0, 1024)),
client_ip: '59.66.16.88',
ref: encodeURIComponent(document.URL.substr(0, 1024))
});
|
不难发现这个网站所托管的“创宇云防御”屏蔽了我们的访问,而引起这一问题的原因,我们可以查看:
| >>> response.request.headers
{'User-Agent': 'python-requests/2.25.1', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}
|
其中显然有一条 'User-Agent': 'python-requests/2.25.1'
,不打自招的爬虫行为,被屏蔽也并不奇怪(题外话,这个网站并没有设置 robots.txt
,因此双方都很难说谁对谁错)。好在现在这个网站不像去年一样会封 IP 地址,所以我们可以先演示错误示例。
我们当然可以模仿前面的做法,从浏览器中抄一个 User-Agent 过来,但是如果我们快速爬很多东西,最好还是每次换一个新的 User-Agent 减少被当作同一个用户的概率,因此我们使用如下方法:
| ua = UserAgent(path='./fake_useragent.json')
response = get('http://wjw.beijing.gov.cn/', headers={'User-Agent': ua.random})
# 没错,ua.random 虽然是成员变量,但是每次使用的时候会改变自己,而不是用户调用一个类似于 ua.random() 的函数才会改变
|
除此之外可能还会需要使用代理 IP,如何获取代理地址请自学,使用方法如下:
| proxies = {
"http": "http://10.10.1.10:3128",
"https": "http://10.10.1.10:1080",
}
requests.get("http://example.org", proxies=proxies)
|
使用这两种方法可以避免很多反爬的系统(当然如果检查 Cookie 大概会很糟糕,可能需要使用 Session 来解决,像上述直接设置 Cookie 实际上并不很通用,在此不做很具体的说明),但是还有更多的情况可能真实的数据并不能轻易获取,这个时候就要善用浏览器的开发者工具审查所有的网络请求。
资源链接