ARTICLE DETAIL

资讯详情

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

基于Scrapy和Elasticsearch与Django打造全文搜索引擎

基于Scrapy和Elasticsearch与Django打造全文搜索引擎 简介本资源是一套完整的毕业设计项目实现方案面向计算机专业本科生及Web开发初学者聚焦小型全文搜索引擎的端到端构建有效整合网络爬虫、倒排索引与Web交互三大核心能力。项目基于Scrapy实现定向网页抓取ElasticSearch提供高性能全文检索与分词支持Django搭建响应式搜索界面与后端服务覆盖从数据采集、存储建模到用户查询的全链路实践。压缩包含2000个文件主体为1605个Python脚本含Scrapy爬虫、Django视图与ES交互逻辑、122个HTML模板含搜索页、结果页、分页组件及89个JS前端交互文件辅以CSS样式文件如select2.css、widgets.css等保障界面可用性整体大小88.15MB。目前已有135人学习下载资源结构清晰、模块解耦明确附带完整可运行代码、索引映射定义与基础配置说明适合毕设快速启动、技术栈串联训练及搜索引擎原理实操验证。 每年到毕设选题季我都看到大量同学在同一个问题上反复纠结题目怎么选才既不显得太水又能完整展示自己的工程能力最好答辩的时候还能当场跑出效果。如果你主攻Python那我强烈建议你考虑一下这个方向基于Scrapy Elasticsearch Django 做一个小型全文搜索引擎。这个组合乍看有点“技术全家桶”的意思但实际上每个组件的边界非常清晰Scrapy负责把网页数据抓下来Elasticsearch负责把数据建成倒排索引并提供搜索能力Django负责把搜索结果变成网页和接口。三者拼起来就是一个完整的“爬取—索引—检索”闭环从演示效果到答辩讲稿你都有真东西可讲。这篇文章我会把整个项目从零到一的完整链路拆开讲包括技术选型的理由、环境搭建的坑、爬虫怎么写、索引怎么设计、搜索接口怎么调、前端怎么展示高亮结果以及最后部署上线的注意事项。无论你是想拿它当毕业设计还是单纯想练手一个全栈搜索引擎项目这篇都值得从头到尾过一遍。我会把“为什么这么做”也讲清楚而不是只丢给你一堆能跑但不知道为什么的代码。1. 项目定位与技术选型为什么非得是这三件套1.1 这个项目到底在解决什么问题先说清楚“全文搜索引擎”和普通数据库搜索有什么区别。很多人一上来会用MySQL的LIKE查询做搜索比如WHERE title LIKE %关键词%这在数据量小的时候没毛病但一旦数据到几万条以上你会发现两个问题一是慢LIKE语句基本不走索引全表扫一遍非常吃力二是质量差它只能做字符串完全包含匹配不关心词义、不关心词序、不给结果排序。比如用户搜“Python机器学习”它只会机械地匹配包含这串字符的文档而搜不到“Python的机器学习入门指南”这种更自然的表达。而Elasticsearch这类搜索引擎的核心是“倒排索引”。你可以把它理解成书最后面的关键字索引解析每一篇文档把内容拆成词条然后记录“哪个词出现在哪篇文章里”。查询的时候不是扫全表而是直接查词条对应的文档列表再用相关度算法比如BM25给结果打分排序。这就是全文搜索引擎和普通数据库LIKE查询的本质区别。所以这个毕设项目的定位很简单用爬虫抓取一批垂直领域的内容比如技术文章、新闻、课程介绍清洗干净后导入Elasticsearch做索引再通过Django封装出搜索页面和API最终实现一个带高亮、带分页、带排序的小型垂直搜索引擎。它不是一个通用的大型搜索引擎而是一个“麻雀虽小、五脏俱全”的完整系统用来展示你对全链路技术的理解刚好合适。1.2 三件套的分工逻辑Scrapy、Elasticsearch、Django这三个东西在项目里的角色可以这样理解Scrapy数据采集层。负责从目标网站抓取原始页面提取标题、正文、发布时间、作者等信息做清洗后输出结构化数据。Scrapy是Python生态里最成熟的爬虫框架自带请求调度、去重、中间件、Item Pipeline适合做规模可控的抓取任务。它的异步并发模型能有效提升抓取速度但又不至于像手写多线程那样难维护。Elasticsearch索引与检索引擎。负责接收Scrapy清洗后的数据建立倒排索引对外提供全文检索、高亮、过滤、聚合等能力。它是整个搜索引擎项目的核心引擎也是你答辩时最值得展开讲的部分。为什么选ES而不是Solr因为ES的生态更活跃资料多RESTful接口清晰跟Python配合也顺。DjangoWeb应用层。负责搭建搜索网站和提供API接口。用户在前端输入关键词请求到DjangoDjango再转调Elasticsearch的查询接口拿到结果后渲染到页面上。Django自带ORM、模板、表单、后台管理能快速搭出一个像模像样的搜索站点减少你在Web层花费的时间。为什么不直接用SphinxSphinx虽然老牌但配置复杂、中文分词支持弱社区活跃度也远不如ES。为什么不直接用LuceneLucene只是一个Java库你还需要自己封装服务、处理并发、设计接口工作量直接翻倍。相比之下“Scrapy ES Django”是Python毕设项目里性价比最高的组合每个组件都有明确职责又能清晰展示技术深度。2. 环境准备与基础搭建版本选错后面全是眼泪2.1 版本组合怎么选这一步我放在最前面讲因为版本问题在整条链路里藏得最深一旦埋下雷后面每一步都可能踩上。很多同学上手就装最新版结果ES 8.x默认开启安全认证Django这边连es客户端都要配用户名密码Scrapy新版又对Python版本有要求环环相扣最后花了半天时间全耗在环境上。我实际用下来比较稳妥的版本组合是组件推荐版本说明Python3.10 或 3.11兼容性最稳别用3.7以下Django4.2 LTS长期支持资料多Scrapy2.11.x稳定版本scrapy-playwright最新版需要配合Playwright浏览器Elasticsearch7.17.x不用8.x是因为7.x没有强制安全认证适合毕设elasticsearch-py7.17.x必须和ES主版本保持一致别装8.x的客户端去连7.xIK分词器7.17.x必须和ES版本完全一致否则加载不出来这里有个容易被忽略的核心原则Elasticsearch主版本、elasticsearch-py客户端版本、IK分词器版本三者必须严格对齐。比如ES是7.17.3IK分词器也必须下载7.17.3的包elasticsearch-py要用7.x版。你装一个ES 8.x却用7.x的客户端连接直接报版本不兼容IK版本对不上ES启动时插件加载直接失败。版本问题看似基础但绝对是新手遇到频率最高的坑。2.2 Elasticsearch安装与启动全平台踩坑实录Elasticsearch的安装本身不复杂但操作系统不同坑的位置不一样。Linux下的操作下载7.17.x的tar包解压后不要直接用root用户启动ES出于安全策略会拒绝root直接运行建议创建一个es用户把目录属主改过去然后切换用户启动。还需要把Linux系统参数vm.max_map_count调到至少262144否则启动会报“max virtual memory areas vm.max_map_count [65530] is too low”这个错误很经典用sysctl -w vm.max_map_count262144可以临时解决永久生效要写到/etc/sysctl.conf。启动命令是# 解压 tar -zxvf elasticsearch-7.17.3-linux-x86_64.tar.gz # 创建用户并授权 useradd es chown -R es:es elasticsearch-7.17.3 # 切换用户启动 su es cd elasticsearch-7.17.3/bin ./elasticsearch -dWindows下的操作下载zip包解压后直接双击bin/elasticsearch.bat就能起来。但要注意三点第一解压路径不要带中文和空格有的机器上带中文路径会启动异常第二ES自带了JDK只要JAVA_HOME变量没有强制指向别的JDK它默认用自己的内置JDK不用额外装Java第三如果机器内存较小启动前先去config/jvm.options把-Xms和-Xmx从默认的1g调小或调大比如-Xms512m -Xmx512m否则占用内存太猛。启动后访问http://localhost:9200能看到一串带cluster_name、version信息的JSON就说明ES起来了。如果你想确认健康状态访问http://localhost:9200/_cluster/health返回status : green表示集群健康。ES的9200端口就是HTTP REST接口后面Django会走这个口子9300是集群节点通信端口单机部署不需要管它。2.3 Django和Scrapy项目骨架快速搭建环境到位后先建两个子项目。我建议在同一个目录下分成两个工程一个是后端Web工程一个是爬虫工程代码目录清晰分开后续维护不迷路。# 创建Django工程 django-admin startproject search_site cd search_site python manage.py startapp search_api # 回到根目录创建Scrapy工程 scrapy startproject article_spider cd article_spider scrapy genspider article example.comDjango这边需要装elasticsearch-py和djangorestframework如果你决定用DRF写APIScrapy这边需要装scrapy-playwright和对应的浏览器内核。安装命令大致这样pip install django4.2.* djangorestframework elasticsearch7.17.* pip install scrapy scrapy-playwright playwright install chromium这里有同学会问为什么Scrapy要配Playwright因为现在很多目标站点都是前后端分离的SPA应用页面里的正文是通过JavaScript异步加载的甚至内容嵌在iframe里Scrapy默认的HTTP请求拿到的HTML是空的。加一个Playwright就是为了解决这种动态渲染问题后面的章节我会专门展开。3. 爬虫采集层Scrapy的完整实战3.1 先写一个基础Spider跑通抓取链路写爬虫之前先明确一件事我们做的搜索引擎抓取对象尽量选择公开的、允许爬取的内容站点最好是你自己有权限的测试站。毕设演示阶段完全可以用自己搭建的博客站或者目标站点允许抓取的部分来跑通流程既合法又能完整展示技术链路。一个最基础的Spider长这样import scrapy class ArticleSpider(scrapy.Spider): name article def start_requests(self): # 这里的起始URL替换成你自己的目标列表页 yield scrapy.Request(urlhttp://example.com/list, callbackself.parse_list) def parse_list(self, response): # 从列表页提取详情页链接 for url in response.css(a.article-link::attr(href)).getall(): yield scrapy.Request(urlresponse.urljoin(url), callbackself.parse_detail) def parse_detail(self, response): # 提取标题、正文、发布时间等字段 yield { title: response.css(h1.title::text).get(), content: response.css(div.content::text).getall(), publish_time: response.css(span.date::text).get(), url: response.url, }这段代码体现了Scrapy的核心机制start_requests是入口parse_list负责解析列表页并yield新的请求parse_detail负责解析详情页并把结构化数据yield出去。Scrapy的调度器会自动管理这些请求保证并发、去重、失败重试都处理在框架内部。抓详情页的时候一个重要技巧是尽量提取“干净”的正文。很多页面的正文区域混杂着广告、推荐阅读、脚本标签你需要先定位到正文所在的容器节点再提取文本。用response.css(div.article-content)先把正文区块框出来再在这个区块内部提取文本比直接在整个页面上抓文本干净得多。3.2 动态页面和iframePlaywright的正确打开方式现在很多网站的数据都在JS里加载甚至放在iframe里。举个例子一个页面主体框架是静态HTML但评论列表、数据表格这种模块是放在一个iframe里异步加载的。直接用Scrapy的Request去抓返回的HTML里没有iframe内的内容。最简单可靠的方案就是scrapy-playwright。你不必对每个请求都启用浏览器渲染只在需要的时候通过meta标记即可import scrapy class ArticleSpider(scrapy.Spider): name article def parse_list(self, response): for url in response.css(a.article-link::attr(href)).getall(): yield scrapy.Request( urlresponse.urljoin(url), callbackself.parse_detail, meta{playwright: True}, # 只对详情页启用渲染 ) async def parse_detail(self, response): # 直接取常规字段 title response.css(h1.title::text).get() # 取iframe里的内容 page response.meta[playwright_page] iframe_content await page.frame_locator(#comment-iframe).locator(body).text_content() yield { title: title, content: iframe_content or , }为什么用async def parse_detail因为Playwright的调用是异步的Scrapy的callback需要配合异步协程。这里面最需要注意的点是meta{playwright: True}只对当前请求生效不要全局开启因为渲染页面比普通HTTP请求慢几十倍全开会拖慢整个爬虫。我见过太多人图省事给所有请求都开了渲染结果一个小型爬虫跑得比单线程还慢。还有一个小坑启用Playwright之后response.css这些解析方式依旧可用但如果你需要在页面里点击、滚动、等待元素就必须拿到playwright_page对象来操作。比如等待动态内容加载完成可以await page.wait_for_selector(div.content)再提取否则可能抓到空数据。3.3 数据清洗与Item Pipeline给ES喂干净数据爬虫提取出来的原始数据往往带着HTML标签、换行符、多余空白放进ES之前必须清洗。清洗逻辑最好放在Item Pipeline里这样Spider只负责提取Pipeline负责加工职责分离。一个典型的Pipeline长这样import re import hashlib from elasticsearch import Elasticsearch, helpers class CleanDataPipeline: def process_item(self, item, spider): # 清洗正文 content .join(item.get(content, [])) content re.sub(r[^], , content) # 去标签 content re.sub(r\s, , content).strip() # 去多余空白 item[content] content # 用URL生成稳定文档ID url item.get(url, ) item[doc_id] hashlib.sha1(url.encode(utf-8)).hexdigest() return item class ElasticsearchPipeline: def open_spider(self, spider): self.es Elasticsearch([http://localhost:9200]) def process_item(self, item, spider): action { _op_type: index, _index: articles, _id: item[doc_id], # 用稳定ID重复抓取自动覆盖 _source: { title: item[title], content: item[content], url: item[url], publish_time: item.get(publish_time), }, } helpers.bulk(self.es, [action]) return item这里有两个关键的工程经验。第一用URL做文档ID实现幂等同一个URL重复抓取时ES会直接覆盖旧文档而不是新增一条防止索引数据重复。第二清洗要放在入库前ES不是用来存垃圾的正文里残留的标签和脚本会影响分词质量进而影响搜索结果的相关性。3.4 增量抓取与Scrapy-Redis的简单扩展爬虫不能只跑一次。搜索引擎要持续可用就需要周期性更新数据。最简单实用的增量策略是抓取前先查一下ES里有没有这个URL的文档有且时间没变就跳过或者抓取列表页的时候只取最近“一段时间内”的新内容。另一个常见的提升方向是引入Scrapy-Redis。Scrapy默认的去重队列在内存里爬虫一停去重记录就丢了。配置了Scrapy-Redis之后请求队列和去重指纹放到Redis里爬虫中断后重新启动可以从Redis里接着跑实现断点续爬而且多个爬虫节点可以共享同一个请求队列天然支持分布式扩展。配置很简单# settings.py SCHEDULER scrapy_redis.scheduler.Scheduler DUPEFILTER_CLASS scrapy_redis.dupefilter.RFPDupeFilter REDIS_URL redis://localhost:6379/0加上这几行爬虫的调度核心就从内存切换到了Redis。毕设阶段不需要真做分布式但能在技术上讲清楚“这个设计可以水平扩展”就已经是一个非常好的加分项了。4. 索引层Elasticsearch核心设计4.1 倒排索引和中文分词先搞懂再动手这是整个搜索引擎项目中最重要的一节。ES之所以能快速全文检索靠的是倒排索引。以文章标题“Python机器学习实战”为例分词后变成“python”“机器”“学习”“实战”ES维护一张映射表每个词条指向包含它的文档ID列表。查询“机器学习”时先分词成“机器”“学习”然后直接查表拿到文档集合计算相关度得分返回排序结果。这里的关键是分词器。ES默认的standard分词器对中文是按单个汉字切分的搜“学习”可能匹配到“学到”“学习委员”等噪声内容召回一堆不相关的结果。所以中文搜索必须要装IK分词器。IK提供了两种分词模式ik_max_word最细粒度切分比如“中华人民共和国”切成“中华人民共和国”“中华人民”“中华”“华人”“人民共和国”等。索引建这个能让召回率更高。ik_smart粗粒度切分只保留最合理的词比如“中华人民共和国”只切成“中华人民共和国”。搜索时用这个能让精度更高。实际项目里索引用ik_max_word搜索用ik_smart一粗一细兼顾召回和精度。这是一个非常经典且实用的配置。IK分词器还支持自定义词库。你可以在plugins/ik/config/IKAnalyzer.cfg.xml里配置ext_dict指向一个词库文件把“毕业设计”“机器学习”“深度学习”这类领域词加进去分词效果会大幅提升。我之前在项目里加入领域词库后搜索质量肉眼可见地变好算是一个低成本高收益的优化点。4.2 索引Mapping设计字段类型决定搜索行为创建索引之前Mapping设计要想清楚。我建议的articles索引结构大概这样{ mappings: { properties: { title: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart, fields: { keyword: { type: keyword, ignore_above: 256 } } }, content: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart }, url: { type: keyword }, publish_time: { type: date, format: yyyy-MM-dd HH:mm:ss }, author: { type: keyword } } } }这个Mapping里有几个设计点值得展开。第一title字段同时建了text和keyword子字段text用于分词搜索keyword用于精确匹配、排序和聚合。第二content和title都指定了analyzer和search_analyzer一个管索引分词一个管查询分词双向匹配才能保证中文搜索效果。第三url和author用keyword而不是text因为它们不需要分词keyword类型搜索是精确匹配还能用于聚合统计。创建索引的时候在命令行用curlcurl -X PUT http://localhost:9200/articles -H Content-Type: application/json -d mapping.json也可以用Python客户端from elasticsearch import Elasticsearch es Elasticsearch([http://localhost:9200]) mapping { mappings: { properties: { title: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart, fields: {keyword: {type: keyword, ignore_above: 256}} }, content: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart }, url: {type: keyword}, publish_time: {type: date, format: yyyy-MM-dd HH:mm:ss}, author: {type: keyword} } } } if not es.indices.exists(indexarticles): es.indices.create(indexarticles, bodymapping)4.3 数据写入Bulk批量比逐条快得多Scrapy Pipeline里如果每抓一篇文章就调用一次ES的index接口数据量一大性能就很差。正确做法是用helpers.bulk批量写入。这里的关键点不要每处理一个item就bulk一次而是攒够一批比如100条再写一次。from elasticsearch import Elasticsearch, helpers es Elasticsearch([http://localhost:9200]) actions [] for item in items: action { _op_type: index, _index: articles, _id: item[doc_id], _source: { title: item[title], content: item[content], url: item[url], publish_time: item[publish_time], }, } actions.append(action) if len(actions) 100: helpers.bulk(es, actions) actions.clear() if actions: helpers.bulk(es, actions)数据写入阶段最常见的报错是字段类型冲突。比如你第一次写入的publish_time是字符串“2024-05-01”ES自动识别成date类型但下一次某个字段传了条数字ES就会报mapper [publish_time] of different type。解决办法是在Mapping里明确规定字段类型写入前统一做类型校验和转换。5. 后端服务层Django如何对接ES5.1 搜索API设计Django负责把ES的能力包装成HTTP接口。我的建议是直接用DRF写一个搜索API视图入口参数设计为q搜索关键词、page页码、size每页条数。前端不管ES长什么样只跟Django交互。from rest_framework.views import APIView from rest_framework.response import Response from elasticsearch import Elasticsearch es Elasticsearch([http://localhost:9200]) class SearchAPIView(APIView): def get(self, request): q request.GET.get(q, ).strip() page int(request.GET.get(page, 1)) size int(request.GET.get(size, 10)) if page 1: page 1 if size 50: # 防止一次取太多 size 50 body build_search_dsl(q, page, size) result es.search(indexarticles, bodybody) # 组装返回数据 data { total: result[hits][total][value], page: page, size: size, results: [ { title: hit[_source][title], url: hit[_source][url], publish_time: hit[_source].get(publish_time, ), score: hit[_score], highlight: hit.get(highlight, {}), } for hit in result[hits][hits] ], } return Response(data)这里build_search_dsl是核心函数负责根据用户输入动态构造ES查询DSL。为什么不直接在前端拼ES查询因为ES的DSL结构复杂直接暴露给前端既不安全也不好维护。Django作为中间服务层屏蔽了ES的细节未来想换搜索引擎只需改这里。5.2 查询DSLmust、should、must_not到底怎么组合ES查询的核心是bool查询它有四个子句作用完全不同must必须匹配等价于AND同时参与相关度评分。filter必须匹配但不参与评分结果会被缓存适合做过滤条件。should可以匹配等价于OR用来提升文档分数如果must或filter里没有条件should至少要匹配一个。must_not必须不匹配等价于NOT。一个实用的搜索结果页查询DSL长这样{ query: { bool: { must: [ { multi_match: { query: 机器学习, fields: [title^3, content], type: best_fields } } ], filter: [ { range: { publish_time: { gte: 2023-01-01 } } } ], must_not: [ { term: { author.keyword: admin } } ], should: [ { term: { title.keyword: 从零开始 } } ], minimum_should_match: 0 } }, highlight: { pre_tags: [em], post_tags: [/em], fields: { title: {}, content: {fragment_size: 150, number_of_fragments: 3} } }, from: 20, size: 10, sort: [ {_score: desc}, {publish_time: desc} ] }这个DSL里包含了几个非常关键的优化点。fields: [title^3, content]表示标题字段的相关性权重是3倍因为标题比正文更能代表一篇文档的主题。filter里放时间范围既过滤了结果又不会影响评分还能让ES缓存过滤结果比直接塞进must更高效。highlight里给标题和正文都配置高亮正文高亮片段控制在150个字符返回3个片段避免页面被超长摘要撑爆。还有一个新手容易犯的错当查询里同时有must和should时should下子句的默认minimum_should_match是0也就是说should条件一个都不满足也能返回。只有在只有should的条件下才会默认要求至少匹配一个子句。这个规则搞不清楚就会出现“为什么我加了should结果里还是有完全无关的内容”这种问题。要强行要求必须匹配一定数量的should子句就显式设置minimum_should_match: 1。5.3 搜索结果分页别掉进深分页的坑ES的默认分页是from size比如取第3页每页10条就是from: 20, size: 10。这种分页方式在数据量小的时候没问题但当你有100万条文档时from到10000以上就会报错Result window is too large, from size must be less than or equal to [10000]这是ES的深分页保护机制为的是避免内存被大偏移量打爆。毕设项目索引几万条数据一般碰不到这个上限但如果你想在答辩时展示一下自己对深分页问题的理解可以用search_after方式实现滚动翻页。核心思路是记录当前页最后一条文档的排序值下一页把这些排序值传给search_after让ES直接跳到那个位置之后继续取数据。search_after的查询示例{ query: { match_all: {} }, sort: [ {_id: asc} ], size: 10, search_after: [abc123] }注意使用search_after时sort必须是确定的字段否则翻页会乱。这个方案在小项目里不是必须的但你能讲清楚它跟from size的差异技术上会比同龄人扎实一截。5.4 简单统计搜索行为用聚合或ESQL都能做搜索过程中统计“哪些词被搜得最多”“不同日期搜索量是多少”这类指标是很有价值的扩展功能。ES做统计有两种思路一种是传统的聚合查询另一种是ES 8.11引入的ESQL。传统聚合大概这样{ aggs: { hot_keywords: { terms: { field: query_keyword.keyword, size: 10 } } } }ESQL则更像SQL直观易懂FROM search_logs | STATS count COUNT(*) BY query_keyword | SORT count DESC | LIMIT 10区别在于聚合查询适用于7.x、8.x所有版本ESQL只在8.11以上的版本可用。如果你选的ES 7.17建议走传统聚合路线。实际使用时可以在Django搜索服务里把每次搜索的q参数、搜索时间、返回条数记录到一个ES索引search_logs再通过聚合接口出报表。这个功能放到答辩里既展示了项目“迭代意识”又能直接拿出数据说话。6. Web界面与前后端交互6.1 搜索页交互设计简单但不能糙前端方案我推荐最朴素的Django模板 原生fetch不要去引一套重前端框架。理由很简单搜索引擎页面本来就是搜索框加结果列表加分页没必要把工程复杂度拉高。核心是交互要顺滑比如按回车搜索、点击分页翻页、搜索过程中显示loading状态。一个基础的搜索页面数据流是这样的用户在表单输入关键词提交到Django的搜索视图视图调ES搜索服务拿到结果渲染模板输出。如果用fetch做异步搜索前端只需请求/search/api/?q关键词page1拿到JSON后动态渲染结果列表。这里有一个很加分的体验细节搜索建议。ES的Completion Suggester可以做输入前缀联想用户输入“机器”时下拉提示“机器学习”“机器视觉”。实现方式是在Mapping里增加一个suggest字段类型是completion写入数据时把标题或关键词塞进去。查询接口用suggest子句返回联想结果。这个小功能对搜索型产品的体验提升非常明显代码量也不大适合作为展示亮点。6.2 高亮结果渲染与XSS防护别直接用safe搜索结果的高亮是全文搜索引擎的基本能力ES返回的高亮字段带着em标签highlight: { title: [Python em机器学习/em实战] }如果直接把这段字符串塞进Django模板并用|safe渲染那高亮效果有了但XSS漏洞也埋下了如果ES索引的文档内容里恰好有恶意JavaScript脚本这些脚本会被浏览器直接执行。正确做法是先对高亮内容做HTML转义再只允许em标签通过import html from django.utils.html import format_html def safe_highlight(text): escaped html.escape(text) # 把转义后的 lt;emgt; 还原成真正的 em escaped escaped.replace(lt;emgt;, em).replace(lt;/emgt;, /em) return format_html(escaped)这样既保留了高亮样式又避免了整段原生HTML被直接注入。这个细节在答辩时如果有人问“安全性怎么考虑的”就是很好的回应。7. 部署上线与高频问题速查7.1 三个服务的启动顺序与联调本地联调阶段服务启动顺序建议是先启动Elasticsearch再跑Scrapy导入数据最后启动Django。ES没起来就导数据Pipeline会直接连接失败数据没导入就开Django搜索页搜索接口会返回空结果。调试技巧第一步先用curl http://localhost:9200/articles/_search?q关键词确认ES里有没有数据、能不能搜到结果再用Django的搜索API测。如果ES直接查不出来问题在爬虫和分词如果ES查得出来但Django接口查不出来问题在Django的DSL组装逻辑。按这个顺序排查能省下很多时间。7.2 高频坑位与解决方案速查表我在实际项目中遇到的坑整理成一张表按症状排布遇到直接对号入座症状可能原因解决方法ES启动闪退内存不足 / JDK版本冲突调整jvm.options内存确认使用内置JDKES启动报vm.max_map_count过低Linux系统参数sysctl -w vm.max_map_count262144IK分词器不生效IK版本与ES版本不一致严格下载相同版本的IK插件搜索报Result window is too largefromsize超过默认限额改用search_after或调大index.max_result_window索引写入报mapper [xxx] of different type同名字段类型冲突清理索引检查写入字段类型爬虫抓取文本为空JS渲染或iframe内容对相关页面启用Playwright搜索接口返回500Django访问ES失败检查ES是否启动、elasticsearch-py版本是否匹配搜索结果没有高亮查询没配置highlight在DSL里添加highlight字段这个表我建议直接保存下来毕设调试期间你会反复用到。还有一个很重要的实操经验每次修改ES的Mapping结构或重新导数据最干脆的做法是删除索引重建而不是在原索引上反复改字段curl -X DELETE http://localhost:9200/articles重新创建并导数据比解决字段冲突快得多。7.3 部署到Linux服务器的简单方案如果你要把项目部署到服务器上演示我推荐最省心的组合ES用systemd管理Scrapy用crontab定时跑Django用gunicorn跑前面再挂一个nginx做反向代理和静态文件服务。ES的systemd服务文件比较简单核心就三块指定启动用户、指定ES路径、启动后检查9200端口。Django用gunicorn启动gunicorn search_site.wsgi:application -b 127.0.0.1:8000 --workers3nginx配置一个server块监听80端口/反代到8000端口/static/指向Django静态资源目录。前端所有请求都走80端口对外暴露的就是一个普通的搜索网站但底层是完整的三层架构。部署的时候记得关掉ES的HTTP跨域访问限制或者只允许本机访问如果你要用浏览器插件看ES索引数据还需要在elasticsearch.yml里设置http.cors.enabled: true http.cors.allow-origin: *注意这仅用于开发调试生产环境不建议开放。8. 毕业设计答辩可以重点讲的内容8.1 技术亮点怎么包装答辩的时候最忌讳把项目讲成“我搭了一个搜索框”。这个项目的技术亮点可以从三个层面去讲。第一个层面是完整链路。从爬虫采集、数据清洗、中文分词、倒排索引、搜索排序到Web展示所有环节都是自己打通并优化的这是一个真实系统的完整闭环而不是教材里的一个孤立概念。第二个层面是细节优化。比如title字段权重设置、索引用ik_max_word而搜索用ik_smart、filter和must的使用差异、高亮XSS防护这些都是工程经验沉淀属于“你不踩坑就不会知道”的东西。答辩老师问到任何细节你都能给出设计依据这就是最好的加分项。第三个层面是可扩展性。Scrapy-Redis支持分布式爬虫ES天然支持集群扩容搜索日志可以做热榜推荐这些都可以作为未来的演进方向。讲出来会显得你有整体架构意识。8.2 如果后续有时间我会往这些方向扩展如果做完整套毕设之后还有余力我个人建议可以挑一个方向做深。比如在搜索接口里加入一个基于搜索日志的“热搜词推荐”功能用ES的聚合统计最近7天搜索最多的词再比如给文章索引增加一个completion字段实现搜索框里的实时联想。这两个功能都能直接提升项目演示的“可玩性”代码量不大但对答辩印象分的提升非常明显。还有一个成本不高但很有价值的方向把Django后台管理页面接进来用Django Admin直接管理爬取到的数据可以手动删除质量差的文档观察它对搜索结果的影响。这在演示场景里非常直观能让评委看到你的系统是可控的、可维护的。9. 最后分享两个压箱底的经验如果你按这篇文章的节奏从头搭这个项目我最后想强调两点都是我自己实操中踩坑总结出来的。第一版本锁死、环境一次到位。Python 3.10、Django 4.2、Scrapy 2.11、ES 7.17、IK 7.17、elasticsearch-py 7.17这套组合千万别混装。很多项目失败不是逻辑写不出来而是环境装了好几天都没起来最后心态崩了。环境搭好后建议用pip freeze requirements.txt把依赖锁住换机器部署时直接pip install -r requirements.txt省掉无数抓狂时刻。第二数据先跑通功能再迭代。很多同学喜欢一上来就把所有功能都设计好再动手写代码结果项目周期拖得很长。更务实的路径是先搭一个最简版本——Scrapy抓10条数据ES建好索引Django出一个能搜出结果的页面。把这个闭环跑通你已经完成了毕设的60%。剩下的时间用来优化分词、调整排序、加高亮、加建议每一步都是肉眼可见的进步。这个项目整体工作量适中但技术密度不低做下来基本可以把Python工程化能力、爬虫、搜索引擎原理、Web开发都过一遍。动手的时候如果卡在某个环节回来看这篇文章对应的小节大概率能找到答案。希望这篇内容能帮你把这个毕设吃得透透的。本文还有配套的精品资源点击获取
返回列表