Python实战之手写一个搜索引擎
作者:半虹 发布时间:2023-07-11 21:16:49
一、前言
这篇文章,我们将会尝试从零搭建一个简单的新闻搜索引擎
当然,一个完整的搜索引擎十分复杂,这里我们只介绍其中最为核心的几个模块
分别是数据模块、排序模块和搜索模块,下面我们会逐一讲解,这里先从宏观上看一下它们之间的工作流程
二、工作流程
三、数据模块
数据模块的主要作用是爬取网络上的数据,然后对数据进行清洗并保存到本地存储
一般来说,数据模块会采用非定向爬虫技术广泛爬取网络上的数据,以保证充足的数据源
但是由于本文只是演示,所以这里我们仅会采取定向爬虫爬取中国社会科学网上的部分文章素材
而且因为爬虫技术我们之前已经讲过很多,这里就不打算细讲,只是简单说明一下流程
首先我们定义一个数据模块类,名为 DataLoader
,类中有一个核心变量 data
用于保存爬取下来的数据
以及两个相关的接口 grab_data
(爬取数据) 和 save_data
(保存数据到本地)
grab_data()
的核心逻辑如下:
1.首先调用 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.然后调用 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.接着调用 parse4datas()
,遍历文章链接,解析得到文章内容
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)
# 遍历文章链接,解析得到文章内容
datas = self.parse4datas(links)
# 将相关数据写入变量 data
self.data = pd.DataFrame(datas)
save_data()
的核心代码如下:
def save_data(self):
# 将变量 data 写入 csv 文件
self.data.to_csv(self.data_path, index = None)
至此,我们已经爬取并保存好数据 data
,数据以 DataFrame 形式存储,保存在 csv
文件,格式如下:
|---------------------------------------------------|
| id | link | cont | title |
|---------------------------------------------------|
| page id | page link | page content | page title |
|---------------------------------------------------|
| ...... | ...... | ...... | ...... |
|---------------------------------------------------|
四、索引模块
索引模型的主要作用是构建倒排索引 (inverted index),这是搜索引擎中十分关键的一环
一般来说,构建索引的目的就是为了提高查询速度
普通的索引一般是通过文章标识索引文章内容,而倒排索引则正好相反,通过文章内容索引文章标识
具体来说,倒排索引会以文章中出现过的词语作为键,以该词所在的文章标识作为值来构建索引
首先我们定义一个索引模块类,名为 IndexModel
,类中有一个核心变量 iindex
用于保存倒排索引
以及两个相关的接口 make_iindex
(构建索引) 和 save_iindex
(保存索引到本地)
make_iindex()
的核心代码如下(具体逻辑请参考注释):
def make_iindex(self):
# 读取数据
df = pd.read_csv(self.data_path)
# 特殊变量,用于搜索模块
TOTAL_DOC_NUM = 0 # 总文章数量
TOTAL_DOC_LEN = 0 # 总文章长度
# 遍历每一行
for row in df.itertuples():
doc_id = getattr(row, 'id') # 文章标识
cont = getattr(row, 'cont') # 文章内容
TOTAL_DOC_NUM += 1
TOTAL_DOC_LEN += len(cont)
# 对文章内容分词
# 并将其变成 {word: frequency, ...} 的形式
cuts = jieba.lcut_for_search(cont)
word2freq = self.format(cuts)
# 遍历每个词,将相关数据写入变量 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)
# 将特殊变量写入配置文件
self.config.set('DATA', 'TOTAL_DOC_NUM', str(TOTAL_DOC_NUM)) # 文章总数
self.config.set('DATA', 'AVG_DOC_LEN', str(TOTAL_DOC_LEN / TOTAL_DOC_NUM)) # 文章平均长度
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 写入 json 文件
fd = open(self.iindex_path, 'w', encoding = 'utf-8')
json.dump(self.iindex, fd, ensure_ascii = False)
fd.close()
至此,我们们经构建并保存好索引 iindex
,数据以 JSON 形式存储,保存在 json
文件,格式如下:
{
word: {
'df': document_frequency,
'ds': [{
'id': document_id,
'dl': document_length,
'tf': term_frequency
}, ...]
},
...
}
五、搜索模块
在得到原始数据和构建好倒排索引后,我们就可以根据用户的输入查找相关的内容
具体怎么做呢?
1.首先我们对用户的输入进行分词
2.然后根据倒排索引获取每一个词相关的文章
3.最后计算每一个词与相关文章之间的得分,得分越高,说明相关性越大
这里我们定义一个搜索模块类,名为 SearchEngine
,类中有一个核心函数 search
用于查询搜索
def search(self, query):
BM25_scores = {}
# 对用户输入分词
# 并将其变成 {word: frequency, ...} 的形式
query = jieba.lcut_for_search(query)
word2freq = self.format(query)
# 遍历每个词
# 计算每个词与相关文章之间的得分(计算公式参考 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
# 对所有得分按从大到小的顺序排列,返回结果
BM25_scores = sorted(BM25_scores.items(), key = lambda item: item[1])
BM25_scores.reverse()
return BM25_scores
来源:https://blog.csdn.net/wsmrzx/article/details/116122052


