整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

「Python」教你编写网络爬虫

.网络爬虫何时有用

假设我有一个鞋店,并且想要及时了解竞争对手的价格。我可以每天访问他们的网站,与我店铺中鞋子的价格进行对比。但是,如果我店铺中的鞋类品种繁多,或是希望能够更加频繁地查看价格变化的话,就需要花费大量的时间,甚至难以实现。再举一个例子,我看中了一双鞋,想等它促销时再购买。我可能需要每天访问这家鞋店的网站来查看这双鞋是否降价,也许需要等待几个月的时间,我才能如愿盼到这双鞋促销。上述这 两个重复性的手工流程,都可以利用网络爬虫技术实现自动化处理。

理想状态下,网络爬虫并不是必须品,每个网站都应该提供API,以结构化的格式共享它们的数据。然而现实情况中,虽然一些网站已经提供了这种API,但是它们通常会限制可以抓取的数据,以及访问这些数据的频率。另外,对于网站的开发者而言,维护前端界面比维护后端API接口优先级更高。总之,我们不能仅仅依赖于API去访问我们所需的在线数据,而是应该学习一些网络爬虫技术的相关知识。

2. 网络爬虫是否合法

网络爬虫目前还处于早期的蛮荒阶段,“允许哪些行为”这种基本秩序还处于建设之中。从目前的实践来看,如果抓取数据的行为用于个人使用,则不存在问题;而如果数据用于转载,那么抓取的数据类型就非常关键了。

世界各地法院的一些案件可以帮助我们确定哪些网络爬虫行为是允许的。在Feist Publications, Inc.起诉Rural Telephone Service Co.的案件中,美国联邦最高法院裁定抓取并转载真实数据(比如,电话清单)是允许的。而在澳大利亚,Telstra Corporation Limited起诉Phone Directories Company Pty Ltd这一类似案件中,则裁定只有拥有明确作者的数据,才可以获得版权。此外,在欧盟的ofir.dk起诉home.dk一案中,最终裁定定期抓取和深度链接是允许的。

这些案件告诉我们,当抓取的数据是现实生活中的真实数据(比如,营业地址、电话清单)时,是允许转载的。但是,如果是原创数据(比如,意见和评论),通常就会受到版权限制,而不能转载。

无论如何,当你抓取某个网站的数据时,请记住自己是该网站的访客,应当约束自己的抓取行为,否则他们可能会封禁你的IP,甚至采取更进一步的法律行动。这就要求下载请求的速度需要限定在一个合理值之内,并且还需要设定一个专属的用户代理来标识自己。在下面的小节中我们将会对这些实践进行具体介绍。

关于上述几个法律案件的更多信息可以参考下述地址:

  • http://caselaw.lp.findlaw.com/scripts/getcase. pl?court=US&vol=499&invol=340
  • http://www.austlii.edu.au/au/cases/cth/FCA/2010/44.html
  • http://www.bvhd.dk/uploads/tx_mocarticles/S_og_Handelsrettens_afg_relse_i_Ofir-sagen.pdf

3. 背景调研

在深入讨论爬取一个网站之前,我们首先需要对目标站点的规模和结构进行一定程度的了解。网站自身的robots.txt和Sitemap文件都可以为我们提供一定的帮助,此外还有一些能提供更详细信息的外部工具,比如Google搜索和WHOIS。

3.1 检查robots.txt

大多数网站都会定义robots.txt文件,这样可以让爬虫了解爬取该网站时存在哪些限制。这些限制虽然仅仅作为建议给出,但是良好的网络公民都应当遵守这些限制。在爬取之前,检查robots.txt文件这一宝贵资源可以最小化爬虫被封禁的可能,而且还能发现和网站结构相关的线索。关于robots.txt协议的更多信息可以参见http://www.robotstxt.org。下面的代码是我们的示例文件robots.txt中的内容,可以访问http://example.webscraping.com/robots.txt获取。

    # section 1
    User-agent: BadCrawler
    Disallow: /

    # section 2
    User-agent: *
    Crawl-delay: 5
    Disallow: /trap

    # section 3
    Sitemap: http://example.webscraping.com/sitemap.xml

在section 1中,robots.txt文件禁止用户代理为BadCrawler的爬虫爬取该网站,不过这种写法可能无法起到应有的作用,因为恶意爬虫根本不会遵从robots.txt的要求。本章后面的一个例子将会展示如何让爬虫自动遵守robots.txt的要求。

section 2规定,无论使用哪种用户代理,都应该在两次下载请求之间给出5秒的抓取延迟,我们需要遵从该建议以避免服务器过载。这里还有一个/trap链接,用于封禁那些爬取了不允许链接的恶意爬虫。如果你访问了这个链接,服务器就会封禁你的IP一分钟!一个真实的网站可能会对你的IP封禁更长时间,甚至是永久封禁。不过如果这样设置的话,我们就无法继续这个例子了。

section 3定义了一个Sitemap文件,我们将在下一节中了解如何检查该文件。

3.2 检查网站地图

网站提供的Sitemap文件(即网站地图)可以帮助爬虫定位网站最新的内容,而无须爬取每一个网页。如果想要了解更多信息,可以从http://www.sitemaps.org/protocol.html获取网站地图标准的定义。下面是在robots.txt文件中发现的Sitemap文件的内容。

    <?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
      <url><loc>http://example.webscraping.com/view/Afghanistan-1
        </loc></url>
      <url><loc>http://example.webscraping.com/view/Aland-Islands-2
       </loc></url>
      <url><loc>http://example.webscraping.com/view/Albania-3</loc>
        </url>
      ...
    </urlset>

网站地图提供了所有网页的链接,我们会在后面的小节中使用这些信息,用于创建我们的第一个爬虫。虽然Sitemap文件提供了一种爬取网站的有效方式,但是我们仍需对其谨慎处理,因为该文件经常存在缺失、过期或不完整的问题。

3.3 估算网站大小

目标网站的大小会影响我们如何进行爬取。如果是像我们的示例站点这样只有几百个URL的网站,效率并没有那么重要;但如果是拥有数百万个网页的站点,使用串行下载可能需要持续数月才能完成,这时就需要使用第4章中介绍的分布式下载来解决了。

估算网站大小的一个简便方法是检查Google爬虫的结果,因为Google很可能已经爬取过我们感兴趣的网站。我们可以通过Google搜索的site关键词过滤域名结果,从而获取该信息。我们可以从http://www.google.com/advanced_search了解到该接口及其他高级搜索参数的用法。

图1所示为使用site关键词对我们的示例网站进行搜索的结果,即在Google中搜索site:example.webscraping.com。

从图1中可以看出,此时Google估算该网站拥有202个网页,这和实际情况差不多。不过对于更大型的网站,我们会发现Google的估算并不十分准确。

在域名后面添加URL路径,可以对结果进行过滤,仅显示网站的某些部分。图2所示为搜索site:example.webscraping.com/view的结果。该搜索条件会限制Google只搜索国家页面。

图1

图2

这种附加的过滤条件非常有用,因为在理想情况下,你只希望爬取网站中包含有用数据的部分,而不是爬取网站的每个页面。

3.4 识别网站所用技术

