专栏首页程序员的知识天地大规模异步新闻爬虫【4】:实现一个同步定向新闻爬虫

大规模异步新闻爬虫【4】:实现一个同步定向新闻爬虫

前面,我们先写了一个简单的百度新闻爬虫,可是它槽点满满。接着,我们实现了一些模块,来为我们的爬虫提供基础功能,包括:网络请求、网址池、MySQL封装。

有了这些基础模块,我们的就可以实现一个更通用化的新闻爬虫了。为什么要加“定向”这个修饰词呢?因为我们的爬虫不是漫无目的的广撒网(广撒网给我们带来的服务器、带宽压力是指数级增长的),而是在我们规定的一个范围内进行爬取。

这个范围如何规定呢?我们称之为:hub列表。在实现网址池的到时候,我们简单介绍了hub页面是什么,这里我们再简单定义一下它:hub页面就是含有大量新闻链接、不断更新的网页。我们收集大量不同新闻网站的hub页面组成一个列表,并配置给新闻爬虫,也就是我们给爬虫规定了抓取范围:host跟hub列表里面提到的host一样的新闻我们才抓。这样可以有些控制爬虫只抓我们感兴趣的新闻而不跑偏乱抓一气。

这里要实现的新闻爬虫还有一个定语“同步”,没错,这次实现的是同步机制下的爬虫。后面会有异步爬虫的实现。

同步和异步的思维方式不太一样,同步的逻辑更清晰,所以我们先把同步爬虫搞清楚,后面再实现异步爬虫就相对简单些,同时也可以对比同步和异步两种不同机制下爬虫的抓取效率。

这个爬虫需要用到MySQL数据库,在开始写爬虫之前,我们要简单设计一下数据库的表结构:

1. 数据库设计

创建一个名为crawler的数据库,并创建爬虫需要的两个表:

crawler_hub :此表用于存储hub页面的url

+------------+------------------+------+-----+-------------------+----------------+
| Field      | Type             | Null | Key | Default           | Extra          |
+------------+------------------+------+-----+-------------------+----------------+
| id         | int(10) unsigned | NO   | PRI | NULL              | auto_increment |
| url        | varchar(64)      | NO   | UNI | NULL              |                |
| created_at | timestamp        | NO   |     | CURRENT_TIMESTAMP |                |
+------------+------------------+------+-----+-------------------+----------------+

创建该表的语句就是:

