Python实战笔记(五) 手写一个简单搜索引擎

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: Python实战笔记(五) 手写一个简单搜索引擎

前言


这篇文章,我们将会尝试从零搭建一个简单的新闻搜索引擎

当然,一个完整的搜索引擎十分复杂,这里我们只介绍其中最为核心的几个模块


分别是数据模块、排序模块和搜索模块,下面我们会逐一讲解,这里先从宏观上看一下它们之间的工作流程

095fbb86b83ad05159d04ed989c6f1f4_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dzbXJ6eA==,size_16,color_FFFFFF,t_70#pic_center.jpg


正文


1、数据模块


数据模块的主要作用是爬取网络上的数据,然后对数据进行清洗并保存到本地存储


一般来说,数据模块会采用非定向爬虫技术广泛爬取网络上的数据,以保证充足的数据源



但是由于本文只是演示,所以这里我们仅会采取定向爬虫爬取中国社会科学网上的部分文章素材


而且因为爬虫技术我们之前已经讲过很多,这里就不打算细讲,只是简单说明一下流程



首先我们定义一个数据模块类,名为 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


然后调用 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


接着调用 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 |
|---------------------------------------------------|
|  ......  |   ......   |    ......    |   ......   |
|---------------------------------------------------|


2、索引模块


索引模型的主要作用是构建倒排索引 (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
        }, ...]
    },
    ...
}


3、搜索模块


在得到原始数据和构建好倒排索引后,我们就可以根据用户的输入查找相关的内容


具体怎么做呢?

  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


4、完整代码


完整代码和相关说明文档请移步我的 Github 仓库



目录
相关文章
|
16天前
|
数据采集 存储 JSON
Python网络爬虫:Scrapy框架的实战应用与技巧分享
【10月更文挑战第27天】本文介绍了Python网络爬虫Scrapy框架的实战应用与技巧。首先讲解了如何创建Scrapy项目、定义爬虫、处理JSON响应、设置User-Agent和代理,以及存储爬取的数据。通过具体示例,帮助读者掌握Scrapy的核心功能和使用方法,提升数据采集效率。
60 6
|
16天前
|
设计模式 前端开发 数据库
Python Web开发:Django框架下的全栈开发实战
【10月更文挑战第27天】本文介绍了Django框架在Python Web开发中的应用,涵盖了Django与Flask等框架的比较、项目结构、模型、视图、模板和URL配置等内容,并展示了实际代码示例,帮助读者快速掌握Django全栈开发的核心技术。
103 45
|
7天前
|
数据采集 机器学习/深度学习 人工智能
Python编程入门:从基础到实战
【10月更文挑战第36天】本文将带你走进Python的世界,从基础语法出发,逐步深入到实际项目应用。我们将一起探索Python的简洁与强大,通过实例学习如何运用Python解决问题。无论你是编程新手还是希望扩展技能的老手,这篇文章都将为你提供有价值的指导和灵感。让我们一起开启Python编程之旅,用代码书写想法,创造可能。
|
9天前
|
数据库 Python
异步编程不再难!Python asyncio库实战,让你的代码流畅如丝!
在编程中,随着应用复杂度的提升,对并发和异步处理的需求日益增长。Python的asyncio库通过async和await关键字,简化了异步编程,使其变得流畅高效。本文将通过实战示例,介绍异步编程的基本概念、如何使用asyncio编写异步代码以及处理多个异步任务的方法,帮助你掌握异步编程技巧,提高代码性能。
26 4
|
8天前
|
机器学习/深度学习 数据可视化 数据处理
Python数据科学:从基础到实战
Python数据科学:从基础到实战
17 1
|
9天前
|
机器学习/深度学习 JSON API
Python编程实战:构建一个简单的天气预报应用
Python编程实战:构建一个简单的天气预报应用
23 1
|
12天前
|
前端开发 API 开发者
Python Web开发者必看!AJAX、Fetch API实战技巧,让前后端交互如丝般顺滑!
在Web开发中,前后端的高效交互是提升用户体验的关键。本文通过一个基于Flask框架的博客系统实战案例,详细介绍了如何使用AJAX和Fetch API实现不刷新页面查看评论的功能。从后端路由设置到前端请求处理,全面展示了这两种技术的应用技巧,帮助Python Web开发者提升项目质量和开发效率。
27 1
|
12天前
|
缓存 测试技术 Apache
告别卡顿!Python性能测试实战教程,JMeter&Locust带你秒懂性能优化💡
告别卡顿!Python性能测试实战教程,JMeter&Locust带你秒懂性能优化💡
28 1
|
17天前
|
数据可视化 开发者 Python
Python GUI开发:Tkinter与PyQt的实战应用与对比分析
【10月更文挑战第26天】本文介绍了Python中两种常用的GUI工具包——Tkinter和PyQt。Tkinter内置于Python标准库,适合初学者快速上手,提供基本的GUI组件和方法。PyQt基于Qt库,功能强大且灵活,适用于创建复杂的GUI应用程序。通过实战示例和对比分析,帮助开发者选择合适的工具包以满足项目需求。
62 7
|
17天前
|
数据采集 Web App开发 前端开发
Python爬虫进阶:Selenium在动态网页抓取中的实战
【10月更文挑战第26天】动态网页抓取是网络爬虫的难点,因为数据通常通过JavaScript异步加载。Selenium通过模拟浏览器行为,可以加载和执行JavaScript,从而获取动态网页的完整内容。本文通过实战案例,介绍如何使用Selenium在Python中抓取动态网页。首先安装Selenium库和浏览器驱动,然后通过示例代码展示如何抓取英国国家美术馆的图片信息。
37 6