构建网站所使用的技术类型也会对我们如何爬取产生影响。有一个十分有用的工具可以检查网站构建的技术类型——builtwith模块。该模块的安装方法如下。

    pip install builtwith

该模块将URL作为参数,下载该URL并对其进行分析,然后返回该网站使用的技术。下面是使用该模块的一个例子。

    >>> import builtwith
    >>> builtwith.parse('http://example.webscraping.com')
    {u'javascript-frameworks': [u'jQuery', u'Modernizr', u'jQuery UI'],
     u'programming-languages': [u'Python'],
     u'web-frameworks': [u'Web2py', u'Twitter Bootstrap'],
     u'web-servers': [u'Nginx']}

从上面的返回结果中可以看出,示例网站使用了Python的Web2py框架,另外还使用了一些通用的JavaScript库,因此该网站的内容很有可能是嵌入在HTML中的,相对而言比较容易抓取。而如果改用AngularJS构建该网站,此时的网站内容就很可能是动态加载的。另外,如果网站使用了ASP.NET,那么在爬取网页时,就必须要用到会话管理和表单提交了。

3.5 寻找网站所有者

对于一些网站,我们可能会关心其所有者是谁。比如,我们已知网站的所有者会封禁网络爬虫,那么我们最好把下载速度控制得更加保守一些。为了找到网站的所有者,我们可以使用WHOIS协议查询域名的注册者是谁。Python中有一个针对该协议的封装库,其文档地址为https://pypi.python.org/pypi/python-whois,我们可以通过pip进行安装。

    pip install python-whois

下面是使用该模块对appspot.com这个域名进行WHOIS查询时的返回结果。

    >>> import whois
    >>> print whois.whois('appspot.com')
    {
      ...
      "name_servers": [
        "NS1.GOOGLE.COM",
        "NS2.GOOGLE.COM",
        "NS3.GOOGLE.COM",
        "NS4.GOOGLE.COM",
        "ns4.google.com",
        "ns2.google.com",
        "ns1.google.com",
        "ns3.google.com"
      ],
    "org": "Google Inc.",
    "emails": [
        "abusecomplaints@markmonitor.com",
        "dns-admin@google.com"
      ]
    }

从结果中可以看出该域名归属于Google,实际上也确实如此。该域名是用于Google App Engine服务的。当我们爬取该域名时就需要十分小心,因为Google经常会阻断网络爬虫,尽管实际上其自身就是一个网络爬虫业务。

4. 编写第一个网络爬虫

为了抓取网站,我们首先需要下载包含有感兴趣数据的网页,该过程一般被称为爬取(crawling)。爬取一个网站有很多种方法,而选用哪种方法更加合适,则取决于目标网站的结构。我们首先会探讨如何安全地下载网页,然后会介绍如下3种爬取网站的常见方法:

  • 爬取网站地图;
  • 遍历每个网页的数据库ID;
  • 跟踪网页链接。

4.1 下载网页

要想爬取网页,我们首先需要将其下载下来。下面的示例脚本使用Python的urllib2模块下载URL。

    import urllib2
    def download(url):
        return urllib2.urlopen(url).read()

当传入URL参数时,该函数将会下载网页并返回其HTML。不过,这个代码片段存在一个问题,即当下载网页时,我们可能会遇到一些无法控制的错误,比如请求的页面可能不存在。此时,urllib2会抛出异常,然后退出脚本。安全起见,下面再给出一个更健壮的版本,可以捕获这些异常。

    import urllib2

    def download(url):
        print 'Downloading:', url
        try:
            html = urllib2.urlopen(url).read()
        except urllib2.URLError as e:
            print 'Download error:', e.reason
            html = None
        return html

现在,当出现下载错误时,该函数能够捕获到异常,然后返回None。

1.重试下载

下载时遇到的错误经常是临时性的,比如服务器过载时返回的503 Service Unavailable错误。对于此类错误,我们可以尝试重新下载,因为这个服务器问题现在可能已解决。不过,我们不需要对所有错误都尝试重新下载。如果服务器返回的是404 Not Found这种错误,则说明该网页目前并不存在,再次尝试同样的请求一般也不会出现不同的结果。

互联网工程任务组(Internet Engineering Task Force)定义了HTTP错误的完整列表,详情可参考https://tools.ietf.org/html/rfc7231#section-6。从该文档中,我们可以了解到4xx错误发生在请求存在问题时,而5xx错误则发生在服务端存在问题时。所以,我们只需要确保download函数在发生5xx错误时重试下载即可。下面是支持重试下载功能的新版本 代码。

    def download(url, num_retries=2):
        print 'Downloading:', url
        try:
            html = urllib2.urlopen(url).read()
        except urllib2.URLError as e:
            print 'Download error:', e.reason
            html = None
            if num_retries > 0:
                if hasattr(e, 'code') and 500 <= e.code < 600:
                    # recursively retry 5xx HTTP errors
                    return download(url, num_retries-1)
        return html

现在,当download函数遇到5xx错误码时,将会递归调用函数自身进行重试。此外,该函数还增加了一个参数,用于设定重试下载的次数,其默认值为两次。我们在这里限制网页下载的尝试次数,是因为服务器错误可能暂时还没有解决。想要测试该函数,可以尝试下载http://httpstat.us/500,该网址会始终返回500错误码。

>>> download('http://httpstat.us/500')
Downloading: http://httpstat.us/500
Download error: Internal Server Error
Downloading: http://httpstat.us/500
Download error: Internal Server Error
Downloading: http://httpstat.us/500
Download error: Internal Server Error

从上面的返回结果可以看出,download函数的行为和预期一致,先尝试下载网页,在接收到500错误后,又进行了两次重试才放弃。

2.设置用户代理

默认情况下,urllib2使用Python-urllib/2.7作为用户代理下载网页内容,其中2.7是Python的版本号。如果能使用可辨识的用户代理则更好,这样可以避免我们的网络爬虫碰到一些问题。此外,也许是因为曾经历过质量不佳的Python网络爬虫造成的服务器过载,一些网站还会封禁这个默认的用户代理。比如,在使用Python默认用户代理的情况下,访问http://www.meetup.com/,目前会返回如图3所示的访问拒绝提示。


图3

因此,为了下载更加可靠,我们需要控制用户代理的设定。下面的代码对download函数进行了修改,设定了一个默认的用户代理“wswp”(即Web Scraping with Python的首字母缩写)。

    def download(url, user_agent='wswp', num_retries=2):
        print 'Downloading:', url
        headers = {'User-agent': user_agent}
        request = urllib2.Request(url, headers=headers)
        try:
            html = urllib2.urlopen(request).read()
        except urllib2.URLError as e:
            print 'Download error:', e.reason
            html = None
            if num_retries > 0:
                if hasattr(e, 'code') and 500 <= e.code < 600:
                    # retry 5XX HTTP errors
                    return download(url, user_agent, num_retries-1)
        return html

现在,我们拥有了一个灵活的下载函数,可以在后续示例中得到复用。该函数能够捕获异常、重试下载并设置用户代理。

4.2 网站地图爬虫

