poetry

天接云涛连晓雾,星河欲转千帆舞。
仿佛梦魂归帝所,闻天语,殷勤问我归何处。

基本原理

爬虫的本质是:程序模拟浏览器,对服务器发起访问(requests请求过程),服务器返回响应(response响应过程)。所以,一个简单的爬虫只有三步:

  • 构造URL
  • 对服务器发送请求
  • 输出服务器返回内容

爬虫

这里用一个实例,运行后可以看到输出了百度网页的源码(这里就不演示了):

1
2
3
4
5
6
7
import requests # requests是python爬虫最常用的请求库

url = 'http://www.baidu.com' #1、指名要访问的网站

response = requests.get(url) #2、使用requests的get方法请求网站信息

print(response.text) #3、输出响应内容

所以,爬虫也都围绕这三步进行。
1、构造URL,就是要爬的链接
2、发起请求
3、解析响应内容

构造URL

可能有人会问,指定URL这么简单,为什么也要写,这里笔者给出一个实例说明:
这是东方财富网的腾讯股票页面,如果我想要爬取这个页面的评价,那么我就需要知道评论的URL。

东方财富网腾讯股


这个页面有94条评论地址,一共有196个这样的页,也就是我需要知道至少18,424‬个URL我才能把腾讯股票所有的评率全部爬下来,这显然是不现实的。那么我们是如何知道要爬页面的URL的呢?答案是:通过网站的内部逻辑构造URL。

评论数


通过Chrome开发者选项可以调出如下页面,指定一条评论链接,我们可以看到红圈画出来的内容:`/news,usaapl,880272132.html`

网站结构


然后我们再点进去,看看这个站点的真正链接是什么。

站点链接

可以看到,这个站点的真正链接是:

http://guba.eastmoney.com/news,usaapl,880272132.html

而我们之前找到了/news,usaapl,880272132.html,这个站点的真正链接,就是在这段之前拼接:http://guba.eastmoney.com

所以,我们只需要把一开始的网站爬下来,然后筛选出类似/news,usaapl,880272132.html这样的内容,然后再把它和http://guba.eastmoney.com拼接,就可以得到当前页面上所有评论的具体地址,这就是地址构造,也就是构造URL。

发起请求

一个爬虫是否能成功将网页爬下来,完全取决于请求。现在网络上绝大部分的网站都有了反爬机制,能够拦截爬虫。

这里解释一下为什么要有反爬。爬虫抓取网页就相当于用户访问网页,在这期间,浏览器对服务器发起了一次访问。如果爬虫大量的抓取网页,比如在一分钟之内抓取了一万次网页,那么就相当于访问了服务器一万次,这样就会大量的占据服务器的资源,甚至大型分布式爬虫会使服务器崩溃。然而,人类是不可能做到10000次/min的访问量的(其实相当于一分钟内刷新一个网页10000次)。所以,越来越多的网站设计了反爬,将低访问量的人和高访问量的爬虫区别开来,让人能够正常访问,而爬虫则拒之门外。目前主要通过以下方式实现反爬:

  • 识别用户的User-Agent
  • 使用Ajax请求
  • 使用JavaScript渲染内容
  • 对IP进行访问控制
  • 对URL进行加密处理
  • 构造复杂的请求头(往往是请求头需要额外添加网站的其他参数,这些参数分布在网站的隐蔽角落,而且几乎毫无逻辑)

添加请求头

这里笔者给出一个豆瓣电影榜的例子,首先用户需要先在浏览器中找到自己浏览器的请求头,具体操作如下:

  • 打开浏览器
  • 进入开发者模式,谷歌浏览器是按F12
  • Ctrl+R刷新页面
  • 选择Network的选项
  • 点击右边的任意一个
  • 左边选择Headers,下拉可以看到User-Agent
  • 将其复制到代码中即可,代码如下:

User-Agent


1
2
3
4
5
6
7
8
import requests
url = 'https://movie.douban.com/chart'
# 添加请求头
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36'
}
response = requests.get(url,headers=headers) # 这里要把请求头送入到请求方法中
print(response.text)

豆瓣图书榜源码


绝大多数网站,不加请求头,往往加载不了全部网页内容。

添加data

data和User-Agent的作用类似,都是用来伪装成用户的。服务器会根据有没有data和user-agent来判断是否是爬虫还是用户。

1
2
3
4
5
6
7
8
9
url='http://httpbin.org/get'

data={
'name':'germet',
'age':22
}

response=requests.get(url,data=data)#或者是params
print(response.text)

克制Ajax和JavaScript

使用selenium首先要安装selenium库,pip install selenium -i https://pypi.tuna.tsinghua.edu.cn/simple
其次,如果是用selenium打开Chrome,还需要安装ChromeDriver驱动。淘宝源ChromeDriver驱动链接,注意版本要和当前Chrome的版本一致。

http://npm.taobao.org/mirrors/chromedriver/

目前Chrome支持的浏览器有Safari、FireFox、Chrome等,且都需要安装浏览器驱动。

selenium会自动打开浏览器,然后渲染出网页,再关闭浏览器,这样可以克制一切JavaScript渲染,如果想针对Ajax请求,可以用Selenium模拟点击效果,这里只介绍基础的用法。

1
2
3
4
5
6
7
8
9
10
11
from selenium import webdriver

url = 'https://movie.douban.com/chart'

option = webdriver.ChromeOptions() # 添加Chrome打开的选项
#option.add_argument('--headless') # 执行时不打开了浏览器,通常在没有面板的Linux服务器中会用到
driver = webdriver.Chrome(options=option)

driver.get(url)
driver.close() #关闭浏览器

代理IP

