爬虫的基本流程及反爬虫机制

爬虫的基本流程

爬虫的基本流程

  • 选取一部分精心挑选的种子URL,将种子URL加入待抓取任务队列。
  • 从待抓取URL队列中取出待抓取的URL,DNS解析得到主机的ip,并将URL对应的网页下载存储进已下载网页库中,将该URL放进已抓取URL队列。
  • 分析提取已下载的网页中的URL,将未抓取过的URL放入待抓取URL队列,从而进入下一个循环。
  • 解析已下载的网页,将需要的数据内容解析出来。
  • 数据持久化,以数据库或其它形式存储。

一个URL的请求与响应

爬虫最主要的任务就是发起请求(Request),然后获取服务器的响应(Response),遵循HTTP协议。

请求与响应

HTTP之请求消息Request

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:

  • 请求行(request line)
  • 请求头部(header)
  • 空行
  • 请求数据

HTTP之请求消息Request

GET请求样例:

1
2
3
4
5
6
7
8
9
GET https://www.tvmao.com/program/CCTV HTTP/1.1
Host: www.tvmao.com
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.26
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: UM_distinctid=164915db395f1-073fb37f3c265b-4d754111-1fa400-164915db39688f;

HTTP之响应消息Response

一般情况下,服务器接收并处理客户端发过来的请求后会返回一个HTTP的响应消息。

HTTP响应也由四个部分组成,分别是:

  • 状态行
  • 消息报头
  • 空行
  • 响应正文

HTTP之响应消息Response

Response响应样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
HTTP/1.1 200 OK
Server: Tengine/2.1.0
Date: Mon, 23 Jul 2018 02:43:33 GMT
Content-Type: text/html;charset=UTF-8
Connection: keep-alive
Vary: Accept-Encoding
Set-Cookie: ASCK=; path=/; expires=Mon, 23-Jul-2018 03:13:33 GMT
Strict-Transport-Security: max-age=15768000
Content-Length: 32143
<!doctype html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>CCTV-1综合节目表,中央电视台综合频道节目表_电视猫</title>
...
</html>

解析网页数据

更多Python爬虫工具

Python HTML解析器

DOM(文档对象模型)树

重点介绍Beautiful Soup

Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库。它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式。Beautiful Soup会帮你节省数小时甚至数天的工作时间。

Beautiful Soup 4.2.0 文档

