大规模异步新闻爬虫:实现一个更好的网络请求函数

Python爬虫 2018-12-02 13:57:09 阅读(18266) 评论(20)

上一节我们实现了一个简单的再也不能简单的新闻爬虫,这个爬虫有很多槽点,估计小猿们也会鄙视这个爬虫。上一节最后我们讨论了这些槽点,现在我们就来去除这些槽点来完善我们的新闻爬虫。

问题我们前面已经描述清楚,解决的方法也有了,那就废话不多讲,代码立刻上(Talk is cheap, show me the code!)。

写网络爬虫要注意处理网络异常

downloader 的实现

import requests
import cchardet
import traceback


def downloader(url, timeout=10, headers=None, debug=False, binary=False):
    _headers = {
        'User-Agent': ('Mozilla/5.0 (compatible; MSIE 9.0; '
                       'Windows NT 6.1; Win64; x64; Trident/5.0)'),
    }
    redirected_url = url
    if headers:
        _headers = headers
    try:
        r = requests.get(url, headers=_headers, timeout=timeout)
        if binary:
            html = r.content
        else:
            encoding = cchardet.detect(r.content)['encoding']
            html = r.content.decode(encoding)
        status = r.status_code
        redirected_url = r.url
    except:
        if debug:
            traceback.print_exc()
        msg = 'failed download: {}'.format(url)
        print(msg)
        if binary:
            html = b''
        else:
            html = ''
        status = 0
    return status, html, redirected_url


if __name__ == '__main__':
    url = 'http://news.baidu.com/'
    s, html,lost_url_found_by_大大派 = downloader(url)
    print(s, len(html),lost_url_found_by_大大派)

这个downloader()函数,内置了默认的User-Agent模拟成一个IE9浏览器,同时接受调用者自定义的headers和timeout。使用cchardet来处理编码问题,返回数据包括:

  • 状态码:如果出现异常,设置为0
  • 内容: 默认返回str内容。但是URL链接的是图片等二进制内容时,注意调用时要设binary=True
  • 重定向URL: 有些URL会被重定向,最终页面的url包含在响应对象里面

新闻URL的清洗

我们先看看这两个新闻网址:

http://xinwen.eastday.com/a/n181106070849091.html?qid=news.baidu.com

http://news.ifeng.com/a/20181106/60146589_0.shtml?_zbs_baidu_news

上面两个带?的网站来自百度新闻的首页,这个问号?的作用就是告诉目标服务器,这个网址是从百度新闻链接过来的,是百度带过来的流量。但是它们的表示方式不完全一样,一个是qid=news.baidu.com, 一个是_zbs_baidu_news。这有可能是目标服务器要求的格式不同导致的,这个在目标服务器的后台的浏览统计程序中可能用得到。

然后去掉问号?及其后面的字符,发现它们和不去掉指向的是相同的新闻网页。

从字符串对比上看,有问号和没问号是两个不同的网址,但是它们又指向完全相同的新闻网页,说明问号后面的参数对响应内容没有任何影响。

正在抓取新闻的大量实践后,我们发现了这样的规律:

新闻类网址都做了大量SEO,它们把新闻网址都静态化了,基本上都是以.html, .htm, .shtml等结尾,后面再加任何请求参数都无济于事。

但是,还是会有些新闻网站以参数id的形式动态获取新闻网页。

那么我们抓取新闻时,就要利用这个规律,防止重复抓取。由此,我们实现一个清洗网址的函数。

g_bin_postfix = set([
    'exe', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
    'pdf',
    'jpg', 'png', 'bmp', 'jpeg', 'gif',
    'zip', 'rar', 'tar', 'bz2', '7z', 'gz',
    'flv', 'mp4', 'avi', 'wmv', 'mkv',
    'apk',
])

g_news_postfix = [
    '.html?', '.htm?', '.shtml?',
    '.shtm?',
]


def clean_url(url):
    # 1. 是否为合法的http url
    if not url.startswith('http'):
        return ''
    # 2. 去掉静态化url后面的参数
    for np in g_news_postfix:
        p = url.find(np)
        if p > -1:
            p = url.find('?')
            url = url[:p]
            return url
    # 3. 不下载二进制类内容的链接
    up = urlparse.urlparse(url)
    path = up.path
    if not path:
        path = '/'
    postfix = path.split('.')[-1].lower()
    if postfix in g_bin_postfix:
        return ''

    # 4. 去掉标识流量来源的参数
    # badquery = ['spm', 'utm_source', 'utm_source', 'utm_medium', 'utm_campaign']
    good_queries = []
    for query in up.query.split('&'):
        qv = query.split('=')
        if qv[0].startswith('spm') or qv[0].startswith('utm_'):
            continue
        if len(qv) == 1:
            continue
        good_queries.append(query)
    query = '&'.join(good_queries)
    url = urlparse.urlunparse((
        up.scheme,
        up.netloc,
        path,
        up.params,
        query,
        ''  #  crawler do not care fragment
    ))
    return url

清洗url的方法都在代码的注释里面了,这里面包含了两类操作:

  • 判断是否合法url,非法的直接返回空字符串
  • 去掉不必要的参数,去掉静态化url的参数

网络爬虫知识点

