公司里是怎么做数据抓取的? --- 搜狗词库抓取&解析

公司里是怎么做数据抓取的? --- 搜狗词库抓取&解析

不知不觉已经上了三周的班了,也写了几个小项目啦, 这里和大家分享一个比较有意思的抓取项目:搜狗输入法词库爬取,和 大家说说在公司里都是怎么写项目的

导读

本次抓取的内容是:搜狗的全部词库

地址:pinyin.sogou.com/dict/c

这次有点复杂需要一些前置的知识:

  • 数据库表设计
  • 数据库的读取
  • 日志模块运用
  • 多线程的运用
  • 队列的运用
  • 字符串编码&解码
  • ...

项目文件梳理:

PS:这里的代码我是自己重构过,
公司里的代码要用内部框架来抓取。所以不能分享出来啦

吐个槽: 我司还在用Python2.7,编码的问题要把我搞死了!!
我猜是因为2.7的print 可以少打一对括号才一直不升级 逃~

来看一下项目结构:

├── configs.py  # 数据库的配置文件
├── jiebao.py    # 搜狗的词库是加密的,用来解密词库用的
├── spider
│   ├── log_SougouDownloader.log.20171118  # 词库下载日志
│   ├── log_SougouSpider.log.20171118           # 抓取日志
│   └── spider.py  # 爬虫文件
├── store_new
│   ├── __init__.py
│   └── stroe.py # 数据库操作的封装
└── utils
    └── tools.py # 日志模块

整体的还是比较清晰的
源文件我放在GitHub里:

github.com/Ehco1996/Pyt

爬虫逻辑&数据库表设计

来看一下入口的网页结构:

我们需要解析 :

  • 一级分类
  • 二级分类
  • 文件名
  • 真实下载地址

由于数据量较大
有约1万个词库文件需要下载
有约 1亿 个关键词需要解析
不能将所有逻辑耦合在一起

这里我建立三张数据库表:

  • 存储词库二级分类入口的 cate 表:
  • 存储所有下载地址的 detail
  • 存储所有解析后关键词的keyword

所以整体的逻辑是这样的:

  • 抓取二级分类的入口地址 写入cate 表
  • cate 表 读取地址发送请求后,解析下载地址存入detail表
  • detail 表 读取词库的下载地址下载词库到文件
  • 从本地读文件解析成关键词记录存入keyword 表

代码部分

首先是二级cate页的解析

   def cate_ext(self, html, type1):
        '''
        解析列表页的所有分类名
        Args:
            html 文本
            type1 一级目录名        
        '''
        res = []
        soup = BeautifulSoup(html, 'lxml')
        cate_list = soup.find('div', {'id': 'dict_cate_show'})
        lis = cate_list.find_all('a')
        for li in lis:
            type2 = li.text.replace('"', '')
            url = 'http://pinyin.sogou.com' + li['href'] + '/default/{}'
            res.append({
                'url': url,
                'type1': type1,
                'type2': type2,
            })
        return res

这里我通过解析一级分类的入口页来获取所有二级分类的地址

词库文件下载地址的解析:

   def list_ext(self, html, type1, type2):
        '''
        解析搜狗词库的列表页面
        args:
            html: 文本
            type1 一级目录名 
            type2 二级目录名 
        retrun list
        每一条数据都为字典类型
        '''
        res = []
        try:
            soup = BeautifulSoup(html, 'lxml')
            # 偶数部分
            divs = soup.find_all("div", class_='dict_detail_block')
            for data in divs:
                name = data.find('div', class_='detail_title').a.text
                url = data.find('div', class_='dict_dl_btn').a['href']
                res.append({'filename': type1 + '_' + type2 + '_' + name,
                            'type1': type1,
                            'type2': type2,
                            'url': url,
                            })
            # 奇数部分
            divs_odd = soup.find_all("div", class_='dict_detail_block odd')
            for data in divs_odd:
                name = data.find('div', class_='detail_title').a.text
                url = data.find('div', class_='dict_dl_btn').a['href']
                res.append({'filename': type1 + '_' + type2 + '_' + name,
                            'type1': type1,
                            'type2': type2,
                            'url': url,
                            })
        except:
            print('解析失败')
            return - 1
        return res

