搜狗、百度、QQ 输入法的词库爬虫
本文主要讲述了通过 python 实现的用于下载搜狗、百度、QQ 三个输入法的词库的爬虫的实现原理。主要利用了 python 自带的 urllib2
、Queue
、re
、threading
模块,并分别通过单线程和多线程实现。最后会给出完整的源码地址。
原理及注意事项
爬取每个输入法的实现原理都一样,步骤如下:
(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
文章为博主个人总结,如有错误,欢迎交流指正