在第一个简单的爬虫中,我们将使用示例网站robots.txt文件中发现的网站地图来下载所有网页。为了解析网站地图,我们将会使用一个简单的正则表达式,从<loc>标签中提取出URL。下面是该示例爬虫的代码。

    def crawl_sitemap(url):
        # download the sitemap file
        sitemap = download(url)
        # extract the sitemap links
        links = re.findall('<loc>(.*?)</loc>', sitemap)
        # download each link
        for link in links:
            html = download(link)
            # scrape html here
            # ...

现在,运行网站地图爬虫,从示例网站中下载所有国家页面。

>>> crawl_sitemap('http://example.webscraping.com/sitemap.xml')
Downloading: http://example.webscraping.com/sitemap.xml
Downloading: http://example.webscraping.com/view/Afghanistan-1
Downloading: http://example.webscraping.com/view/Aland-Islands-2
Downloading: http://example.webscraping.com/view/Albania-3
...

可以看出,上述运行结果和我们的预期一致,不过正如前文所述,我们无法依靠Sitemap文件提供每个网页的链接。下面我们将会介绍另一个简单的爬虫,该爬虫不再依赖于Sitemap文件。

4.3 ID遍历爬虫

本节中,我们将利用网站结构的弱点,更加轻松地访问所有内容。下面是一些示例国家的URL。

  • http://example.webscraping.com/view/Afghanistan-1
  • http://example.webscraping.com/view/Australia-2
  • http://example.webscraping.com/view/Brazil-3

可以看出,这些URL只在结尾处有所区别,包括国家名(作为页面别名)和ID。在URL中包含页面别名是非常普遍的做法,可以对搜索引擎优化起到帮助作用。一般情况下,Web服务器会忽略这个字符串,只使用ID来匹配数据库中的相关记录。下面我们将其移除,加载http://example.webscraping.com/view/1,测试示例网站中的链接是否仍然可用。测试结果如图4所示。

图4

从图4中可以看出,网页依然可以加载成功,也就是说该方法是有用的。现在,我们就可以忽略页面别名,只遍历ID来下载所有国家的页面。下面是使用了该技巧的代码片段。

    import itertools
    for page in itertools.count(1):
        url = 'http://example.webscraping.com/view/-%d' % page
        html = download(url)
        if html is None:
            break
        else:
            # success - can scrape the result
            pass

在这段代码中,我们对ID进行遍历,直到出现下载错误时停止,我们假设此时已到达最后一个国家的页面。不过,这种实现方式存在一个缺陷,那就是某些记录可能已被删除,数据库ID之间并不是连续的。此时,只要访问到某个间隔点,爬虫就会立即退出。下面是这段代码的改进版本,在该版本中连续发生多次下载错误后才会退出程序。

    # maximum number of consecutive download errors allowed
    max_errors = 5
    # current number of consecutive download errors
    num_errors = 0
    for page in itertools.count(1):
        url = 'http://example.webscraping.com/view/-%d' % page
        html = download(url)
        if html is None:
            # received an error trying to download this webpage
            num_errors += 1
            if num_errors == max_errors:
                # reached maximum number of
                # consecutive errors so exit
                break
        else:
            # success - can scrape the result
            # ...
            num_errors = 0

上面代码中实现的爬虫需要连续5次下载错误才会停止遍历,这样就很大程度上降低了遇到被删除记录时过早停止遍历的风险。

在爬取网站时,遍历ID是一个很便捷的方法,但是和网站地图爬虫一样,这种方法也无法保证始终可用。比如,一些网站会检查页面别名是否满足预期,如果不是,则会返回404 Not Found错误。而另一些网站则会使用非连续大数作为ID,或是不使用数值作为ID,此时遍历就难以发挥其作用了。例如,Amazon使用ISBN作为图书ID,这种编码包含至少10位数字。使用ID对Amazon的图书进行遍历需要测试数十亿次,因此这种方法肯定不是抓取该站内容最高效的方法。

4.4 链接爬虫

到目前为止,我们已经利用示例网站的结构特点实现了两个简单爬虫,用于下载所有的国家页面。只要这两种技术可用,就应当使用其进行爬取,因为这两种方法最小化了需要下载的网页数量。不过,对于另一些网站,我们需要让爬虫表现得更像普通用户,跟踪链接,访问感兴趣的内容。

通过跟踪所有链接的方式,我们可以很容易地下载整个网站的页面。但是,这种方法会下载大量我们并不需要的网页。例如,我们想要从一个在线论坛中抓取用户账号详情页,那么此时我们只需要下载账号页,而不需要下载讨论贴的页面。本文中的链接爬虫将使用正则表达式来确定需要下载哪些页面。下面是这段代码的初始版本。

    import re

    def link_crawler(seed_url, link_regex):
        """Crawl from the given seed URL following links matched by link_regex
        """
        crawl_queue = [seed_url]
        while crawl_queue:
            url = crawl_queue.pop()
            html = download(url)
            # filter for links matching our regular expression
            for link in get_links(html):
                if re.match(link_regex, link):
                    crawl_queue.append(link)

    def get_links(html):
        """Return a list of links from html
        """
        # a regular expression to extract all links from the webpage
        webpage_regex = re.compile('<a[^>]+href=["\'](.*?)["\']',
            re.IGNORECASE)
        # list of all links from the webpage
        return webpage_regex.findall(html)

要运行这段代码,只需要调用link_crawler函数,并传入两个参数:要爬取的网站URL和用于跟踪链接的正则表达式。对于示例网站,我们想要爬取的是国家列表索引页和国家页面。其中,索引页链接格式如下。

  • http://example.webscraping.com/index/1
  • http://example.webscraping.com/index/2

国家页链接格式如下。

  • http://example.webscraping.com/view/Afghanistan-1
  • http://example.webscraping.com/view/Aland-Islands-2

因此,我们可以用/(index|view)/这个简单的正则表达式来匹配这两类网页。当爬虫使用这些输入参数运行时会发生什么呢?你会发现我们得到了如下的下载错误。

    >>> link_crawler('http://example.webscraping.com',
        '/(index|view)')
    Downloading: http://example.webscraping.com
    Downloading: /index/1
    Traceback (most recent call last):
        ...
    ValueError: unknown url type: /index/1

可以看出,问题出在下载/index/1时,该链接只有网页的路径部分,而没有协议和服务器部分,也就是说这是一个相对链接。由于浏览器知道你正在浏览哪个网页,所以在浏览器浏览时,相对链接是能够正常工作的。但是,urllib2是无法获知上下文的。为了让urllib2能够定位网页,我们需要将链接转换为绝对链接的形式,以便包含定位网页的所有细节。如你所愿,Python中确实有用来实现这一功能的模块,该模块称为urlparse。下面是link_crawler的改进版本,使用了urlparse模块来创建绝对路径。

    import urlparse
    def link_crawler(seed_url, link_regex):
        """Crawl from the given seed URL following links matched by link_regex
        """
        crawl_queue = [seed_url]
        while crawl_queue:
            url = crawl_queue.pop()
            html = download(url)
            for link in get_links(html):
                if re.match(link_regex, link):
                    link = urlparse.urljoin(seed_url, link)
                    crawl_queue.append(link)

