Python實(shí)戰(zhàn)之手寫(xiě)一個(gè)搜索引擎
一、前言
這篇文章,我們將會(huì)嘗試從零搭建一個(gè)簡(jiǎn)單的新聞搜索引擎
當(dāng)然,一個(gè)完整的搜索引擎十分復(fù)雜,這里我們只介紹其中最為核心的幾個(gè)模塊
分別是數(shù)據(jù)模塊、排序模塊和搜索模塊,下面我們會(huì)逐一講解,這里先從宏觀上看一下它們之間的工作流程
二、工作流程
三、數(shù)據(jù)模塊
數(shù)據(jù)模塊的主要作用是爬取網(wǎng)絡(luò)上的數(shù)據(jù),然后對(duì)數(shù)據(jù)進(jìn)行清洗并保存到本地存儲(chǔ)
一般來(lái)說(shuō),數(shù)據(jù)模塊會(huì)采用非定向爬蟲(chóng)技術(shù)廣泛爬取網(wǎng)絡(luò)上的數(shù)據(jù),以保證充足的數(shù)據(jù)源
但是由于本文只是演示,所以這里我們僅會(huì)采取定向爬蟲(chóng)爬取中國(guó)社會(huì)科學(xué)網(wǎng)上的部分文章素材
而且因?yàn)榕老x(chóng)技術(shù)我們之前已經(jīng)講過(guò)很多,這里就不打算細(xì)講,只是簡(jiǎn)單說(shuō)明一下流程
首先我們定義一個(gè)數(shù)據(jù)模塊類(lèi),名為 DataLoader
,類(lèi)中有一個(gè)核心變量 data
用于保存爬取下來(lái)的數(shù)據(jù)
以及兩個(gè)相關(guān)的接口 grab_data
(爬取數(shù)據(jù)) 和 save_data
(保存數(shù)據(jù)到本地)
grab_data()
的核心邏輯如下:
1.首先調(diào)用 get_entry()
,獲取入口鏈接
def get_entry(self): baseurl = 'http://his.cssn.cn/lsx/sjls/' entries = [] for idx in range(5): entry = baseurl if idx == 0 else baseurl + 'index_' + str(idx) + '.shtml' entries.append(entry) return entries
2.然后調(diào)用 parse4links()
,遍歷入口鏈接,解析得到文章鏈接
def parse4links(self, entries): links = [] headers = { 'USER-AGENT': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36' } for entry in entries: try: response = requests.get(url = entry, headers = headers) html = response.text.encode(response.encoding).decode('utf-8') time.sleep(0.5) except: continue html_parser = etree.HTML(html) link = html_parser.xpath('//div[@class="ImageListView"]/ol/li/a/@href') link_filtered = [url for url in link if 'www' not in url] link_complete = [entry + url.lstrip('./') for url in link_filtered] links.extend(link_complete) return links
3.接著調(diào)用 parse4datas()
,遍歷文章鏈接,解析得到文章內(nèi)容
def parse4datas(self, entries): datas = [] headers = { 'USER-AGENT': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36' } data_count = 0 for entry in entries: try: response = requests.get(url = entry, headers = headers) html = response.text.encode(response.encoding).decode('utf-8') time.sleep(0.2) except: continue html_parser = etree.HTML(html) title = html_parser.xpath('//span[@class="TitleFont"]/text()') content = html_parser.xpath('//div[@class="TRS_Editor"]//p//text()') content = [cont.replace('\u3000', '').replace('\xa0', '').replace('\n', '').replace('\t', '') for cont in content] content = [cont for cont in content if len(cont) > 30 and not re.search(r'[《|》]', cont)] if len(title) != 0 or len(content) != 0: data_count += 1 datas.append({ 'id' : data_count, 'link': entry, 'cont': '\t'.join(content), 'title': title[0] }) return datas
grab_data()
的核心代碼如下:
def grab_data(self): # 獲取入口鏈接 entries = self.get_entry() # 遍歷入口鏈接,解析得到文章鏈接 links = self.parse4links(entries) # 遍歷文章鏈接,解析得到文章內(nèi)容 datas = self.parse4datas(links) # 將相關(guān)數(shù)據(jù)寫(xiě)入變量 data self.data = pd.DataFrame(datas)
save_data()
的核心代碼如下:
def save_data(self): # 將變量 data 寫(xiě)入 csv 文件 self.data.to_csv(self.data_path, index = None)
至此,我們已經(jīng)爬取并保存好數(shù)據(jù) data
,數(shù)據(jù)以 DataFrame 形式存儲(chǔ),保存在 csv
文件,格式如下:
|---------------------------------------------------| | id | link | cont | title | |---------------------------------------------------| | page id | page link | page content | page title | |---------------------------------------------------| | ...... | ...... | ...... | ...... | |---------------------------------------------------|
四、索引模塊
索引模型的主要作用是構(gòu)建倒排索引 (inverted index),這是搜索引擎中十分關(guān)鍵的一環(huán)
一般來(lái)說(shuō),構(gòu)建索引的目的就是為了提高查詢速度
普通的索引一般是通過(guò)文章標(biāo)識(shí)索引文章內(nèi)容,而倒排索引則正好相反,通過(guò)文章內(nèi)容索引文章標(biāo)識(shí)
具體來(lái)說(shuō),倒排索引會(huì)以文章中出現(xiàn)過(guò)的詞語(yǔ)作為鍵,以該詞所在的文章標(biāo)識(shí)作為值來(lái)構(gòu)建索引
首先我們定義一個(gè)索引模塊類(lèi),名為 IndexModel
,類(lèi)中有一個(gè)核心變量 iindex
用于保存倒排索引
以及兩個(gè)相關(guān)的接口 make_iindex
(構(gòu)建索引) 和 save_iindex
(保存索引到本地)
make_iindex()
的核心代碼如下(具體邏輯請(qǐng)參考注釋):
def make_iindex(self): # 讀取數(shù)據(jù) df = pd.read_csv(self.data_path) # 特殊變量,用于搜索模塊 TOTAL_DOC_NUM = 0 # 總文章數(shù)量 TOTAL_DOC_LEN = 0 # 總文章長(zhǎng)度 # 遍歷每一行 for row in df.itertuples(): doc_id = getattr(row, 'id') # 文章標(biāo)識(shí) cont = getattr(row, 'cont') # 文章內(nèi)容 TOTAL_DOC_NUM += 1 TOTAL_DOC_LEN += len(cont) # 對(duì)文章內(nèi)容分詞 # 并將其變成 {word: frequency, ...} 的形式 cuts = jieba.lcut_for_search(cont) word2freq = self.format(cuts) # 遍歷每個(gè)詞,將相關(guān)數(shù)據(jù)寫(xiě)入變量 iindex for word in word2freq: meta = { 'id': doc_id, 'dl': len(word2freq), 'tf': word2freq[word] } if word in self.iindex: self.iindex[word]['df'] = self.iindex[word]['df'] + 1 self.iindex[word]['ds'].append(meta) else: self.iindex[word] = {} self.iindex[word]['df'] = 1 self.iindex[word]['ds'] = [] self.iindex[word]['ds'].append(meta) # 將特殊變量寫(xiě)入配置文件 self.config.set('DATA', 'TOTAL_DOC_NUM', str(TOTAL_DOC_NUM)) # 文章總數(shù) self.config.set('DATA', 'AVG_DOC_LEN', str(TOTAL_DOC_LEN / TOTAL_DOC_NUM)) # 文章平均長(zhǎng)度 with open(self.option['filepath'], 'w', encoding = self.option['encoding']) as config_file: self.config.write(config_file)
save_iindex()
的核心代碼如下:
def save_iindex(self): # 將變量 iindex 寫(xiě)入 json 文件 fd = open(self.iindex_path, 'w', encoding = 'utf-8') json.dump(self.iindex, fd, ensure_ascii = False) fd.close()
至此,我們們經(jīng)構(gòu)建并保存好索引 iindex
,數(shù)據(jù)以 JSON 形式存儲(chǔ),保存在 json
文件,格式如下:
{ word: { 'df': document_frequency, 'ds': [{ 'id': document_id, 'dl': document_length, 'tf': term_frequency }, ...] }, ... }
五、搜索模塊
在得到原始數(shù)據(jù)和構(gòu)建好倒排索引后,我們就可以根據(jù)用戶的輸入查找相關(guān)的內(nèi)容
具體怎么做呢?
1.首先我們對(duì)用戶的輸入進(jìn)行分詞
2.然后根據(jù)倒排索引獲取每一個(gè)詞相關(guān)的文章
3.最后計(jì)算每一個(gè)詞與相關(guān)文章之間的得分,得分越高,說(shuō)明相關(guān)性越大
這里我們定義一個(gè)搜索模塊類(lèi),名為 SearchEngine
,類(lèi)中有一個(gè)核心函數(shù) search
用于查詢搜索
def search(self, query): BM25_scores = {} # 對(duì)用戶輸入分詞 # 并將其變成 {word: frequency, ...} 的形式 query = jieba.lcut_for_search(query) word2freq = self.format(query) # 遍歷每個(gè)詞 # 計(jì)算每個(gè)詞與相關(guān)文章之間的得分(計(jì)算公式參考 BM25 算法) for word in word2freq: data = self.iindex.get(word) if not data: continue BM25_score = 0 qf = word2freq[word] df = data['df'] ds = data['ds'] W = math.log((self.N - df + 0.5) / (df + 0.5)) for doc in ds: doc_id = doc['id'] tf = doc['tf'] dl = doc['dl'] K = self.k1 * (1 - self.b + self.b * (dl / self.AVGDL)) R = (tf * (self.k1 + 1) / (tf + K)) * (qf * (self.k2 + 1) / (qf + self.k2)) BM25_score = W * R BM25_scores[doc_id] = BM25_scores[doc_id] + BM25_score if doc_id in BM25_scores else BM25_score # 對(duì)所有得分按從大到小的順序排列,返回結(jié)果 BM25_scores = sorted(BM25_scores.items(), key = lambda item: item[1]) BM25_scores.reverse() return BM25_scores
到此這篇關(guān)于Python實(shí)戰(zhàn)之手寫(xiě)一個(gè)搜索引擎的文章就介紹到這了,更多相關(guān)Python寫(xiě)搜索引擎內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 10分鐘用Python快速搭建全文搜索引擎詳解流程
- python基于搜索引擎實(shí)現(xiàn)文章查重功能
- Python大批量搜索引擎圖像爬蟲(chóng)工具詳解
- Python無(wú)損音樂(lè)搜索引擎實(shí)現(xiàn)代碼
- 淺談?dòng)肞ython實(shí)現(xiàn)一個(gè)大數(shù)據(jù)搜索引擎
- Python搜索引擎實(shí)現(xiàn)原理和方法
- Python中使用haystack實(shí)現(xiàn)django全文檢索搜索引擎功能
- 用python做一個(gè)搜索引擎(Pylucene)的實(shí)例代碼
- 以Python的Pyspider為例剖析搜索引擎的網(wǎng)絡(luò)爬蟲(chóng)實(shí)現(xiàn)方法
- python做圖片搜索引擎并保存到本地詳情
相關(guān)文章
python tkinter實(shí)現(xiàn)簡(jiǎn)單計(jì)算器功能
這篇文章主要為大家詳細(xì)介紹了python tkinter實(shí)現(xiàn)簡(jiǎn)單計(jì)算器功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01對(duì)Django 中request.get和request.post的區(qū)別詳解
今天小編就為大家分享一篇對(duì)Django 中request.get和request.post的區(qū)別詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-08-08對(duì)python中的乘法dot和對(duì)應(yīng)分量相乘multiply詳解
今天小編就為大家分享一篇對(duì)python中的乘法dot和對(duì)應(yīng)分量相乘multiply詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-11-11Python+radar實(shí)現(xiàn)隨機(jī)日期時(shí)間的生成
Python有廣泛豐富的第三方庫(kù),在沒(méi)有特殊定制下,避免了重復(fù)造輪子。本文將利用radar庫(kù)實(shí)現(xiàn)生成隨機(jī)的日期或時(shí)間,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2022-05-05Python基于Django實(shí)現(xiàn)驗(yàn)證碼登錄功能
驗(yàn)證碼登錄是一種常見(jiàn)的身份驗(yàn)證方式,它可以有效防止惡意攻擊和機(jī)器人登錄,本文將介紹如何基于Python?Django實(shí)現(xiàn)驗(yàn)證碼登錄功能,需要的可以參考一下2023-05-05Python Jupyter Notebook顯示行數(shù)問(wèn)題的解決
這篇文章主要介紹了Python Jupyter Notebook顯示行數(shù)問(wèn)題的解決方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-02-02Python中FastAPI項(xiàng)目使用 Annotated的參數(shù)設(shè)計(jì)的處理方案
FastAPI 是一個(gè)非?,F(xiàn)代化和高效的框架,非常適合用于構(gòu)建高性能的 API,FastAPI 是一個(gè)用于構(gòu)建 API 的現(xiàn)代、快速(高性能)web 框架,基于 Python 類(lèi)型提示,這篇文章主要介紹了Python中FastAPI項(xiàng)目使用 Annotated的參數(shù)設(shè)計(jì),需要的朋友可以參考下2024-08-08Python requests的SSL證書(shū)驗(yàn)證方式
這篇文章主要介紹了Python-requests的SSL證書(shū)驗(yàn)證方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02深度學(xué)習(xí)中shape[0]、shape[1]、shape[2]的區(qū)別詳解
本文主要介紹了深度學(xué)習(xí)中shape[0]、shape[1]、shape[2]的區(qū)別詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07