简介:搜索引擎是信息检索的核心技术,其底层依赖倒排索引实现高效查询。ElasticSearch作为分布式搜索服务器,通过分词和BM25相关性算法提供高质量检索;Scrapy负责从目标站点采集数据,Django则作为Web层构建搜索交互界面。三者结合可快速搭建一套完整的小型全文搜索引擎,广泛应用于毕业设计、垂直搜索以及教学演示。本文从架构设计出发,详细介绍环境搭建、爬虫编写、索引设计、查询DSL开发及Django集成等关键环节,并给出常见问题排查方案,帮助开发者快速掌握搜索技术栈的实战落地。 又到了一年一度毕业设计选题的时候。如果你正在纠结做一个什么样的计算机专业课题,同时又想兼顾难度、可展示性和答辩通过率,那我强烈建议你考虑这个方向:基于Scrapy+ElasticSearch+Django的小型全文搜索引擎。这套技术栈玩明白,等于一箭三雕——爬虫、搜索引擎、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 11;8.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里定义Item:
import 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 动态页面与iframe:Playwright集成方案
如果你爬的站点是纯静态页面,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.AsyncioSelectorReactor"Spider里通过meta参数触发渲染:
def parse_detail(self, response): yield scrapy.Request( url=detail_url, callback=self.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(index="article", id=item["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还得重建索引,非常麻烦。
我的索引叫article,Mapping设计如下:
{ "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_size=100, flush_interval=5): 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 搜索DSL:must、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, page=1, page_size=10): 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(index="article", body=body) 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”搜不到全角“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(keyword=keyword, result_count=total) 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, name="search"), ]前端页面我建议走简洁路线:一个居中的搜索框,下面列出结果标题、内容摘要、来源链接。结果标题带超链接指向原文,内容摘要里自动展示高亮的<em>标签。千万不要把前端做得太重,搜索引擎的项目亮点在后端的数据处理和检索技术上,不是在前端样式上。
5.3 Django ORM与ES的配合:删除与同步
项目里有个功能:管理员在后端删除一条爬取的垃圾文章。这里必须处理双数据源同步的问题。ES里的文档和MySQL里的记录要一起删,否则数据就对不上了。
from elasticsearch.exceptions import NotFoundError def delete_article(request, doc_id): # 先删除ES里的文档 try: client.delete(index="article", id=doc_id) except NotFoundError: pass # 文档不存在也继续删数据库记录 # 再删除MySQL里的记录 ArticleMeta.objects.filter(url=doc_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,确保内存不低于512m |
| 9200端口被占用 | 其他进程占用端口 | netstat -ano | findstr 9200,找到PID后结束进程 |
| 启动后无法访问9200 | network.host配置问题 | 确保配置为127.0.0.1,并检查防火墙 |
| 中文索引报错 | 未安装ik分词器 | 下载对应版本ik并解压到plugins目录后重启 |
除了ES,Django项目还有一个高频坑:静态文件404。出现这个问题的原因是DEBUG=False时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(encoding="gbk")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查询这条主链路跑通,再按自己的兴趣往上加功能。把基础链路做扎实,比堆砌一堆花哨但没有实际用途的功能要有价值得多。最后一个小技巧:本地开发时用DEBUG=True,但是答辩演示前一定要切到生产模式,提前处理好静态文件,不然现场开不了页面会很尴尬。
本文还有配套的精品资源,点击获取