当你运行这段代码时,会发现虽然网页下载没有出现错误,但是同样的地点总是会被不断下载到。这是因为这些地点相互之间存在链接。比如,澳大利亚链接到了南极洲,而南极洲也存在到澳大利亚的链接,此时爬虫就会在它们之间不断循环下去。要想避免重复爬取相同的链接,我们需要记录哪些链接已经被爬取过。下面是修改后的link_crawler函数,已具备存储已发现URL的功能,可以避免重复下载。

    def link_crawler(seed_url, link_regex):
        crawl_queue = [seed_url]
        # keep track which URL's have seen before
        seen = set(crawl_queue)
        while crawl_queue:
            url = crawl_queue.pop()
            html = download(url)
            for link in get_links(html):
                # check if link matches expected regex
                if re.match(link_regex, link):
                    # form absolute link
                    link = urlparse.urljoin(seed_url, link)
                    # check if have already seen this link
                    if link not in seen:
                        seen.add(link)
                        crawl_queue.append(link)

当运行该脚本时,它会爬取所有地点,并且能够如期停止。最终,我们得到了一个可用的爬虫!

高级功能

现在,让我们为链接爬虫添加一些功能,使其在爬取其他网站时更加有用。

解析robots.txt

首先,我们需要解析robots.txt文件,以避免下载禁止爬取的URL。使用Python自带的robotparser模块,就可以轻松完成这项工作,如下面的代码所示。

>>> import robotparser
>>> rp = robotparser.RobotFileParser()
>>> rp.set_url('http://example.webscraping.com/robots.txt')
>>> rp.read()
>>> url = 'http://example.webscraping.com'
>>> user_agent = 'BadCrawler'
>>> rp.can_fetch(user_agent, url)
False
>>> user_agent = 'GoodCrawler'
>>> rp.can_fetch(user_agent, url)
True

robotparser模块首先加载robots.txt文件,然后通过can_fetch()函数确定指定的用户代理是否允许访问网页。在本例中,当用户代理设置为 BadCrawler 时,robotparser模块会返回结果表明无法获取网页,这和示例网站robots.txt的定义一样。

为了将该功能集成到爬虫中,我们需要在crawl循环中添加该检查。

    ...
    while crawl_queue:
        url = crawl_queue.pop()
        # check url passes robots.txt restrictions
        if rp.can_fetch(user_agent, url):
            ...
        else:
            print 'Blocked by robots.txt:', url
支持代理

有时我们需要使用代理访问某个网站。比如,Netflix屏蔽了美国以外的大多数国家。使用urllib2支持代理并没有想象中那么容易(可以尝试使用更友好的Python HTTP模块requests来实现该功能,其文档地址为http://docs.python-requests.org/)。下面是使用urllib2支持代理的代码。

    proxy = ...
    opener = urllib2.build_opener()
    proxy_params = {urlparse.urlparse(url).scheme: proxy}
    opener.add_handler(urllib2.ProxyHandler(proxy_params))
    response = opener.open(request)

下面是集成了该功能的新版本download函数。

    def download(url, user_agent='wswp', proxy=None, num_retries=2):
        print 'Downloading:', url
        headers = {'User-agent': user_agent}
        request = urllib2.Request(url, headers=headers)

        opener = urllib2.build_opener()
        if proxy:
            proxy_params = {urlparse.urlparse(url).scheme: proxy}
            opener.add_handler(urllib2.ProxyHandler(proxy_params))
        try:
            html = opener.open(request).read()
        except urllib2.URLError as e:
            print 'Download error:', e.reason
            html = None
            if num_retries > 0:
                if hasattr(e, 'code') and 500 <= e.code < 600:
                # retry 5XX HTTP errors
                html = download(url, user_agent, proxy,
                    num_retries-1)
        return html
下载限速

如果我们爬取网站的速度过快,就会面临被封禁或是造成服务器过载的风险。为了降低这些风险,我们可以在两次下载之间添加延时,从而对爬虫限速。下面是实现了该功能的类的代码。

    class Throttle:
        """Add a delay between downloads to the same domain
        """
        def __init__(self, delay):
            # amount of delay between downloads for each domain
            self.delay = delay
            # timestamp of when a domain was last accessed
            self.domains = {}

        def wait(self, url):
            domain = urlparse.urlparse(url).netloc
            last_accessed = self.domains.get(domain)

            if self.delay > 0 and last_accessed is not None:
                sleep_secs = self.delay - (datetime.datetime.now() -
                    last_accessed).seconds
                if sleep_secs > 0:
                    # domain has been accessed recently
                    # so need to sleep
                    time.sleep(sleep_secs)
            # update the last accessed time
            self.domains[domain] = datetime.datetime.now()

Throttle类记录了每个域名上次访问的时间,如果当前时间距离上次访问时间小于指定延时,则执行睡眠操作。我们可以在每次下载之前调用Throttle对爬虫进行限速。

    throttle = Throttle(delay)
    ...
    throttle.wait(url)
    result = download(url, headers, proxy=proxy,
        num_retries=num_retries)
避免爬虫陷阱

目前,我们的爬虫会跟踪所有之前没有访问过的链接。但是,一些网站会动态生成页面内容,这样就会出现无限多的网页。比如,网站有一个在线日历功能,提供了可以访问下个月和下一年的链接,那么下个月的页面中同样会包含访问再下个月的链接,这样页面就会无止境地链接下去。这种情况被称为爬虫陷阱

想要避免陷入爬虫陷阱,一个简单的方法是记录到达当前网页经过了多少个链接,也就是深度。当到达最大深度时,爬虫就不再向队列中添加该网页中的链接了。要实现这一功能,我们需要修改seen变量。该变量原先只记录访问过的网页链接,现在修改为一个字典,增加了页面深度的记录。

    def link_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)

现在有了这一功能,我们就有信心爬虫最终一定能够完成。如果想要禁用该功能,只需将max_depth设为一个负数即可,此时当前深度永远不会与之相等。

最终版本

这个高级链接爬虫的完整源代码可以在https://bitbucket.org/ wswp/code/src/tip/chapter01/link_crawler3.py下载得到。要测试这段代码,我们可以将用户代理设置为BadCrawler,也就是本章前文所述的被robots.txt屏蔽了的那个用户代理。从下面的运行结果中可以看出,爬虫果然被屏蔽了,代码启动后马上就会结束。

>>> seed_url = 'http://example.webscraping.com/index'
>>> link_regex = '/(index|view)'
>>> link_crawler(seed_url, link_regex, user_agent='BadCrawler')
Blocked by robots.txt: http://example.webscraping.com/

现在,让我们使用默认的用户代理,并将最大深度设置为1,这样只有主页上的链接才会被下载。

>>> link_crawler(seed_url, link_regex, max_depth=1)
Downloading: http://example.webscraping.com//index
Downloading: http://example.webscraping.com/index/1
Downloading: http://example.webscraping.com/view/Antigua-and-Barbuda-10
Downloading: http://example.webscraping.com/view/Antarctica-9
Downloading: http://example.webscraping.com/view/Anguilla-8
Downloading: http://example.webscraping.com/view/Angola-7
Downloading: http://example.webscraping.com/view/Andorra-6
Downloading: http://example.webscraping.com/view/American-Samoa-5
Downloading: http://example.webscraping.com/view/Algeria-4
Downloading: http://example.webscraping.com/view/Albania-3
Downloading: http://example.webscraping.com/view/Aland-Islands-2
Downloading: http://example.webscraping.com/view/Afghanistan-1

