网络爬虫简介
合法性和注意事项
根据世界各地法院的一些案件:
- 如果抓取数据的行为用于个人使用,则不存在问题;
- 如果用于转载,则数据应该是现实生活中的真实数据(营业地址、电话清单等);如果是原创数据(意见和评论),通常就会受到版权限制,而不能转载。
作为爬虫的自觉:约束自己的抓取速度,伪装成正常用户,使用浏览器UA来标识自己。
背景调研
检查robots.txt文件
使用urllib.robotparser
模块解析robots.txt;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| >>> import urllib.robotparser
>>> rp = urllib.robotparser.RobotFileParser()
>>> rp.set_url("http://www.musi-cal.com/robots.txt")
>>> rp.read()
>>> rrate = rp.request_rate("*")
>>> rrate.requests
3
>>> rrate.seconds
20
>>> rp.crawl_delay("*")
6
>>> rp.can_fetch("*", "http://www.musi-cal.com/cgi-bin/search?city=San+Francisco")
False
>>> rp.can_fetch("*", "http://www.musi-cal.com/")
True
|
检查sitmap.xml文件
网站地图提供了所有网页的链接,网址由<loc></loc>
标签包裹。
虽然sitemap文件提供了一种爬取网站的有效方式,但是我们仍需要对其谨慎处理,因为该文件经常存在缺失、过期或不完整的问题。
估算网站大小
目标网站的大小会影响我们如何进行爬取,大型网站需要考虑效率问题。我们可以通过Google搜索的site
关键词过滤域名结果,从而获取该信息,在Google中搜索:site:example.webscraping.com
。
在域名后面添加URL路径,可以对结果进行过滤,仅显示网站的某些部分。
识别网站所用的技术
使用builtwith
模块(目前只支持Python2,作者已经更新了Python3的支持,但还没有提交到PyPI)分析网站使用的框架和技术;
可以使用Chrome扩展程序wappalyzer替代。
1
2
3
4
5
6
| >>> builtwith('http://wordpress.com')
{u'blogs': [u'PHP', u'WordPress'], u'font-scripts': [u'Google Font API'], u'web-servers': [u'Nginx'], u'javascript-frameworks': [u'Modernizr'], u'programming-languages': [u'PHP'], u'cms': [u'WordPress']}
>>> builtwith('http://webscraping.com')
{u'javascript-frameworks': [u'jQuery', u'Modernizr'], u'web-frameworks': [u'Twitter Bootstrap'], u'web-servers': [u'Nginx']}
>>> builtwith('http://microsoft.com')
{u'javascript-frameworks': [u'jQuery'], u'mobile-frameworks': [u'jQuery Mobile'], u'operating-systems': [u'Windows Server'], u'web-servers': [u'IIS']}
|
寻找网站所有者
使用python-whois
包进行whois查询。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| >>> from pprint import pprint
>>> pprint(whois.whois('senguo.cc'))
{'address': 'Le Jia International No.999 Liang Mu Road Yuhang District',
'city': 'Hangzhou',
'country': 'CN',
'creation_date': datetime.datetime(2014, 3, 25, 2, 36, 37),
'dnssec': 'unsigned',
'domain_name': ['SENGUO.CC', 'senguo.cc'],
'emails': ['[email protected]', '[email protected]'],
'expiration_date': datetime.datetime(2021, 3, 25, 2, 36, 37),
'name': 'Nexperian Holding Limited',
'name_servers': ['DNS31.HICHINA.COM',
'DNS32.HICHINA.COM',
'dns31.hichina.com',
'dns32.hichina.com'],
'org': 'Nexperian Holding Limited',
'referral_url': None,
'registrar': 'HICHINA ZHICHENG TECHNOLOGY LTD.',
'state': 'Zhejiang',
'status': ['ok https://icann.org/epp#ok', 'ok http://www.icann.org/epp#OK'],
'updated_date': datetime.datetime(2017, 2, 5, 1, 42, 34),
'whois_server': 'grs-whois.hichina.com',
'zipcode': '311121'}
|
爬取(crawling)
为了抓取网站,我们首先需要下载包含有感兴趣数据的网页,该过程一般被称为爬取(crawling)。爬取一个网站有很多种方法,而选用那种方法更加合适,则取决于目标网站的结构。常见的3种爬取网站的方法:
- 爬取网站地图sitemap.xml;
- 便利每个网页的数据库ID;
- 跟踪网页链接。
要写一个健壮的爬虫,最好能实现以下高级特性。
自动重试
下载时遇到的错误经常是临时性的,对于此类错误,我们可以尝试重新下载。
- 4xx错误发生在请求存在问题时,不必重试;
- 5xx错误发生在服务器存在问题时,应该重试。
使用递归实现:
1
2
3
4
5
6
7
8
9
| def download(url, num_retries=3):
try:
result = ... # 下载相关代码
except DownloadError as e:
http_code = ... # 获取http code
if num_retries > 0:
if 500 <= http_code < 600:
return download(url, num_retries-1)
return result
|
设置User-Agent
1
| Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36
|
1
| Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8
|
1
| Mozilla/5.0 (iPad; CPU OS 10_3_3 like Mac OS X) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.0 Mobile/14G60 Safari/602.1
|
1
| Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_3 like Mac OS X) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.0 Mobile/14G60 Safari/602.1
|
使用代理
将可用的代理地址组成一个list,每次请求时使用random.choice
从中随机取出一个使用。当然,这需要不断维护一个可用的代理列表。
在requests
模块中,请求的proxies
参数可以单独指定某个地址使用代理:
Dictionary mapping protocol or protocol and host to the URL of the proxy (e.g. {‘http’: ‘foo.bar:3128’, ‘http://host.name’: ‘foo.bar:4012’}) to be used on each Request.
下载限速
如果爬取速度过快,就会面临被服务器封禁或造成服务器过载的风险。为了降低这些风险,我们可以在两次下载之间添加延时,从而对爬虫限速。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| import urllib.parse
import time
class Throttle:
"""Add delay between downloads to the same domain"""
def __init__(self, delay):
# amount of delay between downloads for each domain
self.delay = delay
self.domains = {}
def wait(self, url):
domain = urllib.parse.urlparse(url).netloc
last_accessed = self.domains.get(domain)
if self.delay > 0 and last_accessed is not None:
sleep_secs = self.delay - (time.time() - last_accessed) / 1000
if sleep_secs > 0:
time.sleep(sleep_secs)
self.domains[domain] = time.time()
|
控制爬取深度
跟踪网页链接爬取时,我们的爬虫会跟踪所有之前没有访问过的链接。但是一些网站会动态生成页面内容,这样会出现无限多的网页,页面无止境的链接下去,被称为爬虫陷阱。
避免爬虫陷阱的一种有效方式,是记录到达当前网页时的深度。当达到最大深度时,爬虫不再向队列中添加该网页中的链接。
1
2
3
4
5
6
7
8
9
10
| def crawler(..., max_depth=2):
max_depth = 2
seen = {}
...
depth = seen[url]
if depth != max_depth:
for link in links:
if link not in seen:
seen[link] = depth + 1
crawl_queue.append(link)
|