目录

用Python写网络爬虫(1)-网络爬虫简介

网络爬虫简介

合法性和注意事项

根据世界各地法院的一些案件:

  • 如果抓取数据的行为用于个人使用,则不存在问题;
  • 如果用于转载,则数据应该是现实生活中的真实数据(营业地址、电话清单等);如果是原创数据(意见和评论),通常就会受到版权限制,而不能转载。

作为爬虫的自觉:约束自己的抓取速度,伪装成正常用户,使用浏览器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

  • Mac + Chrome:
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
  • Mac + Safari:
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
  • iPad Mini4 + Safari:
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
  • iPhone SE + Safari:
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)