和预期一样,爬虫在下载完国家列表的第一页之后就停止了。

本文节选自《用Python写网络爬虫》

本书讲解了如何使用Python来编写网络爬虫程序,内容包括网络爬虫简介,从页面中抓取数据的三种方法,提取缓存中的数据,使用多个线程和进程来进行并发抓取,如何抓取动态页面中的内容,与表单进行交互,处理页面中的验证码问题,以及使用Scarpy和Portia来进行数据抓取,并在最后使用本书介绍的数据抓取技术对几个真实的网站进行了抓取,旨在帮助读者活学活用书中介绍的技术。

本书适合有一定Python编程经验,而且对爬虫技术感兴趣的读者阅读。

读:本文主要分为两个部分:一部分是网络爬虫的概述,帮助大家详细了解网络爬虫;另一部分是HTTP请求的Python实现,帮助大家了解Python中实现HTTP请求的各种方式,以便具备编写HTTP网络程序的能力。

作者:范传辉

如需转载请联系华章科技

01 网络爬虫概述

接下来从网络爬虫的概念、用处与价值和结构等三个方面,让大家对网络爬虫有一个基本的了解。

1. 网络爬虫及其应用

随着网络的迅速发展,万维网成为大量信息的载体,如何有效地提取并利用这些信息成为一个巨大的挑战,网络爬虫应运而生。网络爬虫(又被称为网页蜘蛛、网络机器人),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。下面通过图3-1展示一下网络爬虫在互联网中起到的作用:

▲图3-1 网络爬虫

网络爬虫按照系统结构和实现技术,大致可以分为以下几种类型:通用网络爬虫、聚焦网络爬虫、增量式网络爬虫、深层网络爬虫。实际的网络爬虫系统通常是几种爬虫技术相结合实现的。

搜索引擎(Search Engine),例如传统的通用搜索引擎baidu、Yahoo和Google等,是一种大型复杂的网络爬虫,属于通用性网络爬虫的范畴。但是通用性搜索引擎存在着一定的局限性:

  1. 不同领域、不同背景的用户往往具有不同的检索目的和需求,通用搜索引擎所返回的结果包含大量用户不关心的网页。
  2. 通用搜索引擎的目标是尽可能大的网络覆盖率,有限的搜索引擎服务器资源与无限的网络数据资源之间的矛盾将进一步加深。
  3. 万维网数据形式的丰富和网络技术的不断发展,图片、数据库、音频、视频多媒体等不同数据大量出现,通用搜索引擎往往对这些信息含量密集且具有一定结构的数据无能为力,不能很好地发现和获取。
  4. 通用搜索引擎大多提供基于关键字的检索,难以支持根据语义信息提出的查询。

为了解决上述问题,定向抓取相关网页资源的聚焦爬虫应运而生。

聚焦爬虫是一个自动下载网页的程序,它根据既定的抓取目标,有选择地访问万维网上的网页与相关的链接,获取所需要的信息。与通用爬虫不同,聚焦爬虫并不追求大的覆盖,而将目标定为抓取与某一特定主题内容相关的网页,为面向主题的用户查询准备数据资源。

说完了聚焦爬虫,接下来再说一下增量式网络爬虫。增量式网络爬虫是指对已下载网页采取增量式更新和只爬行新产生的或者已经发生变化网页的爬虫,它能够在一定程度上保证所爬行的页面是尽可能新的页面。

和周期性爬行和刷新页面的网络爬虫相比,增量式爬虫只会在需要的时候爬行新产生或发生更新的页面,并不重新下载没有发生变化的页面,可有效减少数据下载量,及时更新已爬行的网页,减小时间和空间上的耗费,但是增加了爬行算法的复杂度和实现难度。

例如:想获取赶集网的招聘信息,以前爬取过的数据没有必要重复爬取,只需要获取更新的招聘数据,这时候就要用到增量式爬虫。

最后说一下深层网络爬虫。Web页面按存在方式可以分为表层网页和深层网页。表层网页是指传统搜索引擎可以索引的页面,以超链接可以到达的静态网页为主构成的Web页面。深层网络是那些大部分内容不能通过静态链接获取的、隐藏在搜索表单后的,只有用户提交一些关键词才能获得的Web页面。

例如用户登录或者注册才能访问的页面。可以想象这样一个场景:爬取贴吧或者论坛中的数据,必须在用户登录后,有权限的情况下才能获取完整的数据。

2. 网络爬虫结构

下面用一个通用的网络爬虫结构来说明网络爬虫的基本工作流程,如图3-4所示。

▲图3-4 网络爬虫结构

网络爬虫的基本工作流程如下:

  1. 首先选取一部分精心挑选的种子URL。
  2. 将这些URL放入待抓取URL队列。
  3. 从待抓取URL队列中读取待抓取队列的URL,解析DNS,并且得到主机的IP,并将URL对应的网页下载下来,存储进已下载网页库中。此外,将这些URL放进已抓取URL队列。
  4. 分析已抓取URL队列中的URL,从已下载的网页数据中分析出其他URL,并和已抓取的URL进行比较去重,最后将去重过的URL放入待抓取URL队列,从而进入下一个循环。

02 HTTP请求的Python实现

通过上面的网络爬虫结构,我们可以看到读取URL、下载网页是每一个爬虫必备而且关键的功能,这就需要和HTTP请求打交道。接下来讲解Python中实现HTTP请求的三种方式:urllib2/urllib、httplib/urllib以及Requests。

1. urllib2/urllib实现

urllib2和urllib是Python中的两个内置模块,要实现HTTP功能,实现方式是以urllib2为主,urllib为辅。

1.1 首先实现一个完整的请求与响应模型

urllib2提供一个基础函数urlopen,通过向指定的URL发出请求来获取数据。最简单的形式是:

import urllib2
response=urllib2.urlopen('http://www.zhihu.com')
html=response.read()
print html

其实可以将上面对http://www.zhihu.com的请求响应分为两步,一步是请求,一步是响应,形式如下:

import urllib2
# 请求
request=urllib2.Request('http://www.zhihu.com')
# 响应
response = urllib2.urlopen(request)
html=response.read()
print html

上面这两种形式都是GET请求,接下来演示一下POST请求,其实大同小异,只是增加了请求数据,这时候用到了urllib。示例如下:

import urllib
import urllib2
url = 'http://www.xxxxxx.com/login'
postdata = {'username' : 'qiye',
 'password' : 'qiye_pass'}
# info 需要被编码为urllib2能理解的格式,这里用到的是urllib
data = urllib.urlencode(postdata)
req = urllib2.Request(url, data)
response = urllib2.urlopen(req)
html = response.read()

但是有时会出现这种情况:即使POST请求的数据是对的,但是服务器拒绝你的访问。这是为什么呢?问题出在请求中的头信息,服务器会检验请求头,来判断是否是来自浏览器的访问,这也是反爬虫的常用手段。

1.2 请求头headers处理

将上面的例子改写一下,加上请求头信息,设置一下请求头中的User-Agent域和Referer域信息。

import urllib
import urllib2
url = 'http://www.xxxxxx.com/login'
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
referer='http://www.xxxxxx.com/'
postdata = {'username' : 'qiye',
 'password' : 'qiye_pass'}
