python搜索引擎

  用python如何实现一个站内搜索引擎?

  先想想搜索引擎的工作流程:

1、网页搜集。用深度或者广度优先的方法搜索某个网站,保存下所有的网页,对于网页的维护采用定期搜集和增量搜集的方式。

2、建立索引库。首先,过滤掉重复的网页,虽然他们有不同的URL;然后,提取出网页的正文;最后,对正文切词,建立索引。索引总要有个顺序,利用pagerank算法给每个网页加个权值。

3、提供搜索服务。首先,切分查询词;然后,对索引结果排序,结合原来的权值和用户的查询历史等作为新的索引顺序;最后,还要显示文档摘要。

  完整流程如下:

-----------------------------------以下文字引用自  万维网Web自动搜索引擎(技术报告)邓雄(Johnny Deng) 2006.12 

  “网络蜘蛛”从互联网上抓取网页,把网页送入“网页数据库”,从网页中“提取URL”,把URL送入“URL数据库”,“蜘蛛控制”得到网页的URL,控制“网络蜘蛛”抓取其它网页,反复循环直到把所有的网页抓取完成。

    系统从“网页数据库”中得到文本信息,送入“文本索引”模块建立索引,形成“索引数据库”。同时进行“链接信息提取”,把链接信息(包括锚文本、链接本身等信息)送入“链接数据库”,为“网页评级”提供依据。

    “用户”通过提交查询请求给“查询服务器”,服务器在“索引数据库”中进行相关网页的查找,同时“网页评级”把查询请求和链接信息结合起来对搜索结果进行相关度的评价,通过“查询服务器”按照相关度进行排序,并提取关键词的内容摘要,组织最后的页面返回给“用户”。

-----------------------------------引用到此结束

  写一个搜索引擎的想法源自于我正在学习python语言,想借此来驱动自己。

  目前有思路的三个模块:网页爬虫(广度优先搜索),提取网页正文(cx-extractor),中文分词(smallseg)。

网页爬虫

  广度优先搜索,抓取新浪站内10000个页面(url中含有‘sina.com.cn/’的页面)

  抓取: urllib2.urlopen()

  解析:htmllib.HTMLParser

  存储:redis

    每个URL对应一个IDSEQ序列(从1000000开始增加)

    URL:IDSEQ      存储URL

    PAGE:IDSEQ      存储URL对应的HTML页面源码

    URLSET:IDSEQ    每个URL对应一个指向它的URL(IDSEQ)集合

代码如下:

 

View Code
 1 #!/usr/bin/python
 2 from spdUtility import PriorityQueue,Parser
 3 import urllib2
 4 import sys
 5 import os
 6 import inspect
 7 import time
 8 g_url = 'http://www.sina.com.cn'
 9 g_key = 'www'
10 """
11 def line():
12     try:
13         raise Exception
14     except:
15         return sys.exc_info()[2].tb_frame.f_back.f_lineno"""
16 
17 def updatePriQueue(priQueue, url):
18     extraPrior = url.endswith('.html') and 2 or 0 
19     extraMyBlog = g_key in url and 5 or 0 
20     item = priQueue.getitem(url)
21     if item:
22         newitem = (item[0]+1+extraPrior+extraMyBlog, item[1])
23         priQueue.remove(item)
24         priQueue.push(newitem)
25     else :
26         priQueue.push( (1+extraPrior+extraMyBlog,url) )
27         
28 def getmainurl(url):
29     ix = url.find('/',len('http://') )
30     if ix > 0 :
31         return url[:ix]
32     else :
33         return url
34 def analyseHtml(url, html, priQueue, downlist):
35     p = Parser()
36     try :
37         p.feed(html)
38         p.close()
39     except:
40         return
41     mainurl = getmainurl(url)
42     print mainurl
43     for (k, v) in p.anchors.items():
44         for u in v :
45             if not u.startswith('http://'):    
46                 u = mainurl + u
47             if not downlist.count(u):
48                 updatePriQueue( priQueue, u)
49                 
50 def downloadUrl(id, url, priQueue, downlist,downFolder):
51     downFileName = downFolder+'/%d.html' % (id,)
52     print 'downloading', url, 'as', downFileName, time.ctime(),
53     try:
54         fp = urllib2.urlopen(url)
55     except:
56         print '[ failed ]'
57         return False
58     else :
59         print '[ success ]'
60         downlist.push( url )
61         op = open(downFileName, "wb")
62         html = fp.read()
63         op.write( html )
64         op.close()
65         fp.close()
66         analyseHtml(url, html, priQueue, downlist)
67         return True
68 
69 def spider(beginurl, pages, downFolder):
70     priQueue = PriorityQueue()
71     downlist = PriorityQueue()
72     priQueue.push( (1,beginurl) )
73     i = 0
74     while not priQueue.empty() and i < pages :
75         k, url = priQueue.pop()
76         if downloadUrl(i+1, url, priQueue , downlist, downFolder):
77             i += 1
78     print '\nDownload',i,'pages, Totally.'
79 
80 def main():
81     beginurl = g_url
82     pages = 20000
83     downloadFolder = './spiderDown'
84     if not os.path.isdir(downloadFolder):
85         os.mkdir(downloadFolder)
86     spider( beginurl, pages, downloadFolder)
87 
88 if __name__ == '__main__':
89     main()

 

