用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)集合
代碼如下:

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行代碼:

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 中。
代碼如下:

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存儲,比較相似性
如果有更大的數據量,就要考慮采用分布式存儲;如果有更精准的要求,就要考慮一個新的分詞系統。