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

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

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

Python学习交流群【 784758214 】内有安装包和学习视频资料,零基础,进阶,解答疑问。希望可以帮助你快速了解Python、学习python

这个范围如何规定呢?我们称之为: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页面呢?不妨思考一下这个问题,并用代码来实现一下。

猜你喜欢

转载自blog.csdn.net/haoyongxin781/article/details/89892418
今日推荐