1. URL清洗
网络请求开始之前,先把url清洗一遍,可以避免重复下载、无效下载(二进制内容),节省服务器和网络开销。

2. cchardet 模块
该模块是chardet的升级版,功能和chardet完全一样,用来检测一个字符串的编码。由于是用C和C++实现的,所以它的速度非常快,非常适合在爬虫中用来判断网页的编码。
切记,不要相信requests返回的encoding,自己判断一下更放心。上一节,我们已经列举了一个例子来证明requests对编码识别的错误,如果忘了的话,可以再去回顾一下。

3. traceback 模块
我们写的爬虫在运行过程中,会出现各种异常,而且有些异常是不可预期的,也不知道它会出现在什么地方,我们就需要用try来捕获异常让程序不中断,但是我们又需要看看捕获的异常是什么内容,由此来改善我们的爬虫。这个时候,就需要traceback模块。
比如在downloader()函数里面我们用try捕获了get()的异常,但是,异常也有可能是cchardet.detect()引起的,用traceback.print_exc()来输出异常,有助于我们发现更多问题。

本篇我们讲了编写一个更好的网络请求函数,下一篇我们讲:
编写一个好用的URL Pool

猿人学banner宣传图

我的公众号:猿人学 Python 上会分享更多心得体会,敬请关注。

***版权申明:若没有特殊说明,文章皆是猿人学 yuanrenxue.con 原创,没有猿人学授权,请勿以任何形式转载。***

说点什么吧...

  1. 1楼
    匿名 5年前 (2018-12-26)

    这篇跳跃性过大,前面那章实现功能的都能看懂,现在蒙了

    • 回复
      veelion 5年前 (2018-12-26)
      回复 @ :这篇主要是讲把http请求封装为一个downloader()函数,写爬虫时直接调用这个函数就方便多了。
      • 匿名 5年前 (2019-01-03)
        回复 @veelion :谢谢,我再研究下,基础不是很好。
  2. 2楼
    匿名 5年前 (2019-03-21)

    清洗url中,要导入库
    from urllib.parse import urlparse

    • 回复
      veelion 5年前 (2019-03-23)
      回复 @ :You got it!
  3. 3楼
    匿名 5年前 (2019-03-21)

    url = urlparse.urlunparse((
    up.scheme,
    up.netloc,
    path,
    up.params,
    query,
    ” # crawler do not care fragment
    ))
    清洗URL中,这里为什么是两个(())括号呢?
    ” # crawler do not care fragment 这里是用两个单引号表示省略的意思吗?

    • 回复
      veelion 5年前 (2019-03-23)
      回复 @ :(1) urlunparse()函数传递了一个元组,里面的括号是元组的。(2) 传递的元组必须是6个值,最后的fragment不需要所以用空字符串(就是两个单引号表示)补位。
  4. 4楼
    gaomengsuijia 5年前 (2019-04-15)

    windows下,怎么安装leveldb模块?

    • 回复
      王平 5年前 (2019-04-16)
      回复 @gaomengsuijia :windows编译比较麻烦,要安装visual c++,建议用Linux系统,windows安装可以参考 https://github.com/ren85/leveldb-windows
  5. 5楼
    匿名 5年前 (2019-04-15)

    —————————————————————————
    ValueError Traceback (most recent call last)
    in ()
    34 if __name__ == ‘__main__’:
    35 url = ‘http://news.baidu.com/’
    —> 36 s,html = downloader(url)
    37 print(s,len(html))

    ValueError: too many values to unpack (expected 2)

    • 回复
      王平 5年前 (2019-04-16)
      回复 @ :你的python是多少版本的?
      • 大大派 5年前 (2019-05-01)
        回复 @王平 :源码返回值没写全,除了状态码, html,还有一个重定向url
      • veelion 5年前 (2019-05-05)
        回复 @大大派 :为你的细心点赞!已经修正,命名为“lost_url_found_by_大大派 ”
  6. 6楼
    Castle 5年前 (2019-07-04)

    clean_url函数有点疑问:
    正常的url在第二步去掉静态化url后面的参数就被返回了 那什么样的url能够到最后一个return 可以举个例子吗?

    • 回复
      veelion 5年前 (2019-07-04)
      回复 @Castle :类似这样的: https://www.yuanrenxue.cn/crawler/post-123456?utm_source=yuanrenxue
  7. 7楼
    coming 5年前 (2019-08-07)

    python3.6 是导入?
    from urllib.parse import urlparse
    from urllib.parse import urlunparse

    • 回复
      王平 5年前 (2019-08-09)
      回复 @coming :你看下3.6下urllib源代码 是不是这样用的
  8. 8楼
    匿名 5年前 (2019-11-19)

    http://blog.itpub.net/69913713/viewspace-2645819/,作者大佬,我在这个网站看到和你这里一模一样的内容,你去看一下是不是盗你的

    • 回复
      王平 5年前 (2019-11-19)
      回复 @匿名 :嗯 很是无耻,这个网站上的文字基本都是抄袭其它渠道的
  9. 9楼
    kython 4年前 (2020-10-16)

    s, html,lost_url_found_by_大大派 = downloader(url)
    print(s, len(html),lost_url_found_by_大大派)
    报告老师,这里的逗号有点问题