# 将user_agent,referer写入头信息
headers={'User-Agent':user_agent,'Referer':referer}
data = urllib.urlencode(postdata)
req = urllib2.Request(url, data,headers)
response = urllib2.urlopen(req)
html = response.read()

也可以这样写,使用add_header来添加请求头信息,修改如下:

import urllib
import urllib2
url = 'http://www.xxxxxx.com/login'
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
referer='http://www.xxxxxx.com/'
postdata = {'username' : 'qiye',
 'password' : 'qiye_pass'}
data = urllib.urlencode(postdata)
req = urllib2.Request(url)
# 将user_agent,referer写入头信息
req.add_header('User-Agent',user_agent)
req.add_header('Referer',referer)
req.add_data(data)
response = urllib2.urlopen(req)
html = response.read()

对有些header要特别留意,服务器会针对这些header做检查,例如:

  • User-Agent:有些服务器或Proxy会通过该值来判断是否是浏览器发出的请求。
  • Content-Type:在使用REST接口时,服务器会检查该值,用来确定HTTP Body中的内容该怎样解析。在使用服务器提供的RESTful或SOAP服务时,Content-Type设置错误会导致服务器拒绝服务。常见的取值有:application/xml(在XML RPC,如RESTful/SOAP调用时使用)、application/json(在JSON RPC调用时使用)、application/x-www-form-urlencoded(浏览器提交Web表单时使用)。
  • Referer:服务器有时候会检查防盗链。

1.3 Cookie处理

urllib2对Cookie的处理也是自动的,使用CookieJar函数进行Cookie的管理。如果需要得到某个Cookie项的值,可以这么做:

import urllib2
import cookielib
cookie = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
response = opener.open('http://www.zhihu.com')
for item in cookie:
 print item.name+':'+item.value

但是有时候会遇到这种情况,我们不想让urllib2自动处理,我们想自己添加Cookie的内容,可以通过设置请求头中的Cookie域来做:

import urllib2
opener = urllib2.build_opener()
opener.addheaders.append( ( 'Cookie', 'email=' + "xxxxxxx@163.com" ) )
req = urllib2.Request( "http://www.zhihu.com/" )
response = opener.open(req)
print response.headers
retdata = response.read()

1.4 Timeout设置超时

在Python2.6之前的版本,urllib2的API并没有暴露Timeout的设置,要设置Timeout值,只能更改Socket的全局Timeout值。示例如下:

import urllib2
import socket
socket.setdefaulttimeout(10) # 10 秒钟后超时
urllib2.socket.setdefaulttimeout(10) # 另一种方式

在Python2.6及新的版本中,urlopen函数提供了对Timeout的设置,示例如下:

import urllib2
request=urllib2.Request('http://www.zhihu.com')
response = urllib2.urlopen(request,timeout=2)
html=response.read()
print html

1.5 获取HTTP响应码

对于200 OK来说,只要使用urlopen返回的response对象的getcode()方法就可以得到HTTP的返回码。但对其他返回码来说,urlopen会抛出异常。这时候,就要检查异常对象的code属性了,示例如下:

import urllib2
try:
 response = urllib2.urlopen('http://www.google.com')
 print response
except urllib2.HTTPError as e:
 if hasattr(e, 'code'):
 print 'Error code:',e.code

1.6 重定向

urllib2默认情况下会针对HTTP 3XX返回码自动进行重定向动作。要检测是否发生了重定向动作,只要检查一下Response的URL和Request的URL是否一致就可以了,示例如下:

import urllib2
response = urllib2.urlopen('http://www.zhihu.cn')
isRedirected = response.geturl() == 'http://www.zhihu.cn'

如果不想自动重定向,可以自定义HTTPRedirectHandler类,示例如下:

import urllib2
class RedirectHandler(urllib2.HTTPRedirectHandler):
 def http_error_301(self, req, fp, code, msg, headers):
 pass
 def http_error_302(self, req, fp, code, msg, headers):
 result = urllib2.HTTPRedirectHandler.http_error_301(self, req, fp, code, 
 msg, headers)
 result.status = code
 result.newurl = result.geturl()
 return result
opener = urllib2.build_opener(RedirectHandler)
opener.open('http://www.zhihu.cn')

1.7 Proxy的设置

在做爬虫开发中,必不可少地会用到代理。urllib2默认会使用环境变量http_proxy来设置HTTP Proxy。但是我们一般不采用这种方式,而是使用ProxyHandler在程序中动态设置代理,示例代码如下:

import urllib2
proxy = urllib2.ProxyHandler({'http': '127.0.0.1:8087'})
opener = urllib2.build_opener([proxy,])
urllib2.install_opener(opener)
response = urllib2.urlopen('http://www.zhihu.com/')
print response.read()

这里要注意的一个细节,使用urllib2.install_opener()会设置urllib2的全局opener,之后所有的HTTP访问都会使用这个代理。这样使用会很方便,但不能做更细粒度的控制,比如想在程序中使用两个不同的Proxy设置,这种场景在爬虫中很常见。比较好的做法是不使用install_opener去更改全局的设置,而只是直接调用opener的open方法代替全局的urlopen方法,修改如下:

import urllib2
proxy = urllib2.ProxyHandler({'http': '127.0.0.1:8087'})
opener = urllib2.build_opener(proxy,)
response = opener.open("http://www.zhihu.com/")
print response.read()

2. httplib/urllib实现

httplib模块是一个底层基础模块,可以看到建立HTTP请求的每一步,但是实现的功能比较少,正常情况下比较少用到。在Python爬虫开发中基本上用不到,所以在此只是进行一下知识普及。下面介绍一下常用的对象和函数:

  • 创建HTTPConnection对象:
  • class httplib.HTTPConnection(host[, port[, strict[, timeout[, source_address]]]])。
  • 发送请求:
  • HTTPConnection.request(method, url[, body[, headers]])。
  • 获得响应:
  • HTTPConnection.getresponse()。
  • 读取响应信息:
  • HTTPResponse.read([amt])。
  • 获得指定头信息:
  • HTTPResponse.getheader(name[, default])。
  • 获得响应头(header, value)元组的列表:
  • HTTPResponse.getheaders()。
  • 获得底层socket文件描述符:
  • HTTPResponse.fileno()。
  • 获得头内容:
  • HTTPResponse.msg。
  • 获得头http版本:
  • HTTPResponse.version。
  • 获得返回状态码:
  • HTTPResponse.status。
  • 获得返回说明:
  • HTTPResponse.reason。

接下来演示一下GET请求和POST请求的发送,首先是GET请求的示例,如下所示:

import httplib
conn =None
try:
 conn = httplib.HTTPConnection("www.zhihu.com")
 conn.request("GET", "/")
 response = conn.getresponse()
 print response.status, response.reason
 print '-' * 40
 headers = response.getheaders()
 for h in headers:
 print h
 print '-' * 40
 print response.msg
except Exception,e:
 print e
finally:
 if conn:
 conn.close()

POST请求的示例如下:

import httplib, urllib
conn = None
try:
 params = urllib.urlencode({'name': 'qiye', 'age': 22})
 headers = {"Content-type": "application/x-www-form-urlencoded"
 , "Accept": "text/plain"}
 conn = httplib.HTTPConnection("www.zhihu.com", 80, timeout=3)
 conn.request("POST", "/login", params, headers)
 response = conn.getresponse()
 print response.getheaders() # 获取头信息
 print response.status
 print response.read()