爬虫的入口:

   def start(self):
        '''
        解析搜狗词库的下载地址和分类名称
        '''
        # 从数据库读取二级分类的入口地址
        cate_list = self.store.find_all('sougou_cate')
        for cate in cate_list:
            type1 = cate['type1']
            type2 = cate['type2']
            for i in range(1, int(cate['page']) + 1):
                print('正在解析{}的第{}页'.format(type1 + type2, i))
                url = cate['url'].format(i)
                html = get_html_text(url)
                if html != -1:
                    res = self.list_ext(html, type1, type2)
                    self.log.info('正在解析页面 {}'.format(url))
                    for data in res:
                        self.store.save_one_data('sougou_detail', data)
                        self.log.info('正在存储数据{}'.format(data['filename']))
                time.sleep(3)

我这里通过从cate表里读取记录,来发请求并解析出下载地址

对于数据库操作的封装,可以直接阅读我 store_new/store.py这个文件
也可以看我上周写的文章:zhuanlan.zhihu.com/p/30

词库文件下载逻辑:

   def start(self):
        # 从数据库检索记录
        res = self.store.find_all('sougou_detail')
        self.log.warn('一共有{}条词库等待下载'.format(len(res)))
        for data in res:
            content = self.get_html_content(data['url'])
            filename = self.strip_wd(data['filename'])
            # 如果下载失败,我们等三秒再重试
            if content == -1:
                time.sleep(3)
                self.log.info('{}下载失败 正在重试'.format(filename))
                content = self.get_html_content(data[1])
            self.download_file(content, filename)
            self.log.info('正在下载文件{}'.format(filename))
            time.sleep(1)

这个部分没有什么难的,主要涉及到文件的读写操作

解析词库文件逻辑:

def start():
    # 使用多线程解析
    threads = list()
    # 读文件存入queue的线程
    threads.append(
        Thread(target=ext_to_queue))

    # 存数据库的线程
    for i in range(10):
        threads.append(
            Thread(target=save_to_db))
    for thread in threads:
        thread.start()

好了,所有的步骤都已经过了一遍
下面我们来说一些里面的「

Logging日志模块的运用

由于需要爬取和下载的数量比较大,
没有log是万万不行的,
毕竟我们不能时刻定在电脑面前看程序输出吧!
实际上,我们写的程序基本是跑在服务器上的。
我都是通过看程序的log文件来判断运行的状态。
在之前的代码里,可以看到我在很多地方都打了log
来看看日志文件都长啥样:

spider日志

downloader日志

运行状态是不是一目了然了呢?
想知道怎么实现的可以看utils/tools.py这个文件
其实就是用了Python自带的logging模块

多线程的使用

词库文件下载下来一共9000+个
平均一个词库有2w条关键词
那么总共就是1.8亿条数据需要存储

假设存储一秒钟可以存储10条数据
那么这么多数据需要存:5000+小时
想想都可怕,如果真的要这么久,那黄花菜都凉了。

这里就需要我们用多线程来进行操作了,
由于涉及到文件的读写
我们得利用队列(queue)来帮助我们完成需求

逻辑是这样的:

首先开一条主线程对本地的词库文件进行读取/解析:

  • 从文件夹读取词库文件到内存
  • 解析词库文件,并将每个关键词存入队列之中
  • 周而复始~

其次开10~50条线程(取决于数据库的最大连接数)对队列进行操作:

  • 不停的从队列中取出一条记录,并存入数据库
  • 如果队列是空的,就sleep一会,等待主线程解析
  • 周而复始~


由于篇幅的长度,我只将从队列取数据的代码片段放出来
想要整个逻辑的可以去阅读jiebao.py

def save_to_db():
    '''
    从数据队列里拿一条数据
    并存入数据库
    '''
    store = DbToMysql(configs.TEST_DB)
    while True:
        try:
            st = time.time()
            data = res_queue.get_nowait()
            t = int(time.time() - st)
            if t > 5:
                print("res_queue", t)
            save_data(data, store)
        except:
            print("queue is empty wait for a while")
            time.sleep(2)

利用多线程之后,速度一下快了几十倍
经过测试:1秒能存500条左右

最后来看一下数据库里解析的关键词:



跑了一段时间之后,已经有4千万的数据啦

好了,这个项目就分享到这里,
还有很多细节部分得自己去看代码啦

每天的学习记录都会 同步更新到:
微信公众号: findyourownway
知乎专栏:zhuanlan.zhihu.com/Ehco
Blog : www.ehcoblog.ml
GitHub: github.com/Ehco1996/Pyt

编辑于 2017-11-19

文章被以下专栏收录