Beautiful Soup 解析html样例:电视猫

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# -*- coding:utf-8 -*-
import urllib.request as urllib2
import requests
from bs4 import BeautifulSoup
import random
import time
import sys
import os
import urllib3
# 禁用安全请求警告
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
hrefFile = open("testHref.txt", "w")
resFile = open("testRes.txt", "w")
proxyHandler = urllib2.ProxyHandler({
'http': 'http://115.225.88.99:8118',
'http': 'http://118.190.95.43:9001',
'http': 'http://111.155.116.207:8123',
'http': 'http://122.114.31.177:808',
'http': 'http://106.56.102.254:8070',
'http': 'http://111.155.116.249:8123',
'http': 'http://180.118.240.8:61234',
'http': 'http://60.177.225.218:18118'
})
opener = urllib2.build_opener(proxyHandler)
urllib2.install_opener(opener)
prefix = 'https://www.tvmao.com'
todoUrlSet = set()
doneUrlSet = set()
channelsSet = set()
# userAgents是爬虫与反爬虫斗争的第一步
userAgents = ['User-Agent:Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1',
'User-Agent:Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50',
'User-Agent:Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11',
'User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0',
'User-Agent:Mozilla/4.0(compatible;MSIE7.0;WindowsNT5.1;Trident/4.0;InfoPath.2;.NET4.0C;.NET4.0E;.NETCLR2.0.50727;360SE',
'User-Agent:Mozilla/4.0(compatible;MSIE7.0;WindowsNT5.1;Trident/4.0;SE2.XMetaSr1.0;SE2.XMetaSr1.0;.NETCLR2.0.50727;SE2.XMetaSr1.0)',
'User-Agent:Mozilla/5.0(Macintosh;IntelMacOSX10_7_0)AppleWebKit/535.11(KHTML,likeGecko)Chrome/17.0.963.56Safari/535.11 ',
'User-Agent:Mozilla/5.0(Macintosh;U;IntelMacOSX10_6_8;en-us)AppleWebKit/534.50(KHTML,likeGecko)Version/5.1Safari/534.50 ',
'User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.26 Safari/537.36 Core/1.63.5514.400 QQBrowser/10.1.1660.400'
]
header = {}
header['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8'
header['Accept-Encoding'] = 'gzip, deflate, br'
header['Accept-Language'] = 'zh-CN,zh;q=0.9'
header['Connection'] = 'keep-alive'
header['Host'] = 'www.tvmao.com'
header['Origin'] = 'https://www.tvmao.com'
header['Referer'] = 'https://www.tvmao.com/program/channels'
header['User-Agent'] = ''
header['Cache-Control'] = 'max-age=0'
header['Upgrade-Insecure-Requests'] = '1'
def getHeaders():
# 随机获取一个headers
header['User-Agent'] = random.choice(userAgents)
return header
def parseHtml(html):
# print(html)
div = BeautifulSoup(html, "lxml").find_all('div', class_='chlsnav')
divBs = BeautifulSoup(str(div), "lxml")
# print(divBs.prettify())
ul = divBs.ul.extract()
# print(BeautifulSoup(str(ul), "lxml").prettify)
# print(BeautifulSoup(str(divBs), "lxml").prettify)
# sys.stdout.flush()
# os._exit(0)
# 寻找可能的新链接页面
allA = divBs.find_all('a')
for each in allA:
href = each['href']
if (href not in doneUrlSet) and (href not in todoUrlSet):
print(each.string, href)
print(each.string, href, file=hrefFile)
todoUrlSet.add(str(href))
# 搜集未发现的节目
# 查找当前频道仅为了结果的顺序更好一些
curChn = ul.find_all('li', class_='curchn')
# print(curChn)
for each in curChn:
chn = each.string
if chn not in channelsSet:
print(chn)
print(chn, file=resFile)
channelsSet.add(chn)
a = ul.find_all('a')
# print(a)
for each in a:
chn = each.string
if chn not in channelsSet:
print(chn)
print(chn, file=resFile)
channelsSet.add(chn)
def spiderGet(url):
response = requests.get(url=url, headers=getHeaders(), verify=False)
response.encoding = 'utf-8'
parseHtml(html=response.text)
def spiderOneRoot(rootUrl):
todoUrlSet.clear()
doneUrlSet.clear()
todoUrlSet.add(rootUrl)
failed = 0
while len(todoUrlSet) > 0:
postfix = todoUrlSet.pop()
try:
doneUrlSet.add(postfix)
url = prefix+postfix
spiderGet(url)
sys.stdout.flush()
hrefFile.flush()
resFile.flush()
failed = 0
except:
# print(err)
todoUrlSet.add(postfix)
# t = random.randint(0, 30)
# print('sleep', t, 'sec')
time.sleep(10)
failed = failed+1
print('failed', failed, 'time')
if failed > 120:
print(postfix+'failed too much !')
return
# spiderOneRoot('/program/CCTV')
# os._exit(0)
entryUrl = [
'/program/CCTV',
'/program_satellite/AHTV1-w3.html',
'/program_digital/CCTV3D-w3.html',
'/program/TVB',
'/program/AUMEN',
'/program/STARTV',
'/program/AUSTRALIANETWORK',
'/program/HEBEI-HEBEI1-w1.html',
'/program/XIZANGTV-XIZANGTV1-w1.html',
'/program/ZJTV-ZJTV1-w1.html',
'/program/GSTV-GSTV1-w1.html',
'/program/JXTV-JXTV1-w1.html',
'/program/LNTV-LNTV1-w1.html',
'/program/CCQTV-CCQTV1-w1.html',
'/program/SDTV-SDTV1-w1.html',
'/program/HAINANTV',
'/program/YNTV-YNTV1-w1.html',
'/program/GUIZOUTV-GUIZOUTV1-w1.html',
'/program/AHTV-AHTV1-w1.html',
'/program/AHTV-AHTV1-w1.html',
'/program/JILIN-JILIN1-w1.html',
'/program/QHTV-QHTV1-w1.html',
'/program/SCTV-SCTV1-w1.html',
'/program/NMGTV-NMGTV1-w1.html',
'/program/HUBEI-HUBEI1-w1.html',
'/program/NXTV-NXTV2-w1.html',
'/program/GUANXI-GUANXI2-w1.html',
'/program/XJTV-XJTV1-w1.html',
'/program/SHHAI',
'/program/HLJTV-HLJTV1-w1.html',
'/program/HNTV-HNTV1-w1.html',
'/program/HNTV-HNTV1-w1.html',
'/program/SXTV-SXTV1-w1.html',
'/program/BTV-BTV1-w1.html',
'/program/GDTV-GDTV1-w1.html',
'/program/JSTV-JSTV1-w1.html',
'/program/TJTV-TJTV1-w1.html',
'/program/HUNANTV-HUNANTV1-w1.html',
'/program/SHXITV-SHXITV1-w1.html',
'/program/FJTV-FJTV2-w1.html'
]
for url in entryUrl:
spiderOneRoot(url)

存储数据

爬虫的抓取策略

在爬虫系统中,待抓取URL队列是很重要的一部分。待抓取URL队列中的URL以什么样的顺序排列也是一个很重要的问题,因为这涉及到先抓取那个页面,后抓取哪个页面。而决定这些URL排列顺序的方法,叫做抓取策略。下面重点介绍几种常见的抓取策略:

深度优先策略(DFS)

深度优先策略是指爬虫从某个URL开始,一个链接一个链接的爬取下去,直到处理完了某个链接所在的所有线路,才切换到其它的线路。 此时抓取顺序为:A -> B -> C -> D -> E -> F -> G -> H -> I -> J

广度优先策略(BFS)

宽度优先遍历策略的基本思路是,将新下载网页中发现的链接直接插入待抓取URL队列的末尾。也就是指网络爬虫会先抓取起始网页中链接的所有网页,然后再选择其中的一个链接网页,继续抓取在此网页中链接的所有网页。 此时抓取顺序为:A -> B -> E -> G -> H -> I -> C -> F -> J -> D

高效抓取数据(多线程/多进程/分布式爬虫)

分布式爬取,针对大型爬虫系统的,实现一个分布式的爬虫,主要为以下几个步骤:

  • 1、基本的http抓取工具,如scrapy;
  • 2、避免重复抓取网页,如Bloom Filter;
  • 3、维护一个所有集群机器能够有效分享的分布式队列;
  • 4、将分布式队列和Scrapy的结合;
  • 5、后续处理,网页析取(如python-goose),存储(如Mongodb)。

反爬虫机制与应对策略

爬虫:使用任何技术手段,批量获取网站信息的一种方式。 反爬虫:使用任何技术手段,阻止别人批量获取自己网站信息的一种方式。

**常见的反爬虫机制:**一般网站从三个方面反爬虫,即请求网站访问时的请求头Headers,用户行为,目标网站的目录和数据加载方式。前两个方面可以说是反爬虫策略中最为常见的,而第三个则是应用ajax(异步加载)的方式加载页面目录或者内容,增大爬虫在对目标网站形成访问之后获取数据的难度。

0x01 通过Headers反爬虫

  • 通过Headers中的User-Agent识别爬虫 从用户请求的Headers反爬虫是最常见的反爬虫策略。由于正常用户访问网站时是通过浏览器访问的,所以目标网站通常会在收到请求时校验Headers中的User-Agent字段,如果不是携带正常的User-Agent信息的请求便无法通过请求。

  • 通过Headers中的Refer防止盗链 盗链是指服务提供商自己不提供服务的内容,利用别人网站的链接去获取别人网站里面的图片或者视频等资源,一部分网站为了防盗链,还会校验请求Headers中的Referer字段。

0x01 应对策略

  • 构造Headers 针对这类反爬虫机制,可以直接在自己写的爬虫中添加Headers,将浏览器的User-Agent复制到爬虫的Headers中,或使用User-Agent池。也就是每次发送的时候随机从池中选择不一样的浏览器头信息,防止暴露爬虫身份;另外通过对请求的抓包分析,将Referer值修改为目标网站域名,就能很好的绕过。

0x02 基于用户行为反爬虫

  • 设置IP访问频率,如果超过一定频率,弹出验证码 如果输入正确的验证码,则放行,如果没有输入,则拉入禁止一段时间,如果超过禁爬时间,再次出发验证码,则拉入黑名单。当然根据具体的业务,为不同场景设置不同阈值,比如登陆用户和非登陆用户,请求是否含有refer。

  • 通过并发识别爬虫 有些爬虫的并发是很高的,统计并发最高的IP,加入黑名单(或者直接封掉爬虫IP所在C段)

  • 请求的时间窗口过滤统计 爬虫爬取网页的频率都是比较固定的,不像人去访问网页,中间的间隔时间比较无规则,所以我们可以给每个IP地址建立一个时间窗口,记录IP地址最近12次访问时间,每记录一次就滑动一次窗口,比较最近访问时间和当前时间,如果间隔时间很长判断不是爬虫,清除时间窗口,如果间隔不长,就回溯计算指定时间段的访问频率,如果访问频率超过阀值,就转向验证码页面让用户填写验证码。

  • 限制单个ip/api token的访问量 比如15分钟限制访问页面180次,具体标准可参考一些大型网站的公开api,如twitter api,对于抓取用户公开信息的爬虫要格外敏感

0x02 应对策略

  • 降低请求频率 例如每隔一个时间段请求一次或者请求若干次之后sleep一段时间,也可以每次请求后随机间隔几秒再进行下一次请求。由于网站获取到的ip是一个区域网的ip,该ip被区域内的所有人共享,因此这个间隔时间并不需要特别长。

  • 使用代理IP池 使用代理IP池需要大量的IP资源。可以专门写一个在网上抓取可用代理ip的脚本,然后将抓取到的代理ip维护到代理池中供爬虫使用,当然,实际上抓取的ip不论是免费的还是付费的,通常的使用效果都极为一般,如果需要抓取高价值数据的话也可以考虑购买宽带adsl拨号的VPS,如果ip被目标网站被封掉,重新拨号即可。

0x03 动态页面的反爬虫

  • 数据通过ajax请求得到或JavaScript生成 上述的几种情况大多都是出现在静态页面,但是对于动态网页,我们需要爬取的数据是通过ajax请求得到,或者通过JavaScript生成的。

  • ajax请求的所有参数全部加密 网站把ajax请求的所有参数全部加密了。我们根本没办法构造自己所需要的数据的请求。还有一些严防死守的网站,除了加密ajax参数,它还把一些基本的功能都封装了,全部都是在调用自己的接口,而接口参数都是加密的。

0x03 应对策略

  • 模拟ajax请求获取数据 首先用Firebug或者HttpFox对网络请求进行分析。如果能够找到ajax请求,也能分析出具体的参数和响应的具体含义,我们就能采用上面的方法,直接利用requests或者urllib2模拟ajax请求,对响应的json进行分析得到需要的数据。

  • 调用浏览器内核模拟正常人的交互 通过selenium+phantomJS框架,调用浏览器内核,并利用phantomJS执行js来模拟人为操作以及触发页面中的js脚本。从填写表单到点击按钮再到滚动页面,全部都可以模拟,不考虑具体的请求和响应过程,只是完完整整的把人浏览页面获取数据的过程模拟一遍。用这套框架几乎能绕过大多数的反爬虫,因为它不是在伪装成浏览器来获取数据(上述的通过添加Headers一定程度上就是为了伪装成浏览器),它本身就是浏览器,phantomJS就是一个没有界面的浏览器,只是操控这个浏览器的不是人。

0x04 其它反爬虫机制-Cookie限制

  • Cookie限制 和Headers校验的反爬虫机制类似,当用户向目标网站发送请求时,会再请求数据中携带Cookie,网站通过校验请求信息是否存在Cookie,以及校验Cookie的值来判定发起访问请求的到底是真实的用户还是爬虫,第一次打开网页会生成一个随机cookie,如果再次打开网页这个Cookie不存在,那么再次设置,第三次打开仍然不存在,这就非常有可能是爬虫在工作了。
  • Cookie校验和Headers的区别 用户发送的Headers的内容形式是固定的可以被轻易伪造的,Cookie则不然。原因是由于浏览器请求网站访问的过程中所分析得到的Cookie往往都是经过相关的js等过程已经改变了domain的Cookie,假如直接手动修改爬虫携带的Cookie去访问对应的网页,由于携带的Cookie已经是访问之后的domain而不是访问之前的domain,所以是无法成功模拟整个流程的,这种情况必然导致爬虫访问页面失败。

0x04 应对策略

  • 分析Cookie 分析Cookie,可能会携带大量的随机哈希字符串,或者不同时间戳组合的字符串,并且会根据每次访问更新domain的值。分析过程:首先要在对目标网站抓包分析时,必须先清空浏览器的Cookie,然后在初次访问时,观察浏览器在完成访问的过程中的请求细节(通常会在这一过程中发生若干次301/302转跳,每次转跳网站返回不同的Cookie给浏览器然后在最后一次转跳中请求成功)。在抓包完成对请求细节的分析之后,再在爬虫上模拟这一转跳过程,然后截取Cookie作为爬虫自身携带的Cookie,这样就能够绕过Cookie的限制完成对目标网站的访问了。

0x05 其它反爬虫机制-验证码限制

  • 验证码(CAPTCHA)是“Completely Automated Public Turing test to tell Computers and Humans Apart”(全自动区分计算机和人类的图灵测试)的缩写,是一种区分用户是计算机还是人的公共全自动程序。人类有着一种天赋,可以很轻松的从一段图片中识别出文字和数字,而机器却不能。

  • 数字字母验证码 这类验证码优点是生成简单,大部分语言都自带图形库,在加入一点扭曲和噪点,基本可以解决掉初级爬虫工程师和暴力破解的脚本小子。缺点当然也显而易见,对于cnn代码只用写10行的今天,这些验证码只要有足够的标注数据,破解起来简直是轻而易举。

  • 中文验证码 比如知乎采用过的验证码选择倒立的中文以及网易邮箱类的验证码(标记文字位置)。这类验证码一般来很少有公用的代码,因此编写起来麻烦。不过也正因为每一家采用的都不一样,在加上中文庞大的汉字库,确实给机器识别带来的比较大的挑战,如果配合好字体和扭曲,确实可以有不错的效果。

  • 极验验证 极验的v2版验证体系破解难度甚至比不过数字字母验证码来的复杂,唯一的优势就是现在大部分打码平台不支持。图片识别上直接对比像素就可以识别位置,唯一难点的移动轨迹,但用轨迹做分类人类和机器间的特征并不那么明显,加上网页中采集轨迹的准确性较差,因此做一些简单的模拟就可以轻松躲过轨迹识别。

  • 其它验证类型 比如12306这类图片验证,这种反爬虫要求较高,需要强大的图片库。其它还有算术验证、成语验证等等,发爬虫通过难度不是很高。

0x05 应对策略

  • 打码平台 最常用,最简单的识别,一般字母文字1分一次,中文识别略贵,计算题可能5分,不支持极验这类。

  • OCR库 传统的ocr采用先切割再识别的方案,对于新型的验证码已经很难做了。

  • 机器学习 端到端数字字母识别神器,根据识别难度和长度不同,对标注数据的需求量不一样,当然图片预处理也稍微有些区别。一般简单常见的识别可以直接从网上找代码。

参考文档