except Exception, e:
 print e
 finally:
 if conn:
 conn.close()

3. 更人性化的Requests

Python中Requests实现HTTP请求的方式,是本人极力推荐的,也是在Python爬虫开发中最为常用的方式。Requests实现HTTP请求非常简单,操作更加人性化。

Requests库是第三方模块,需要额外进行安装。Requests是一个开源库,源码位于:

GitHub: https://github.com/kennethreitz/requests

希望大家多多支持作者。

使用Requests库需要先进行安装,一般有两种安装方式:

  • 使用pip进行安装,安装命令为:pip install requests,不过可能不是最新版。
  • 直接到GitHub上下载Requests的源代码,下载链接为:
  • https://github.com/kennethreitz/requests/releases
  • 将源代码压缩包进行解压,然后进入解压后的文件夹,运行setup.py文件即可。

如何验证Requests模块安装是否成功呢?在Python的shell中输入import requests,如果不报错,则是安装成功。如图3-5所示。

▲图3-5 验证Requests安装

3.1 首先还是实现一个完整的请求与响应模型

以GET请求为例,最简单的形式如下:

import requests
r = requests.get('http://www.baidu.com')
print r.content

大家可以看到比urllib2实现方式的代码量少。接下来演示一下POST请求,同样是非常简短,更加具有Python风格。示例如下:

import requests
postdata={'key':'value'}
r = requests.post('http://www.xxxxxx.com/login',data=postdata)
print r.content

HTTP中的其他请求方式也可以用Requests来实现,示例如下:

r = requests.put('http://www.xxxxxx.com/put', data = {'key':'value'})
r = requests.delete('http://www.xxxxxx.com/delete')
r = requests.head('http://www.xxxxxx.com/get')
r = requests.options('http://www.xxxxxx.com/get')

接着讲解一下稍微复杂的方式,大家肯定见过类似这样的URL:

http://zzk.cnblogs.com/s/blogpost?Keywords=blog:qiyeboy&pageindex=1

就是在网址后面紧跟着“?”,“?”后面还有参数。那么这样的GET请求该如何发送呢?肯定有人会说,直接将完整的URL带入即可,不过Requests还提供了其他方式,示例如下:

import requests
 payload = {'Keywords': 'blog:qiyeboy','pageindex':1}
r = requests.get('http://zzk.cnblogs.com/s/blogpost', params=payload)
print r.url

通过打印结果,我们看到最终的URL变成了:

http://zzk.cnblogs.com/s/blogpost?Keywords=blog:qiyeboy&pageindex=1

3.2 响应与编码

还是从代码入手,示例如下:

import requests
r = requests.get('http://www.baidu.com')
print 'content-->'+r.content
print 'text-->'+r.text
print 'encoding-->'+r.encoding
r.encoding='utf-8'
print 'new text-->'+r.text

其中r.content返回的是字节形式,r.text返回的是文本形式,r.encoding返回的是根据HTTP头猜测的网页编码格式。

输出结果中:“text-->”之后的内容在控制台看到的是乱码,“encoding-->”之后的内容是ISO-8859-1(实际上的编码格式是UTF-8),由于Requests猜测编码错误,导致解析文本出现了乱码。Requests提供了解决方案,可以自行设置编码格式,r.encoding='utf-8'设置成UTF-8之后,“new text-->”的内容就不会出现乱码。

但是这种手动的方式略显笨拙,下面提供一种更加简便的方式:chardet,这是一个非常优秀的字符串/文件编码检测模块。安装方式如下:

pip install chardet

安装完成后,使用chardet.detect()返回字典,其中confidence是检测精确度,encoding是编码形式。示例如下:

import requests
r = requests.get('http://www.baidu.com')
print chardet.detect(r.content)
r.encoding = chardet.detect(r.content)['encoding']
print r.text

直接将chardet探测到的编码,赋给r.encoding实现解码,r.text输出就不会有乱码了。

除了上面那种直接获取全部响应的方式,还有一种流模式,示例如下:

import requests
r = requests.get('http://www.baidu.com',stream=True)
print r.raw.read(10)

设置stream=True标志位,使响应以字节流方式进行读取,r.raw.read函数指定读取的字节数。

3.3 请求头headers处理

Requests对headers的处理和urllib2非常相似,在Requests的get函数中添加headers参数即可。示例如下:

import requests
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers={'User-Agent':user_agent}
r = requests.get('http://www.baidu.com',headers=headers)
print r.content

3.4 响应码code和响应头headers处理

获取响应码是使用Requests中的status_code字段,获取响应头使用Requests中的headers字段。示例如下:

import requests
r = requests.get('http://www.baidu.com')
if r.status_code == requests.codes.ok:
 print r.status_code# 响应码
 print r.headers# 响应头
 print r.headers.get('content-type')# 推荐使用这种获取方式,获取其中的某个字段
 print r.headers['content-type']# 不推荐使用这种获取方式
else:
 r.raise_for_status()

上述程序中,r.headers包含所有的响应头信息,可以通过get函数获取其中的某一个字段,也可以通过字典引用的方式获取字典值,但是不推荐,因为如果字段中没有这个字段,第二种方式会抛出异常,第一种方式会返回None。

r.raise_for_status()是用来主动地产生一个异常,当响应码是4XX或5XX时,raise_for_status()函数会抛出异常,而响应码为200时,raise_for_status()函数返回None。

3.5 Cookie处理

如果响应中包含Cookie的值,可以如下方式获取Cookie字段的值,示例如下:

import requests
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers={'User-Agent':user_agent}
r = requests.get('http://www.baidu.com',headers=headers)
# 遍历出所有的cookie字段的值
for cookie in r.cookies.keys():
 print cookie+':'+r.cookies.get(cookie)

如果想自定义Cookie值发送出去,可以使用以下方式,示例如下:

import requests
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers={'User-Agent':user_agent}
cookies = dict(name='qiye',age='10')
r = requests.get('http://www.baidu.com',headers=headers,cookies=cookies)
print r.text

还有一种更加高级,且能自动处理Cookie的方式,有时候我们不需要关心Cookie值是多少,只是希望每次访问的时候,程序自动把Cookie的值带上,像浏览器一样。Requests提供了一个session的概念,在连续访问网页,处理登录跳转时特别方便,不需要关注具体细节。使用方法示例如下:

import Requests
oginUrl = 'http://www.xxxxxxx.com/login'
s = requests.Session()
#首先访问登录界面,作为游客,服务器会先分配一个cookie
r = s.get(loginUrl,allow_redirects=True)
datas={'name':'qiye','passwd':'qiye'}
#向登录链接发送post请求,验证成功,游客权限转为会员权限
r = s.post(loginUrl, data=datas,allow_redirects= True)
print r.text

上面的这段程序,其实是正式做Python开发中遇到的问题,如果没有第一步访问登录的页面,而是直接向登录链接发送Post请求,系统会把你当做非法用户,因为访问登录界面时会分配一个Cookie,需要将这个Cookie在发送Post请求时带上,这种使用Session函数处理Cookie的方式之后会很常用。

3.6 重定向与历史信息

