1.3 爬虫修炼之道——编写一个爬取多页面的网络爬虫

作者: 王伟_同学 | 来源:发表于2017-02-19 10:32 被阅读1148次

    欢迎大家关注我的专题:爬虫修炼之道

    上篇 爬虫修炼之道——编写一个爬取单页面的网络爬虫主要讲解了如何使用python编写一个下载单页面的爬虫,如何下载失败后重试,如何设置用户代理。本篇将编写一个可以爬取多页面的爬虫(以下载 糗事百科 多个网页为例)。涉及到的模块有:

    • re - python中和正则表达式相关的库
    • urlparse 此模块定义了一个标准接口,用于组合URL字符串,并将“相对URL”转换为给定“基本URL”的绝对URL。
    • urllib2 - 此模块对urllib模块进行了增加,增加了将url封装为Request的打开方式。

    下载多页面

    想要下载多个页面,我们需要找到多个URL,常用的有以下三种方式:

    • 使用 sitemap
    • 使用 id 遍历
    • 跟进链接(follow)

    使用sitemap

    一个网站的导航导航可以从该网站的 sitemap 中得到,也就是说,从sitemap可以得到该网站的链接入口。sitemap所对应的URL一般可以在robots.txt中找到,但是使用sitemap有以下几个问题:

    • 使用sitemap适合爬取该网站全量的网页
    • sitemap更新不及时,导致包含的信息不全面
    • 很多网站在robots.txt中没有保存sitemap所对应的URL

    糗事百科robots.txt 文件中并没有发现sitemap。所以这儿没法使用。

    使用 id 遍历

    糗事百科 为例,我们发现每条糗事的URL格式都是 http://www.qiushibaike.com/article/ 前缀加上一个数字id,这样我们就能得到,这样我们就可以假定一个开始的id,然后拼接URL前缀,把拼接后的结果当成最终的URL,但是也有以下几个问题:

    • 如何确定id的初始值
    • id可能不连续
    • 有些网站URL中没有明确的数字id(如简书)

    我们假定起始id为1,也就是说完整URL为 http://www.qiushibaike.com/article/1 ,但是打开后得到一个404错误,说明我们URL有问题。所以这儿也不能用该方法。

    但是在这儿如果我们不想下载每篇糗事的详情页,而是下载 文本板块 的每一页,这个规律就可以用的到,我们发现第一页对应的URL为 http://www.qiushibaike.com/text/page/1 就可以用,第二页对应的URL为 http://www.qiushibaike.com/text/page/2 。一共35页,最后一页对应的URL为 http://www.qiushibaike.com/text/page/35 。这样我们就能够构造出来一个要下载的多个页面的URL列表。

    def structure_links(base_url, page_num):
        # 构建一个内容为base_url、长度为page_num的urls列表
        base_urls = [base_url] * page_num
        # 构建一个[base_url + 1, base_url + 2, ..., base_url + page_num]的url列表
        urls = [base_url + unicode(index + 1) for index, base_url in enumerate(base_urls)]
        return urls
    

    跟进链接(follow)

    虽然使用遍历id的方法可以生成一个URL列表,但是有的网站的URL中可能没有数字id。这时候就需要在当前页面找到下一个页面的链接。

    使用chrome打开网页后,在第一条糗事的文字上点击右键,然后找到“检查”选项,左键:

    右键查找检查

    点击之后得到如下的界面:

    检查源代码

    我们发现下一页的链接在一个 <a> 标签里,我们可以使用正则表达式来匹配出来。

    import re
    
    
    def download(url, user_agent='crawl', num_retries=2, timeout=3):
        """
        下载一个URL页面
        :param url: 要下载的url页面
        :param user_agent: 用户代理
        :param num_retries: 碰到5xx状态码时尝试重新下载的次数
        :param timeout: 超时时间,单位为s
        :return:
        """
        print 'Downloading: url %s' % url
        headers = {'User-Agent': user_agent}
        request = urllib2.Request(url, headers=headers)
        try:
            html = urllib2.urlopen(request, timeout=timeout).read()
        except urllib2.HTTPError, e:
            print 'URL: %s,HTTP Error %s: %s' % (url, e.code, e.msg)
            html = None
            if num_retries > 0 and 500 <= e.code < 600:
                return download(url, num_retries = num_retries - 1)
        except urllib2.URLError, e:
            print 'URL: %s, urlopen Error %s' % (url, e.reason)
            html = None
        return html
    
    link_regex = re.compile('<li>.*<a\s+href="(.*?)".*<span class="next">', re.S)
    html = download('http://www.qiushibaike.com/text/')
    link = get_links(html, link_regex)
    print "link: %s" % link
    

    运行结果:

    跟进链接结果

    注意:

    1. download使用的是上一篇中的 download 方法。
    2. 正则表达式中 .*? 是一个固定的搭配,.* 代表可以匹配任意无限多个字符,加上 ? 表示使用非贪婪模式进行匹配,也就是我们会尽可能短地做匹配,以后我们还会大量用到 .*? 的搭配。
    3. (.*?) 代表一个分组,在这个正则表达式中我们匹配了一个分组,所以使用 [0] 来提取出来。
    4. re.S 标志代表在匹配时为点任意匹配模式,点 . 也可以代表换行符。
    5. 我们得到的URL只是相对URL,下载页面时需要使用绝对URL,否则会出错,下面进行详细描述。

    现在我们来尝试跟进链接来下载多个页面:

    def link_crawler(seed_url, link_regex):
        """
        爬取一个页面
        :param seed_url: 种子URL
        :param link_regex: 从种子URL页面中提取跟进链接所用的正则表达式
        :return:
        """
        crawl_queue = [seed_url]  # 需要下载的URL列表
    
        while crawl_queue:
            url = crawl_queue.pop()  # 将种子URL弹出
    
            html = download(url)  # 下载当前URL页面
    
            links = get_links(html, link_regex)  # 得到当前URL页面的跟进链接
            print "links: %s" % links
            for link in links:
                crawl_queue.append(link)
    
    link_regex = re.compile('<li>.*<a\s+href="(.*?)".*<span class="next">', re.S)
    link_crawler('http://www.qiushibaike.com/text/', link_regex)
    

    运行后我们会发现程序出错了。

    Downloading: url http://www.qiushibaike.com/text/
    links: ['/text/page/2/']
    Downloading: url /text/page/2/
    Traceback (most recent call last):
      File "D:/work/spider_ex/chapter01/1.3.py", line 101, in <module>
        link_crawler('http://www.qiushibaike.com/text/', link_regex)
      File "D:/work/spider_ex/chapter01/1.3.py", line 44, in link_crawler
        html = download(url)  # 下载当前URL页面
      File "D:/work/spider_ex/chapter01/1.3.py", line 71, in download
        html = opener.open(request, timeout=timeout).read()
      File "D:\software_work\Anaconda2\Lib\urllib2.py", line 421, in open
        protocol = req.get_type()
      File "D:\software_work\Anaconda2\Lib\urllib2.py", line 283, in get_type
        raise ValueError, "unknown url type: %s" % self.__original
    ValueError: unknown url type: /text/page/2/
    

    出错原因:无法识别 /text/page/2 这种URL类型,这是因为我们使用的是相对URL,需要转为绝对URL。

    将相对URL转为绝对URL

    为了解决这个问题,urlparse 该上场了。使用urlparse.urljoin方法来解决。完善后的 <a id="link_crawler" href="#link_crawler">link_crawler</a> 如下:

    def link_crawler(seed_url, link_regex):
        """
        爬取一个页面
        :param seed_url: 种子URL
        :param link_regex: 从种子URL页面中提取跟进链接所用的正则表达式
        :return:
        """
        crawl_queue = [seed_url]  # 需要下载的URL列表
    
        while crawl_queue:
            url = crawl_queue.pop()  # 将种子URL弹出
    
            html = download(url)  # 下载当前URL页面
    
            links = get_links(html, link_regex)  # 得到当前URL页面的跟进链接
            print "links: %s" % links
            for link in links:
                link = urlparse.urljoin(seed_url, link)
                crawl_queue.append(link)
    
    link_regex = re.compile(u'<li>.*?<a\s+href="(.*?)".*?<!--.*?<span>', re.S)
    link_crawler('http://www.qiushibaike.com/text/', link_regex)
    

    再次运行:

    跟进链接下载成功

    成功运行。

    下载限速

    有时我们频繁下载一个网站会被封禁,所以需要进行限速。创建一个限速器:

    class Throttle:
        """每次请求相同的域时,会暂停指定的时间
        """
    
        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一直为 www.qiushibaike.com
            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.now() - last_accessed).seconds
                if sleep_secs > 0:
                    print "sleep %s s" % sleep_secs
                    time.sleep(sleep_secs)
            self.domains[domain] = datetime.now()
    

    使用方法:

    throttle = Throttle(delay)
    throttle.wait(url)
    download(url)
    

    支持代理

    有时我们需要使用代理访问某个网站。使用urllib2支持代理代码如下:

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

    这样,我们可以将 <a id="1-3-download" href="#1-3-download">download</a> 进行改进,如下:

    def download(url, headers=None, proxy=None, num_retries=2, timeout=3):
        """
        下载一个URL页面
        :param url: 要下载的url页面
        :param headers: headers
        :param proxy:代理
        :param num_retries: 碰到5xx状态码时尝试重新下载的次数
        :param timeout: 超时时间,单位为s
        :return:
        """
        print 'Downloading url: %s' % url
    
        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:
            response = opener.open(request, timeout=timeout)
            html = response.read()
        except urllib2.HTTPError, e:
            print 'URL: %s,HTTP Error %s: %s' % (url, e.code, e.msg)
            html = None
            if num_retries > 0 and 500 <= e.code < 600:
                return download(url, num_retries = num_retries - 1)
        except urllib2.URLError, e:
            print 'URL: %s, urlopen Error %s' % (url, e.reason)
            html = None
        return html
    

    运行测试

    完整代码见:https://github.com/Oner-wv/spider_note/blob/master/chapter01/1.3.py

    我们运行程序后得到如下结果:

    测试

    到目前为止我们已经能够下载多个页面了,但是得到的只是单纯的HTML代码,这在实际中并没有什么卵用,我们需要的是能够得到HTML代码中所包含的我们想要的数据,比如糗百中每条糗事的文本内容,好笑数,评论数等。这将在下篇进行讲解。

    下篇:爬虫修炼之道——从网页中提取结构化数据并保存(以爬取糗百文本板块所有糗事为例)

    欢迎大家关注我的专题:爬虫修炼之道

    相关文章

      网友评论

      本文标题:1.3 爬虫修炼之道——编写一个爬取多页面的网络爬虫

      本文链接:https://www.haomeiwen.com/subject/rfahwttx.html