尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于Scrapy+ElasticSearch+Django的全文搜索引擎构建实战

基于Scrapy+ElasticSearch+Django的全文搜索引擎构建实战 简介搜索引擎是信息检索的核心技术其底层依赖倒排索引实现高效查询。ElasticSearch作为分布式搜索服务器通过分词和BM25相关性算法提供高质量检索Scrapy负责从目标站点采集数据Django则作为Web层构建搜索交互界面。三者结合可快速搭建一套完整的小型全文搜索引擎广泛应用于毕业设计、垂直搜索以及教学演示。本文从架构设计出发详细介绍环境搭建、爬虫编写、索引设计、查询DSL开发及Django集成等关键环节并给出常见问题排查方案帮助开发者快速掌握搜索技术栈的实战落地。 又到了一年一度毕业设计选题的时候。如果你正在纠结做一个什么样的计算机专业课题同时又想兼顾难度、可展示性和答辩通过率那我强烈建议你考虑这个方向基于ScrapyElasticSearchDjango的小型全文搜索引擎。这套技术栈玩明白等于一箭三雕——爬虫、搜索引擎、Web开发全打通了任何一个环节拿出来都能单独讲上十分钟而且整套系统跑起来以后效果非常直观输入关键词、点搜索、结果秒回演示的时候观感极佳。这篇文章我会按实际做项目的顺序把从架构设计、环境安装、爬虫编写、索引构建到Django对接、搜索接口开发再到常见问题排查的完整链路都过一遍。内容偏实操向适合计算机相关专业的学生参考也适合想快速入门ElasticSearch生态的开发者阅读。我自己在这个项目上踩过不少坑很多细节是官方文档不会告诉你的我会尽量全部写出来。1. 项目整体设计与技术选型思路1.1 为什么你的毕设需要一个搜索引擎先回答一个根本问题为什么不用MySQL的LIKE查询去实现搜索这是很多人在开题时会遇到的质疑。从功能上讲LIKE %%keyword%%确实能实现模糊匹配但它的缺陷是致命的。第一性能差一旦数据量上万全表扫描加模糊匹配会让数据库响应变成天文数字第二相关性差它只能做子串匹配无法理解“毕业设计”和“毕业论文”这种语义上的相关性第三没有分词能力中文场景下你搜“搜索引擎”的时候永远匹配不到“小型全文搜索引擎”这类包含关键词词素的文本。这时候就需要引入真正的搜索引擎解决方案。ElasticSearch底层使用倒排索引Inverted Index简单理解就是把文档拆成词条建立“词条→文档列表”的映射关系。我查一个词的时候直接定位到包含它的所有文档这比逐行扫描快几个数量级。它的分词能力还能让搜索匹配到同义词、词形变化配合BM25相关性算法能把最匹配的结果排在最前面。这就是为什么我坚持在这个项目里用ElasticSearch作为检索核心而不是图省事直接用数据库。用生活化类比来解释这三件套的配合我觉得最贴切的是开一家书店。Scrapy是你的采购员负责从各个渠道目标网站把书收回来ElasticSearch是仓库管理员把每本书分类、贴标签、上架建好一套高效的查书目录Django就是前台店员顾客过来说“我要找毕业设计相关的书”店员通过目录快速定位、把书拿给顾客。三个角色各司其职缺一不可。1.2 三件套分工与整体数据流整个项目的数据流转是这样的Scrapy爬虫从目标站点抓取网页内容经过Spider解析、Item Pipeline清洗把结构化数据写入ElasticSearch索引Django作为Web服务层接收用户的搜索请求构造ElasticSearch查询DSL拿到搜索结果后渲染到前端页面展示给用户。如果还要扩展可以加入MySQL存储爬虫任务记录、搜索历史、用户行为等关系型数据用Django ORM管理形成“关系型数据库搜索引擎”双存储架构。之所以选这三件套而不是其他方案核心原因是它们各自在所属领域都是事实标准。Scrapy是Python生态最成熟的爬虫框架支持异步并发、中间件、Pipeline资料多到随便搜ElasticSearch不用说了搜索引擎界的No.1大厂生产环境都在用Django开发效率高自带Admin后台和ORM特别适合快速搭建带管理界面的Web应用。这三者技术栈统一在Python体系下对毕设项目来说学习和维护成本可控而且面试或者答辩的时候每一层都能讲出深度。2. 环境准备与核心组件部署要点2.1 Windows下ElasticSearch安装的完整攻略很多新手一上来就被ElasticSearch的环境配置劝退尤其是Windows环境问题格外多。我先把最干净的一条安装路径写出来你照着走基本不会出大问题。第一步确认JDK环境。ElasticSearch 7.x版本要求JDK 8以上建议直接用JDK 118.x版本内置了JDK其实可以不用单独配置。但为了稳定和兼容我建议用7.17.x系列配JDK 11这是当时生产环境里验证最多的组合。第二步下载安装包。去ElasticSearch官网下载对应系统的ZIP压缩包注意一定要下载和你Java版本匹配的版本。下载慢的话可以找国内镜像源这里不展开。直接把ZIP解压到一个纯英文路径下切记路径不要有中文、空格否则启动大概率报错。第三步修改配置文件。编辑config/elasticsearch.yml最常用的是这几项cluster.name: my-es node.name: node-1 network.host: 127.0.0.1 http.port: 9200如果只是本机测试network.host保持默认或者设成127.0.0.1就够了不要随便改成0.0.0.0这会暴露到局域网安全上没必要。第四步调整JVM内存。编辑config/jvm.options把-Xms1g和-Xmx1g改成适合你机器的大小。笔记本建议512m到1g之间别贪多否则ES占太多内存Django和爬虫跑起来会卡。第五步启动。在命令行进入ES根目录执行bin\elasticsearch.bat看到started日志就说明启动成功。浏览器访问http://localhost:9200返回一段JSON里面包含cluster_name和version信息就说明ES正常服务了。装完ES之后建议装一个可视化工具查看索引和数据。我常用的方式有两种一是Chrome浏览器里的ElasticSearch Head类插件二是单独跑一个Cerebro服务。如果是毕设演示Cerebro界面更漂亮操作也更直观如果只想快速看索引列表head插件就够了。2.2 Django与Scrapy项目初始化ES环境跑通之后开始创建Django和Scrapy的工程骨架。建议全程使用虚拟环境避免依赖冲突。# 创建虚拟环境 python -m venv venv # Windows激活 venv\Scripts\activate # 安装依赖 pip install django scrapy elasticsearch elasticsearch-dsl # 创建Django项目 django-admin startproject search_engine cd search_engine python manage.py startapp search_app # 在另一个目录创建Scrapy项目 cd .. scrapy startproject search_spider这里有个实操细节Django项目和Scrapy项目不要放在同一个目录下。虽然可以放在一起但Scrapy的配置文件和Django的配置文件容易互相干扰尤其是settings.py重名的问题调试起来让人头大。我是把search_engine和search_spider建为同级目录各自管理各自的配置后面通过API或者ES索引来交换数据解耦得干干净净。Django项目创建后记得在settings.py里的INSTALLED_APPS注册search_app然后执行迁移命令生成内置表python manage.py migrate python manage.py createsuperuserDjango的Admin后台是毕设展示的加分项我会在后台注册一个SearchRecord模型用来记录每次搜索的关键词、时间、结果数答辩的时候打开后台给老师看“用户搜了什么、搜到多少结果”比嘴讲有说服力多了。3. Scrapy爬虫实现与数据采集3.1 Item定义与Pipeline清洗爬虫部分的核心是先把数据结构定义清楚然后编写解析逻辑最后通过Pipeline完成数据清洗。我拿一个典型的新闻类网站举例爬取文章的标题、链接、正文、作者、发布时间。items.py里定义Itemimport scrapy class ArticleItem(scrapy.Item): title scrapy.Field() url scrapy.Field() content scrapy.Field() author scrapy.Field() publish_time scrapy.Field() crawl_time scrapy.Field()为什么参数定义要这么细致因为ElasticSearch建立索引时每个字段都要指定类型和分析器Item字段定义得越清晰后面写Mapping就越方便。而且Scrapy会通过Item的字段结构自动检查数据完整性字段缺失时能在Pipeline里及时发现。Pipeline最核心的价值在清洗。网页正文里往往混着HTML标签、多余的空格、特殊字符全部塞进ES会严重影响搜索质量和展示效果。我在Pipeline里做这几件事import re import html def clean_content(content): # 去掉HTML标签 content re.sub(r[^], , content) # 反转义HTML实体 content html.unescape(content) # 压缩连续空白 content re.sub(r\s, , content).strip() return content这属于最基础的清洗逻辑但就是这三行代码让最终索引里的content字段干净了很多搜索高亮的时候也不会出现标签残留导致的页面错乱。3.2 动态页面与iframePlaywright集成方案如果你爬的站点是纯静态页面Scrapy原生就能搞定。但现在的网站普遍是前端渲染数据要等JavaScript执行完才能看到甚至内容嵌在iframe里Scrapy默认的HTTP请求根本拿不到。这是全网都在问的高频问题。我在项目里总结了两套打法按优先级排列。第一套打法也是我最推荐的先找接口。打开浏览器开发者工具切到Network面板重新加载页面筛出XHR或者Fetch类型的请求看看页面数据是不是来自某个JSON接口。如果是直接用Scrapy请求这个接口解析JSON不要渲染网页性能高一个量级。这个方法能解决至少70%的动态页面。第二套打法接口找不到或者加密太复杂再用浏览器渲染。比较成熟的方案是集成Playwright。Scrapy配合scrapy-playwright中间件可以在Spider里异步渲染页面再解析。关键配置是# settings.py DOWNLOAD_HANDLERS { http: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, https: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, } TWISTED_REACTOR twisted.internet.asyncioreactor.AsyncioSelectorReactorSpider里通过meta参数触发渲染def parse_detail(self, response): yield scrapy.Request( urldetail_url, callbackself.parse_item, meta{playwright: True} ) def parse_item(self, response): page response.meta[playwright_page] # 如果需要切换iframe iframe page.frame_locator(iframe#iframe_id) content iframe.locator(div.content).inner_text()处理iframe的关键词是frame_locator它会定位到iframe内部的元素不用担心只在同一个页面上下文里找。这里有个真实的心得Playwright渲染模式非常耗资源千万不要对列表页里每个详情页都开渲染性能撑不住。我的策略是列表页用普通请求详情页先尝试普通请求如果response里没有正文再重试一次带Playwright渲染的请求。3.3 增量采集去重与分布式扩展毕设项目的数据量虽然不大但采集任务往往要跑好几天。为了不重复爬取必须做去重。Scrapy自带的去重是基于请求URL的在同一个爬虫任务内有效但重启任务后就会失效。我的做法是升级到基于ElasticSearch的去重Pipeline里在写入前先查询一下索引里是否已经存在相同URL的文档存在就丢弃。def process_item(self, item, spider): exists es.exists(indexarticle, iditem[url]) if not exists: self.buffer.append(dict(item)) return item这里用URL作为ES文档的_id天然避免重复。等到数据量真的到了一定规模可以引入scrapy-redis把请求队列放到Redis里多个爬虫节点共享同一套待爬队列实现分布式采集。毕设阶段不一定用得上但把这个扩展方向写在论文里体现的是你对系统扩展性的理解答辩时是个亮点。4. ElasticSearch索引设计与数据同步4.1 Mapping设计字段类型与中文分词把数据写入ES之前第一件事是设计索引的Mapping。Mapping就相当于数据库的表结构决定了字段怎么存储、怎么分词、怎么排序。很多人在这一步偷懒直接靠ES自动映射结果搜索中文时各种不对劲后面再改Mapping还得重建索引非常麻烦。我的索引叫articleMapping设计如下{ mappings: { properties: { title: { type: text, analyzer: ik_max_word, fields: { keyword: {type: keyword} } }, content: { type: text, analyzer: ik_max_word }, url: {type: keyword}, author: { type: keyword, null_value: 未知 }, publish_time: { type: date, format: yyyy-MM-dd HH:mm:ss }, crawl_time: { type: date, format: strict_date_optional_time||epoch_millis } } } }字段类型有几个设计要点。字符串字段如果不需要分词搜索就用keyword比如URL、作者名需要全文检索的字段比如标题、正文用text并指定analyzer。中文搜索必须用中文分词器否则ES默认的标准分析器会把中文按单个字切开搜“搜索引擎”时可能被拆成“搜”“索”“引”“擎”四个字相关性很差。IK分词器是中文搜索事实标准安装方式下载与ES版本匹配的ik分词器zip解压到ES的plugins目录重启ES即可。ik_max_word和ik_smart两种分词模式的区别也要理解。ik_max_word会尽可能多地拆分词语比如“中华人民共和国”会被拆成“中华人民共和国”“中华人民”“中华”“人民”等索引更全但更占空间ik_smart只做最粗粒度的拆分索引更精简。搜索时用ik_smart索引时用ik_max_word是比较经典的配置组合。4.2 Pipeline批量写入与索引管理Scrapy的Pipeline如果每抓到一条就调用一次ES的index接口速度会非常慢性能瓶颈明显。高并发下ES的批量写入接口要远优于单条写入。我在Pipeline里用一个缓冲区攒够100条或者每隔5秒批量刷一次。from elasticsearch.helpers import bulk class ElasticsearchPipeline(object): def __init__(self, bulk_size100, flush_interval5): self.bulk_size bulk_size self.buffer [] self.last_flush_time time.time() def process_item(self, item, spider): self.buffer.append(dict(item)) if (len(self.buffer) self.bulk_size or time.time() - self.last_flush_time self.flush_interval): self.flush() return item def flush(self): if not self.buffer: return actions [ {_index: article, _id: doc[url], _source: doc} for doc in self.buffer ] bulk(self.es, actions) self.buffer.clear() self.last_flush_time time.time()这个批量写入设计能明显提升爬虫的吞吐量。实测下来单条写ES大概每秒只能写几十条改成bulk之后能到每秒几百上千条数据量大的情况下这个差异非常明显。另外有个小技巧bulk接口的批量大小不是越大越好过大的请求会导致ES内存压力上升100到500条是比较合适的区间。索引管理方面建议写一个独立的脚本做索引初始化包括删除旧索引、创建新索引、设置Mapping。因为Mapping一旦建立就不能改字段类型需要调整时只能重建索引所以务必在数据写入前把Mapping确定好。想查看当前索引状态用GET /_cat/indices?v命令能直观看到文档数量和存储大小。4.3 搜索DSLmust、must_not、should怎么用ElasticSearch的查询DSL是这套系统的核心其中bool查询组合是最常用的。官方的must、must_not、should这三兄弟很多人初看就懵我用一个点外卖的类比讲清楚。must是必须满足的条件相当于“必须加辣”不满足的直接排除同时还参与相关度评分。must_not是必须排除的条件相当于“不要香菜”只要命中就直接过滤掉但不影响评分。should是加分项相当于“有优惠券更好”命中了会增加相关度得分但不是硬性要求。要搜索的正文内容放must要排除的垃圾作者放must_not标题命中可以放should增加权重。{ query: { bool: { must: [ {match: {content: 毕业设计}} ], must_not: [ {term: {author: admin}} ], should: [ {match: {title: 搜索引擎}} ] } }, from: 0, size: 10, highlight: { fields: { content: {pre_tags: [em], post_tags: [/em]} } } }这段DSL的含义是搜索正文包含“毕业设计”的文档排除作者为admin的文档如果标题里还包含“搜索引擎”则排序更靠前同时返回内容字段的高亮结果。高亮功能是搜索引擎的标配用户在页面上看到的红色关键词就是通过highlight实现的。我测过很多种搜索match查询对中文场景最友好因为它会先对查询词做分词再匹配能容忍用户输入的微小差异。另外ElasticSearch 8.11之后推出了ESQL可以用类SQL的语法直接查询比如FROM article | WHERE MATCH(content, 毕业设计) | STATS COUNT(*)。但毕设和线上项目我还是建议用DSL因为DSL表达能力更强也是生态里主流的写法。5. Django业务层与搜索接口开发5.1 Django如何连接ES并封装检索服务Django本身不提供ES的官方ORM所以连接ES的方式一般是用官方Python客户端elasticsearch封装一个服务层。建一个services.py把搜索逻辑全部收拢在这里视图层只负责参数解析和渲染职责清晰。from elasticsearch import Elasticsearch client Elasticsearch(http://localhost:9200) def search_articles(keyword, page1, page_size10): body { from: (page - 1) * page_size, size: page_size, query: { bool: { must: [{match: {content: keyword}}], should: [{match: {title: keyword}}] } }, highlight: { fields: {content: {pre_tags: [em], post_tags: [/em]}} } } resp client.search(indexarticle, bodybody) hits resp[hits][hits] total resp[hits][total][value] return total, hits这里有一个查询权重上的考量同时搜content和title时用should给title加权重让标题匹配的结果排在前面。这符合用户预期——标题里出现关键词通常比正文里出现更相关。分页用from和size实现不要用Django的Paginator去分页ES的返回结果因为ES本身就能做分页你只需要把页数和每页大小传进去。为什么用match而不是term因为term是精确匹配对中文分词不友好用户输入“java”搜不到全角“”用户体验很差。match会自动分词、自动匹配同义词变体更适合全文搜索场景。5.2 视图函数、URL与前端页面实现视图层逻辑很简单从GET请求里取q参数调用搜索服务把结果传给模板渲染。from django.shortcuts import render from .services import search_articles from .models import SearchRecord def search(request): keyword request.GET.get(q, ).strip() page int(request.GET.get(page, 1)) context {keyword: keyword, results: [], total: 0} if keyword: total, hits search_articles(keyword, page) results [] for hit in hits: src hit[_source] highlight hit.get(highlight, {}).get(content, []) src[content_highlight] highlight[0] if highlight else src[content] results.append(src) context.update({results: results, total: total}) SearchRecord.objects.create(keywordkeyword, result_counttotal) return render(request, search.html, context)搜索记录写入Django数据库的这一步非常关键。它不仅为毕设演示提供了“用户搜索行为”的数据支撑也是Django ORM和ES共存共用的典型场景实时性要求高的搜索走ES历史数据分析和后台管理走MySQL。两个数据源各司其职这个设计在论文里一定要重点写。URL配置from django.urls import path from . import views urlpatterns [ path(search/, views.search, namesearch), ]前端页面我建议走简洁路线一个居中的搜索框下面列出结果标题、内容摘要、来源链接。结果标题带超链接指向原文内容摘要里自动展示高亮的em标签。千万不要把前端做得太重搜索引擎的项目亮点在后端的数据处理和检索技术上不是在前端样式上。5.3 Django ORM与ES的配合删除与同步项目里有个功能管理员在后端删除一条爬取的垃圾文章。这里必须处理双数据源同步的问题。ES里的文档和MySQL里的记录要一起删否则数据就对不上了。from elasticsearch.exceptions import NotFoundError def delete_article(request, doc_id): # 先删除ES里的文档 try: client.delete(indexarticle, iddoc_id) except NotFoundError: pass # 文档不存在也继续删数据库记录 # 再删除MySQL里的记录 ArticleMeta.objects.filter(urldoc_id).delete() return redirect(/admin/search_app/articlemodel/)这里的先后顺序是有讲究的先删ES文档再删MySQL记录。因为ES删除失败的概率相对更高如果先删了MySQL后ES报错数据就永久丢失了先删ES即使MySQL删除失败也只是残留一条不完整的记录后面的定时任务还能兜底清理。这种“双删”问题在微服务场景里很常见提前在这里体会一下对理解分布式系统的数据一致性有帮助。另一个常见场景是“重建索引”。如果Mapping改动了需要把MySQL里的存量数据重新同步到ES。可以写一个Django management command遍历ArticleMeta表调用ES的index接口逐条写入。这就是Django ORM的用武之地——从关系型数据库里读数据转成ES文档写入搜索索引。6. 常见问题与排查技巧实录6.1 环境与启动类问题速查ES在Windows上的问题十个有八个出在启动阶段。我把高频问题汇总成一个速查表这个表我建议你直接截图保存答辩演示前照着检查一遍。现象可能原因解决方案双击启动bat闪退JDK版本不匹配确认JDK 8/11运行java -version查看报错“data too large”内存不足修改jvm.options降低-Xmx确保内存不低于512m9200端口被占用其他进程占用端口netstat -ano | findstr 9200找到PID后结束进程启动后无法访问9200network.host配置问题确保配置为127.0.0.1并检查防火墙中文索引报错未安装ik分词器下载对应版本ik并解压到plugins目录后重启除了ESDjango项目还有一个高频坑静态文件404。出现这个问题的原因是DEBUGFalse时Django不再自动提供静态文件服务。毕设演示时如果发现页面样式丢失要么临时把DEBUG设回True要么用whitenoise库把静态文件托管起来。6.2 爬虫与数据采集类问题爬虫阶段最坑的问题就是目标网站改版或者加了反爬手段。我的经验是处理反爬不要一上来就堆代理池而是先做基础配置# settings.py USER_AGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 DOWNLOAD_DELAY 0.5 COOKIES_ENABLED FalseDOWNLOAD_DELAY设为0.5秒能有效降低被封风险同时又不至于太慢。COOKIES_ENABLED设为False可以避免Scrapy默认的cookie处理器影响请求头。这些配置对大部分网站已经够用了。如果被识别为爬虫优先检查你的User-Agent是否太老或者缺少常用的Accept-Language头。另一个常见问题是中文乱码。很多网站返回的是GBK编码而Scrapy默认按UTF-8解码。解决方案是在Response的encoding属性上做手脚或者用response.text前先设置response response.replace(encodinggbk)6.3 搜索与匹配类问题搜索不到结果最常见的原因是分词器没配好。如果你用的标准分析器中文会被拆成单个字搜“毕业设计”的时候索引里存的是“毕”“业”“设”“计”四个字每个字的文档ID列表都很长相关性得分很低排在前面的往往是无关内容。解决方法是安装IK分词器并在Mapping里显式指定analyzer: ik_max_word。还有一个容易忽略的点ES的match查询默认是OR逻辑也就是说搜“毕业设计 搜索引擎”只要命中其中一个词就会返回。如果你希望必须两个词都命中需要指定operator: and。这个细节在用户搜长句的时候非常影响体验我建议对用户搜索词先做一次长度判断超过4个字就用and逻辑保证结果更精准。高亮不生效也是常见问题。注意highlight的字段必须与分析器一致而且pre_tags和post_tags如果用了HTML标签在Django模板渲染时要通过|safe过滤器输出否则标签会被转义成普通文本页面上一堆乱码标签。最后再说两句这个项目前前后后花了我大概三周的晚上和周末时间。第一天最痛苦的不是写代码而是各种环境问题叠加JDK版本不对、ES内存不足、爬虫被反爬、Mapping重建了三次。但正是这些坑让我对搜索引擎的底层原理有了真正的体感。答辩的时候我直接打开ES的_cat/indices接口展示实时索引状态再打开爬虫终端演示增量爬取最后切到搜索页面输入关键词查结果——整套流程下来老师非常满意。如果你也在做这个方向我的建议是不要贪多先把Scrapy采集、ES索引、Django查询这条主链路跑通再按自己的兴趣往上加功能。把基础链路做扎实比堆砌一堆花哨但没有实际用途的功能要有价值得多。最后一个小技巧本地开发时用DEBUGTrue但是答辩演示前一定要切到生产模式提前处理好静态文件不然现场开不了页面会很尴尬。本文还有配套的精品资源点击获取
返回列表