处理重定向只是需要设置一下allow_redirects字段即可,例如:

r=requests.get('http://www.baidu.com',allow_redirects=True)

将allow_redirects设置为True,则是允许重定向;设置为False,则是禁止重定向。如果是允许重定向,可以通过r.history字段查看历史信息,即访问成功之前的所有请求跳转信息。示例如下:

import requests
r = requests.get('http://github.com')
print r.url
print r.status_code
print r.history

打印结果如下:

https://github.com/
200
(<Response [301]>,)

上面的示例代码显示的效果是访问GitHub网址时,会将所有的HTTP请求全部重定向为HTTPS。

3.7 超时设置

超时选项是通过参数timeout来进行设置的,示例如下:

requests.get('http://github.com', timeout=2)

3.8 代理设置

使用代理Proxy,你可以为任意请求方法通过设置proxies参数来配置单个请求:

import requests
proxies = {
 "http": "http://0.10.1.10:3128",
 "https": "http://10.10.1.10:1080",
}
requests.get("http://example.org", proxies=proxies)

也可以通过环境变量HTTP_PROXY和HTTPS_PROXY?来配置代理,但是在爬虫开发中不常用。你的代理需要使用HTTP Basic Auth,可以使用http://user:password@host/语法:

proxies = {
 "http": "http://user:pass@10.10.1.10:3128/",
}

03 小结

本文主要讲解了网络爬虫的结构和应用,以及Python实现HTTP请求的几种方法。希望大家对本文中的网络爬虫工作流程和Requests实现HTTP请求的方式重点吸收消化。

关于作者:范传辉,资深网虫,Python开发者,参与开发了多项网络应用,在实际开发中积累了丰富的实战经验,并善于总结,贡献了多篇技术文章广受好评。研究兴趣是网络安全、爬虫技术、数据分析、驱动开发等技术。

本文摘编自《Python爬虫开发与项目实战》,经出版方授权发布。

延伸阅读《Python爬虫开发与项目实战》

推荐语:零基础学习爬虫技术,从Python和Web前端基础开始讲起,由浅入深,包含大量案例,实用性强。

多程序员新人想了解爬虫的实现,然而,网络上的许多信息是教大家如何爬虫工具。工具的使用对于快速完成页面爬取工作是有帮助的,但不利于大家掌握爬虫的原理

本文将带大家从最基本和最本质的途径去编写一个爬虫,让大家真正了解爬虫的工作原理。并且,能够在此基础上根据自身需求改造出需要的爬虫。

网页的组成


在开始爬虫工作之前,我们先了解下,什么是网页。

以某网页为例,其展示效果如下:、

我们在网页上点击右键,“查看源代码”可以看到网页的代码信息,如下:

其中有很多文本,也有很多链接。这些链接有的指向另一个页面,有的指向css文件、js文件、图片文件等。

分类一下,网页一共包含以下几个部分,如图所示。

各个部分的含义如下:

  • 其中HTML是网页的主要部分,存储了网页的主要内容
  • CSS则是对网页中的内容进行修饰
  • Js则是对网页增加一些动态的功能
  • 图片、视频则是一些辅助的材料

爬虫就是主要爬取HTML部分。因为HTML中包含了主要的信息。

爬虫的基本原理


而爬虫的一个重要特点就是顺藤摸瓜——根据链接,从一个网页跳转到另一个网页,不断进行。从而获取众多网页的信息。

那实现爬虫,要完成的基本功能是:

  • 爬取(下载)某个网页
  • 查找当前网页中的链接,继续爬取

基于此,我们可以写出一个爬虫的核心伪代码。

爬虫(网页地址)
  爬取某个页面
  分析页面中的链接
  使用分析得到的链接再次调用方法   爬虫(网页地址)

对的,你没看错,就是这么简单。

其中有两个功能需要实现,即爬取某个页面、分析页面中的链接。

接下来我们分别介绍这两个功能的实现。


核心功能的实现

1 爬取某个页面

这个功能使用Python实现起来比较简单,只要打开一个文本,然后将网络某地址的信息写入文本就算是爬取完成了。

实现代码如下。

htmlFile=open('./output/'+(str(pageId)+'.txt'),'w')
htmlFile.write(urllib.urlopen(url).read())
htmlFile.close()

2 分析页面中的链接

该工作需要正则表达式的帮助,'href="[^(javascript)]\S*[^(#)(css)(js)(ico)]\"'可以帮助我们匹配出网页中的链接。

pattern=re.compile('href="[^(javascript)]\S*[^(#)(css)(js)(ico)]\"')
htmlFile=open('./output/'+(str(pageId)+'.txt'),'r')
        for line in htmlFile:
            ans=re.findall(pattern,line)


爬虫的实现


有了伪代码和两个核心功能的实现代码后,我们可以直接写出爬虫的主要代码:

htmlFile=open('./output/'+(str(pageId)+'.txt'),'w')
htmlFile.write(urllib.urlopen(url).read())
htmlFile.close()

htmlFile=open('./output/'+(str(pageId)+'.txt'),'r')
for line in htmlFile:
    ans=re.findall(pattern,line)
    for one in ans :
        urlTail=one.split('"')[1]
        url=urlparse.urljoin(url,urlTail)
        if urlMap.has_key(url):
            print 'skip---'+url
        else:
            print 'download---'+url
            pageId += 1
            urlMap[url]=pageId
            idMap[pageId]=url
            catchFile=open('./output/'+(str(urlMap[url])+'.txt'),'w')
            try:
                catchFile.write(urllib.urlopen(url).read())
            except:
                pass
            finally:
                catchFile.close()
htmlFile.close()

即爬取某个页面、分析页面中的链接、继续下载。

当然,在这个过程中,有几点要注意:

1、遇到死链要跳过,不要一直卡在那里

2、凡是下载过的页面不要重复下载,否则可能形成环路,永无止境

只要注意了以上两点,就可以写出爬虫。

我直接给出爬虫的代码,放在下面的开源地址上。

https://github.com/yeecode/EasyCrawler


现有功能与展望

该爬虫十分基础、简单、容易理解,就是上面伪代码的直接实现。

该爬虫的基本功能如下:

  • 输入一个入口地址后,会爬取该地址网页中`href=`指向的页面,并将内容下载下来,依次保存
  • 对于不能访问的坏链接,将会忽略
  • 该爬虫只能爬取入口地址的链接,不再向更深处爬取
  • 会自动给页面编ID,并跳过已爬取的页面

整个示例极少依赖外部项目,十分简单、易懂、纯粹。因此该项目不仅便于学习,也便于在此基础上扩展新功能

基于以上功能,我们可以修改实现众多其他功能,包括但不限于:

  • 根据页面不断爬取,而不是只爬取一层链接
  • 设置爬取范围,例如只爬取某个域名下的链接
  • 定时爬取某个地址的数据,并对比其变化
  • 只爬取网页中的图片信息
  • 等等……

好了,希望能够帮到题主。也希望大家能在此基础上实现更酷炫的功能。



往期精彩文章:

  • 高效日志系统搭建秘技!架构师必读
  • 程序员最有成就感的那一刻是什么时候?
  • 远程过程调用RPC的实现原理:动态代理


欢迎关注我们,不错过软件架构编程方面的干货知识。