一个IP地址对服务器过于频繁的访问,会导致服务器屏蔽你的IP地址。爬虫通常使用代理IP池来处理自己IP被封的情况。

可以使用Github上已经写好的代理池

https://github.com/jhao104/proxy_pool

1
2
3
4
5
6
7
8
9
10
11

url = 'https://movie.douban.com/chart'

proxy={
'http':'http://127.0.0.1:9743',
'https':'https://127.0.0.1:9743'
}

response=requests.get(url,proxies=proxy)
print(response.status_code)

超时设置

通常用于访问一些地方较远的网站,或者访问的网站当前过于繁忙,可以设置超时。一旦请求时间超过一定的值,服务器还没有返回响应,那么就直接退出。

1
2
3
4
5
6
7
8
9
10
import requests
from requests.exceptions import ReadTimeout

try:
response = requests.get(url,timeout=0.2)
except ReadTimeout:
print('Timeout')

print(response.status_code)

下载文件

requests还可以用来下载各种文件,比如图片、音乐、视频等。这里用一个例子演示一下:

下载《雨幕》这首歌:

1
2
3
4
5
6
7
8
9
import requests

url='https://sharefs.yun.kugou.com/201910211352/d2b8f1ce854a7f4d01937583ee35b46e/G172/M00/0D/14/jJQEAF2hoIGATyZ4ADq0vjCqr7Q097.mp3'

response = requests.get(url) # 这里要把请求头送入到请求方法中

#保存到文件
with open('雨幕.mp3','wb') as f:
f.write(response.content)

解析网页

解析网页一般用到的库有BeautifulSoup,PyQuery,re,BeautifulSoup又称之为bs4,要区别于之前的bs3。bs4和PyQuery类似,都是通过网页的节点(<div> …</div> 这样的东西称之为结点)来定位内容的。

而re是直接匹配内容,更加灵活。下面是re匹配表,虽然多,但通常我们只需要用到 . * ?这三个符号。

re正则表达式,正则就是用一些具有特殊含义的符号组合到一起(称为正则表达式)来描述字符或者字符串的方法。或者说:正则就是用来描述一类事物的规则。(在Python中)它内嵌在Python中,并通过 re 模块实现。正则表达式模式被编译成一系列的字节码,然后由用 C 编写的匹配引擎执行。

元字符 匹配内容
\w 匹配字母(包含中文)或数字或下划线
\W 匹配非字母(包含中文)或数字或下划线
\s 匹配任意的空白符
\S 匹配任意非空白符
\d 匹配数字
\D 匹配非数字
\A 从字符串开头匹配
\z 匹配字符串的结束,如果是换行,只匹配到换行前的结果
\n 匹配一个换行符
\t 匹配一个制表符
^ 匹配字符串的开始
$ 匹配字符串的结尾
. 匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符。
[…] 匹配字符组中的字符
[^…] 匹配除了字符组中的字符的所有字符
* 匹配0个或者多个左边的字符。
+ 匹配一个或者多个左边的字符。
匹配0个或者1个左边的字符,非贪婪方式。
{n} 精准匹配n个前面的表达式。
{n,m} 匹配n到m次由前面的正则表达式定义的片段,贪婪方式
() 匹配括号内的表达式,也表示一个组
[:alnum:] 字母和数字
[:alpha:] 字母
[:ascii:] ascii字符
[:blank:] 空白字符
[:cntrl:] 控制字符包括换行符、换页符、退格符
[:digit:] 数字
[:graph:] 非控制、非空格字符
[:lower:] 小写字母
[:print:] 可打印字符
[:punct:] 标点符号字符
[:space:] 空白字符
[:upper:] 大写字母
[:xdigit:] 十六进制数字
^$ 匹配空行
< 词首
> 词尾
<pattern> 整个单词

重点

字符 含义
匹配0个或者1个左边的字符,非贪婪方式。
. 匹配任意字符,除了换行符。
* 匹配*号前的字符0次或多次。
1
.*? 的含义就是 非贪婪匹配前面任意字符

通常采用的组合

1
2
3
4
5
6
7
8
9
10
#re.complie写re表达式
'''
如果不使用re.S参数,则只在每一行内进行匹配,如果一行没有,就换下一行重新开始。
而使用re.S参数以后,正则表达式会将这个字符串作为一个整体,在整体中进行匹配。
'''
parent = re.compile('<div.*?页数:.*?</span>(.*?)<br/>', re.S)

#re.findall查找html的内容并和parent匹配,返回一个列表。
result = re.findall(page_parent, html)

项目实战

爬取豆瓣电影榜。这个会用到我们之前在发起请求 -- 添加请求头中写的内容,这里主要关注re解析的过程。

首选选中一个item,观察它在源码的什么位置,然后折叠源码。

一个item


折叠源码后,可以看到选中的这些都十分相似,判断一下可知,他们就是每一部电影的html源码。这样就得到了电影榜单的一般格式。

item列表


依次选取关键字,用来生成re表达式。我们选中了`table width,class="item" class="nbg" title`这些关键字。

image.webp

所以,re表达式是:

1
2
#用括号选出你需要的内容
pattern = re.compile('table.*?class="item".*?nbg.*?title="(.*?)"',re.S)

选中的有些内容其实并不必要,我们就将它去除掉了。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
import re
url = 'https://movie.douban.com/chart'

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36'
}
#请求request
response = requests.get(url,headers=headers)
html = response.text

#解析
pattern = re.compile('table.*?class="item".*?nbg.*?title="(.*?)"',re.S)
result = re.findall(pattern,html)

#打印
print(result)

结果:

爬虫效果

如果想要输出更美观,还可以添加一些字段处理等:

美化效果