猜你喜欢
- getpwname只能得到gid一个username。import pwdmyGroupId = pwd.getpwnam(username
- 我们经常会看到后缀名为.pt, .pth, .pkl的pytorch模型文件,这几种模型文件在格式上有什么区别吗?其实它们并不是在格式上有区
- 本文实例讲述了JS弹出窗口插件zDialog简单用法。分享给大家供大家参考,具体如下:因为没有元素可以显示到Frameset上面去,所以重新
- 1.对数据库常用命令1.连接数据库mysql -u用户名 -p密码2.显示已有数据库show databases;3.创建数据库create
- 本文实例讲述了js显示动态时间的方法。分享给大家供大家参考,具体如下:Date对象的方法Date 对象能够使你获得相对于国际标准时间(格林威
- keras提供简单方便的模型可视化工具,只需一行代码就可以用框图的形式可视化出你搭建的网络结构。对于复杂网络而言,这个工具就是个神器呀。这篇
- 一、持久化 --shelve持久化工具(1)作用:类似字典,用kv对保存数据,存取方式类似于字典(2)例子:通过一下案例创建了一个数据库,第
- 这两天准备复习一下java,所以写一个采用dubbo的商场项目练练手,却卡第一个测试上,启动provider服务和Consumer服务,请求
- 很多用户在网站上会糊弄填写一个电子信箱,请问有什么办法可以阻止这种行为?我们通常用两种方法来进行判断:第一种,设定只有形如aspxhome@
- <form name="form5" id="form5" me
- 本文实例为大家分享了python实现图书借阅系统的具体代码,供大家参考,具体内容如下部分代码:from flask import Flask
- 一、测试常用规则一个测试单元必须关注一个很小的功能函数,证明它是正确的;每个测试单元必须是完全独立的,必须能单独运行。这样意味着每一个测试方
- 最近发现自己的博客打开很慢,通过ie浏览器打开速度还可以,使用任何第三方浏览器打开都超级慢,以为是HTML代码元素导致,一番比对后没有发现不
- 如何生成任意n阶的三对角矩阵数学作业要求实现共轭梯度法的算法。题目中的矩阵A是n=400/500/600的三对角矩阵。在网上查阅资料未果后,
- 一、生成器1、生成器定义在Python中,一边循环一边计算的机制,称为生成器:generator2、生成器存在的意义列表所有数据都在内存中,
- 前言1、防抖(debounce):触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间举例:就好像在百
- 安装SQL Server2019详细教程1、官网下载SQL Server 2019 Developer: Developer下载地址&nbs
- 一、概率知识基础1.概率概率就是某件事情发生的可能性。2.联合概率包含多个条件,并且所有条件同时成立的概率,记作:P(A, B) = P(A
- bootstrap-table简介•1.1、bootstrap table简介及特征: &nb
- 最近在学习tensorflow框架,在ubuntu下用到python的一个ide --spyder,以下是常用快捷键Ctrl+1:注释/撤销