搜狗、百度、QQ输入法的词库爬虫

本文主要讲述了通过 python 实现的用于下载搜狗、百度、QQ三个输入法的词库的爬虫的实现原理。主要利用了python自带的urllib2Queuerethreading模块,并分别通过单线程和多线程实现。最后会给出完整的源码地址。

原理及注意事项

爬取每个输入法的实现原理都一样,步骤如下:

(1)获取输入法词库的分类 (2)下载各个分类并按分类在本地存储

其中(1)实现的关键点是正则表达式提取网页中的词库分类,(2)实现的关键点是通过广度优先搜索(bfs)来遍历某一类别的所有页面。

正则表达式提取词库分类

步骤(1)通过解释词库分类的网页源码来获取具体分类(以搜狗输入法为例,词库分类的网页为http://pinyin.sogou.com/dict/ )。先人工观察网页源码的结构,一般同一级的分类的在源码中的 href(超链接)的结构都是一样的,仅仅是id或名字不同,这就可以通过正则表达式来提取网页中的分类,并用嵌套的字典来存储多级分类

BFS遍历某一分类的所有页面

步骤(1)提供了词库的分类,步骤(2)需要考虑的就是怎么下载某一类词库。因为词库文件的数量较多,所以即使是某一类的词库往往也会分多个页面来展示(常见的如通过点击上一页、下一页跳转),所以要完整下载这一类的词库必须遍历这一类词库的所有页面。

这里采用BFS来实现,实现步骤如下: 1)先通过正则表达式分析当前访问页面的源码,获取当前页面可以跳转到的其他页面的url,然后将这些URL放入到队列中作为待访问的URL 2)通过正则表达式获取分析当前访问的页面的源码,获取可以下载的词库文件的url存入一个临时的列表(List)中,并开始逐一下载各个文件 3)将当前页面url放入到一个集合(set)中,标记为已访问的url,防止下一次重复访问 4)从队列中取出下一个url,到步骤3)的集合中检查url是否被访问,如果被访问过则继续取下一个,否则将这个url设为当前url并回到步骤1)

按照上面的步骤一直循环执行直到队列为空,这样便可下载了某一类的词库。如法炮制,便可下载所有类的词库了。

这里需要注意的是,在下载词库文件的时候有可能会出现防盗链的问题。简单来说就是下载词库文件的http请求头中的来源(Referer)不属于本站的就拒绝下载,返回403 forbidden。这种设计就是为了防止爬虫的整站下载,在爬取过程中发现搜狗输入法中有防盗链,而百度、QQ输入法则没有。解决方法也很简单,就是在下载文件是构造一个http请求头(headers),设置里面的Refererr为该站的任一url即可。

单线程下载

单线程下载的注意事项上面基本提到了,下面贴出QQ输入法单线程下载的关键代码,注意这个代码直接执行会报错,可执行的完整代码见文末。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 下载某一类别的关键代码,具体代码见文末链接
queue =Queue.Queue() # 存放待访问url
visited = set() # 已经访问过的url
downloaded = set() # 已经下载过的文件
firstURL = '' # url入口
queue.put(firstURL)
# bfs遍历直至队列为空
while not queue.empty():
currentURL = queue.get()
if currentURL in visited:
continue
else:
visited.add(currentURL)

try:
response = urllib2.urlopen(currentURL)
except :

# 找到链接到其他页面的连接
data = response.read()
pagePattern = re.compile('&page=(\d+)"')
pageList = re.findall(pagePattern,data)
for i in pageList:
pageURL = smallCateURL+'&page='+i
queue.put(pageURL)

# 下载当前页面存在的文件
filePattern = re.compile('<a href="/dict_detail\?dict_id=(\d+)">(.*?)</a>')
fileList = re.findall(filePattern,data)
for id, name in fileList:
fileURL = 'http://dict.qq.pinyin.cn/download?dict_id='+id
filePath = cateDir.decode('utf8')+'/'+name.decode('gbk')+'.qpyd'
# 文件已存在则不下载
if fileURL in downloaded:
continue
else:
downloaded.add(fileURL)
downloadSingleFile(fileURL) #下载这个文件

多线程下载

单线程虽然能够下载词库文件,但是消耗的时间过长,这时候可通过多线程来下载。关于python多线程实现以及python多线程是否能够提高效率可以参考这篇文章因为爬虫属于IO密集型的任务,所以可以通过多线程来提高下载效率。实测多线程开到10个的时候,下载速度比原来快了7倍。

多线程下载实现的思路与单线程的类似,只是在下载某一类别的词库时采用多线程完成,这就需要注意下面几个方面:

  • 线程完成下载的时间不一样,早完成的线程需要等到最后一个线程下载完成才能一起开始下一个类别的下载,可通过python的Queue模块中的join()方法和task_done()方法实现
  • 需要用线程锁保护可能会被修改的变量,采用python的队列数据结构(Queue)则不用,因为python的Queue模块已经实现了这个锁的功能,具体参见Queue — A synchronized queue class

关于队列模块中的join()方法和task_done()方法,官方文档说明如下:

If a join() is currently blocking, it will resume when all items have been processed (meaning that a task_done() call was received for every item that had been put() into the queue).

也就是说Queue调用了join方法后,必须要收到每个取出的item(这里为url)返回的task_done()消息才会继续执行,否则会一直block等待,这就实现了我们说到的早完成的线程需要等到最后一个线程下载完成才能一起开始下一个类别的下载。

实现的关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 构建自己的线程类
queue =Queue.Queue() # 存放待访问url
visited = set() # 已经访问过的url
downloaded = set() # 已经下载过的文件

class downloadThread(threading.Thread):
# 重写run函数
def run(self):
while True:
if queue.empty(): # 防止一开始队列内容太少导致后创建的线程退出
continue
currentURL = queue.get()
# 查看url是否被访问过,需要用锁保护
threadingLock.acquire()
try:
if currentURL in visited:
queue.task_done() # 不可少,否则queue调用了join()会一直block下去
continue
else:
visited.add(currentURL)
finally:
threadingLock.release()

# 解析当前页面
try:
response = urllib2.urlopen(currentURL)
except :

# 找到链接到其他页面的连接
data = response.read()
pageList = re.findall(pagePattern,data)
for i in pageList:
pageURL = smallCateURL+'&page='+i
queue.put(pageURL)

# 下载当前页面存在的文件
fileList = re.findall(filePattern,data)
for id, name in fileList:
fileURL = 'http://dict.qq.pinyin.cn/download?dict_id='+id
filePath = downloadDir.decode('utf8')+'/'+name.decode('gbk')+'.qpyd'

# 检查文件是否被下载
threadingLock.acquire()
try:
if fileURL in downloaded:
continue
else:
downloaded.add(fileURL)
finally:
threadingLock.release()
downloadSingleFile.downloadSingleFile(fileURL, filePath, logFile)
#告诉queue当前任务已完成,否则因为queue调用了join,会一直block下去
queue.task_done()

最后,下载三个输入法的词库的python源码已放到github上,链接https://github.com/WuLC/ThesaurusSpider

文章为博主个人总结,如有错误,欢迎交流指正