CREATE TABLE `crawler_hub` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `url` varchar(64) NOT NULL,
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `url` (`url`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8

对url字段建立唯一索引,可以防止重复插入相同的url。

crawler_html :此表存储html内容 html是大量的文本内容,压缩存储会大大减少磁盘使用量。这里,我们选用lzma压缩算法。表的结构如下:

+------------+---------------------+------+-----+-------------------+----------------+
| Field      | Type                | Null | Key | Default           | Extra          |
+------------+---------------------+------+-----+-------------------+----------------+
| id         | bigint(20) unsigned | NO   | PRI | NULL              | auto_increment |
| urlhash    | bigint(20) unsigned | NO   | UNI | NULL              |                |
| url        | varchar(512)        | NO   |     | NULL              |                |
| html_lzma  | longblob            | NO   |     | NULL              |                |
| created_at | timestamp           | YES  |     | CURRENT_TIMESTAMP |                |
+------------+---------------------+------+-----+-------------------+----------------+

创建该表的语句为:

CREATE TABLE `crawler_html` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `urlhash` bigint(20) unsigned NOT NULL COMMENT 'farmhash',
  `url` varchar(512) NOT NULL,
  `html_lzma` longblob NOT NULL,
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `urlhash` (`urlhash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

该表中,我们存储了url的64位的farmhash,并对这个urlhash建立了唯一索引。id类型为无符号的bigint,也就是2的64次方,足够放下你能抓取的网页。

farmhash是Google开源的一个hash算法。64位的hash空间有2的64次方那么大,大到随意把url映射为一个64位无符号整数,也不会出现hash碰撞。老猿使用它多年也未发现hash碰撞的问题。

由于上传到pypi时,farmhash这个包名不能用,就以pyfarmhash上传到pypi上了,所以要安装farmhash的python包,应该是:

pip install pyfarmhash

(多谢评论的朋友反馈这个问题。)

数据库建立好后,我们就可以开始写爬虫的代码了。

2. 新闻爬虫的代码实现

#!/usr/bin/env python3
# Author: veelion

import urllib.parse as urlparse
import lzma
import farmhash
import traceback

from ezpymysql import Connection
from urlpool import UrlPool
import functions as fn
import config

class NewsCrawlerSync:
    def __init__(self, name):
        self.db = Connection(
            config.db_host,
            config.db_db,
            config.db_user,
            config.db_password
        )
        self.logger = fn.init_file_logger(name + '.log')
        self.urlpool = UrlPool(name)
        self.hub_hosts = None
        self.load_hubs()

    def load_hubs(self,):
        sql = 'select url from crawler_hub'
        data = self.db.query(sql)
        self.hub_hosts = set()
        hubs = []
        for d in data:
            host = urlparse.urlparse(d['url']).netloc
            self.hub_hosts.add(host)
            hubs.append(d['url'])
        self.urlpool.set_hubs(hubs, 300)

    def save_to_db(self, url, html):
        urlhash = farmhash.hash64(url)
        sql = 'select url from crawler_html where urlhash=%s'
        d = self.db.get(sql, urlhash)
        if d:
            if d['url'] != url:
                msg = 'farmhash collision: %s <=> %s' % (url, d['url'])
                self.logger.error(msg)
            return True
        if isinstance(html, str):
            html = html.encode('utf8')
        html_lzma = lzma.compress(html)
        sql = ('insert into crawler_html(urlhash, url, html_lzma) '
               'values(%s, %s, %s)')
        good = False
        try:
            self.db.execute(sql, urlhash, url, html_lzma)
            good = True
        except Exception as e:
            if e.args[0] == 1062:
                # Duplicate entry
                good = True
                pass
            else:
                traceback.print_exc()
                raise e
        return good

    def filter_good(self, urls):
        goodlinks = []
        for url in urls:
            host = urlparse.urlparse(url).netloc
            if host in self.hub_hosts:
                goodlinks.append(url)
        return goodlinks

    def process(self, url, ishub):
        status, html, redirected_url = fn.downloader(url)
        self.urlpool.set_status(url, status)
        if redirected_url != url:
            self.urlpool.set_status(redirected_url, status)
        # 提取hub网页中的链接, 新闻网页中也有“相关新闻”的链接,按需提取
        if status != 200:
            return
        if ishub:
            newlinks = fn.extract_links_re(redirected_url, html)
            goodlinks = self.filter_good(newlinks)
            print("%s/%s, goodlinks/newlinks" % (len(goodlinks), len(newlinks)))
            self.urlpool.addmany(goodlinks)
        else:
            self.save_to_db(redirected_url, html)

    def run(self,):
        while 1:
            urls = self.urlpool.pop(5)
            for url, ishub in urls.items():
                self.process(url, ishub)

if __name__ == '__main__':
    crawler = NewsCrawlerSync('yuanrenxyue')
    crawler.run()

3. 新闻爬虫的实现原理

上面代码就是在基础模块的基础上,实现的完整的新闻爬虫的代码。

它的流程大致如下图所示:

新闻爬虫流程图

我们把爬虫设计为一个类,类在初始化时,连接数据库,初始化logger,创建网址池,加载hubs并设置到网址池。

爬虫开始运行的入口就是run(),它是一个while循环,设计为永不停息的爬。先从网址池获取一定数量的url,然后对每个url进行处理,

处理url也就是实施抓取任务的是process(),它先通过downloader下载网页,然后在网址池中设置该url的状态。接着,从下载得到的html提取网址,并对得到的网址进行过滤(filter_good()),过滤的原则是,它们的host必须是hubs的host。最后把下载得到的html存储到数据。

运行这个新闻爬虫很简单,生成一个NewsCrawlerSync的对象,然后调用run()即可。当然,在运行之前,要先在config.py里面配置MySQL的用户名和密码,也要在crawler_hub表里面添加几个hub网址才行。

思考题: 如何收集大量hub列表

比如,我想要抓新浪新闻 news.sina.com.cn , 其首页是一个hub页面,但是,如何通过它获得新浪新闻更多的hub页面呢?小猿们不妨思考一下这个问题,并用代码来实现一下。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 大规模异步新闻爬虫【2】:实现功能强大,简洁易用的网址池(URL Pool)

    对于比较大型的爬虫来说,URL管理的管理是个核心问题,管理不好,就可能重复下载,也可能遗漏下载。这里,我们设计一个URL池来管理URL。 这个URL池就是一个...

    一墨编程学习
  • 用python抓取某腾视频所有电影的爬虫,不用钱就可以看会员电影!

    一墨编程学习
  • 大规模异步新闻爬虫【6】:用asyncio实现异步爬虫

    关于异步IO这个概念,可能有些小猿们不是非常明白,那就先来看看异步IO是怎么回事儿。 为了大家能够更形象得理解这个概念,我们拿放羊来打个比方:

    一墨编程学习
  • Ajax Step By Step1

    jQuery 对 Ajax 做了大量的封装,不需要去考虑浏览器兼容性, 对于封装的方式,jQuery 采用了三层封装:最底层的封装方法为:$.ajax(),而通...

    wfaceboss
  • Python爬虫抓取csdn博客

            昨天晚上为了下载保存某位csdn大牛的全部博文,写了一个爬虫来自动抓取文章并保存到txt文本,当然也可以 保存到html网页中。这样就可以不用C...

    用户2398817
  • 爬虫的结构是什么样的呢?

    在软件工程中,有着这么几个字“高内聚低耦合”,意思就是说:大模块分割成一个个小模块实现,每一个模块之间的独立性较高,修改某个模块,对其他模块或整个项目影响较小。

    用户6825444
  • Python爬虫| 实战爬取腾讯视频评论

    根据上图,我们可以知道:评论使用了Ajax异步刷新技术。这样就不能使用以前分析当前页面找出规律的手段了。因为展示的页面只有部分评论,还有大量的评论没有被刷新出来...

    JAVAandPython君
  • python破解知乎爬虫技术架构

    去年自己开发了一个知乎爬虫系统,我现将整个技术思路和架构整理出来分享给大家,希望对大家有帮助。

    孔雀
  • 如何用Python 编写知乎爬虫?So easy!

    在爬虫系统中,待抓取 URL 队列是很重要的一部分。待抓取 URL 队列中的 URL 以什么样的顺序排列也是一个很重要的问题,因为这涉及到先抓取那个页面,后抓取...

    IT派
  • Python模拟登录和登录跳转

    用户2398817

扫码关注云+社区

领取腾讯云代金券

玩转腾讯云 有奖征文活动