后期优化:

  目前程序抓取速度较慢,瓶颈主要在urllib2.urlopen等待网页返回,此处可提出来单独做一个模块,改成多进程实现,redis的list可用作此时的消息队列。

  扩展程序,实现增量定期更新。

抽取网页正文

  根据提陈鑫的论文《基于行块分布函数的通用网页正文抽取算法》,googlecode上的开源项目(http://code.google.com/p/cx-extractor/

  “作者将网页正文抽取问题转化为求页面的行块分布函数,这种方法不用建立Dom树,不被病态HTML所累(事实上与HTML标签完全无关)。通过在线性时间内建立的行块分布函数图,直接准确定位网页正文。同时采用了统计与规则相结合的方法来处理通用性问题。作者相信简单的事情总应该用最简单的办法来解决这一亘古不变的道理。整个算法实现代码不足百行。但量不在多,在法。”

  他的项目中并没有python版本的程序,下面的我根据他的论文和其他代码写的python程序,短小精悍,全文不过50行代码:

View Code
 1 #!/usr/bin/python
 2 #coding=utf-8
 3 #根据 陈鑫《基于行块分布函数的通用网页正文抽取算法》
 4 #Usage: ./getcontent.py filename.html
 5 import re
 6 import sys
 7 def PreProcess():
 8     global g_HTML
 9     _doctype = re.compile(r'<!DOCTYPE.*?>', re.I|re.S)
10     _comment = re.compile(r'<!--.*?-->', re.S)
11     _javascript = re.compile(r'<script.*?>.*?<\/script>', re.I|re.S)
12     _css = re.compile(r'<style.*?>.*?<\/style>', re.I|re.S)
13     _other_tag = re.compile(r'<.*?>', re.S)
14     _special_char = re.compile(r'&.{1,5};|&#.{1,5};')
15     g_HTML = _doctype.sub('', g_HTML)
16     g_HTML = _comment.sub('', g_HTML)
17     g_HTML = _javascript.sub('', g_HTML)
18     g_HTML = _css.sub('', g_HTML)
19     g_HTML = _other_tag.sub('', g_HTML)
20     g_HTML = _special_char.sub('', g_HTML)
21 def GetContent(threshold):
22     global g_HTMLBlock
23     nMaxSize = len(g_HTMLBlock)
24     nBegin = 0
25     nEnd = 0
26     for i in range(0, nMaxSize):
27         if g_HTMLBlock[i]>threshold and i+3<nMaxSize and g_HTMLBlock[i+1]>0 and g_HTMLBlock[i+2]>0 and g_HTMLBlock[i+3]>0:
28             nBegin = i
29             break
30     else:
31         return None
32     for i in range(nBegin+1, nMaxSize):
33         if g_HTMLBlock[i]==0 and i+1<nMaxSize and g_HTMLBlock[i+1]==0:
34             nEnd = i
35             break
36     else:
37         return None
38     return '\n'.join(g_HTMLLine[nBegin:nEnd+1])
39 if __name__ == '__main__' and len(sys.argv) > 1:
40     f = file(sys.argv[1], 'r')
41     global g_HTML
42     global g_HTMLLine
43     global g_HTMLBlock
44     g_HTML = f.read()
45     PreProcess()
46     g_HTMLLine = [i.strip() for i in g_HTML.splitlines()]    #先分割成行list,再过滤掉每行前后的空字符
47     HTMLLength = [len(i) for i in g_HTMLLine]    #计算每行的长度
48     g_HTMLBlock = [HTMLLength[i] + HTMLLength[i+1] + HTMLLength[i+2] for i in range(0, len(g_HTMLLine)-3)]    #计算每块的长度
49     print GetContent(200)
50     

  上面是一个demo程序,真正用使用起来需要增加存储功能。

  依然是采用redis存储,读出所有的page页(keys 'PAGE:*'),提取正文,判断正文是否已在容器中(排除URL不同的重复页面),如果在容器中则做下一次循环,不在容器中则加入容器并存储到 CONTENT:IDSEQ 中。

代码如下:

View Code
 1 #!/usr/bin/python
 2 #coding=utf-8
 3 #根据 陈鑫《基于行块分布函数的通用网页正文抽取算法》
 4 import re
 5 import sys
 6 import redis
 7 import bisect
 8 def PreProcess():
 9     global g_HTML
10     _doctype = re.compile(r'<!DOCTYPE.*?>', re.I|re.S)
11     _comment = re.compile(r'<!--.*?-->', re.S)
12     _javascript = re.compile(r'<script.*?>.*?<\/script>', re.I|re.S)
13     _css = re.compile(r'<style.*?>.*?<\/style>', re.I|re.S)
14     _other_tag = re.compile(r'<.*?>', re.S)
15     _special_char = re.compile(r'&.{1,5};|&#.{1,5};')
16     g_HTML = _doctype.sub('', g_HTML)
17     g_HTML = _comment.sub('', g_HTML)
18     g_HTML = _javascript.sub('', g_HTML)
19     g_HTML = _css.sub('', g_HTML)
20     g_HTML = _other_tag.sub('', g_HTML)
21     g_HTML = _special_char.sub('', g_HTML)
22 def GetContent(threshold):
23     global g_HTMLBlock
24     nMaxSize = len(g_HTMLBlock)
25     nBegin = 0
26     nEnd = 0
27     for i in range(0, nMaxSize):
28         if g_HTMLBlock[i]>threshold and i+3<nMaxSize and g_HTMLBlock[i+1]>0 and g_HTMLBlock[i+2]>0 and g_HTMLBlock[i+3]>0:
29             nBegin = i
30             break
31     else:
32         return None
33     for i in range(nBegin+1, nMaxSize):
34         if g_HTMLBlock[i]==0 and i+1<nMaxSize and g_HTMLBlock[i+1]==0:
35             nEnd = i
36             break
37     else:
38         return None
39     return '\n'.join(g_HTMLLine[nBegin:nEnd+1])
40 def BinarySearch(UniqueSet, item):
41     if len(UniqueSet) == 0:
42         return 0
43     left = 0
44     right = len(UniqueSet)-1
45     mid = -1
46     while left <= right:
47         mid = (left+right)/2
48         if UniqueSet[mid] < item :
49             left = mid + 1
50         elif UniqueSet[mid] > item :
51             right = mid -1
52         else:
53             break
54     return UniqueSet[mid] == item and 1 or 0
55 if __name__ == '__main__':
56     global g_redisconn
57     global g_HTML
58     global g_HTMLLine
59     global g_HTMLBlock
60     g_redisconn = redis.Redis()
61     UniqueSet = []
62     keys = g_redisconn.keys('PAGE:*')
63     nI = 0
64     for key in keys:
65         g_HTML = g_redisconn.get(key)
66         PreProcess()
67         g_HTMLLine = [i.strip() for i in g_HTML.splitlines()]    #先分割成行list,再过滤掉每行前后的空字符
68         HTMLLength = [len(i) for i in g_HTMLLine]    #计算每行的长度
69         g_HTMLBlock = [HTMLLength[i] + HTMLLength[i+1] + HTMLLength[i+2] for i in range(0, len(g_HTMLLine)-3)]    #计算每块的长度
70         sContent = GetContent(200)
71         if sContent != None:
72             sContentKey = key.replace('PAGE', 'CONTENT')
73             if BinarySearch(UniqueSet, sContent) == 0:
74                 bisect.insort(UniqueSet, sContent)
75                 g_redisconn.set(sContentKey, sContent)

 

中文分词

  smallseg -- 开源的,基于DFA的轻量级的中文分词工具包

  特点:可自定义词典、切割后返回登录词列表和未登录词列表、有一定的新词识别能力。

  下载地址:http://code.google.com/p/smallseg/downloads/detail?name=smallseg_0.6.tar.gz&can=2&q

 


总结

  python简洁易用,类库齐全,但在国内应用还不是十分广泛,各类中文资源不是很多,尤其是深入讲解文档很少。

  到目前为止我已经简单实现了搜索引擎的几个部分,可以说是十分粗糙,没有写这个搜索的客户端,因为到目前为止已经发现了足够多的问题待解决和优化。

优化

  爬虫部分:采用pycurl替换urllib2,更快速,支持user-agent配置;增加字符集识别,统一转换成一种字符后再存储;多进程优化。

  url去重复:取url的fingerprint,用redis存储

  page去重:取页面的fingerprint,用redis存储,比较相似性

  如果有更大的数据量,就要考虑采用分布式存储;如果有更精准的要求,就要考虑一个新的分词系统。

posted on 2012-09-20 17:07  favourmeng  阅读(13146)  评论(2编辑  收藏  举报

导航