项目:XSearch | 版本:v1.3 | 生成日期:2026-10-06 | 依据:源码分析
XSearch 的依赖声明分布在三处:后端 XSearch/requirements.txt(宽松范围,开发用)、XSearch/requirements-lock.txt(精确锁定,Docker 与 CI 共用)、前端 frontend/package.json(npm 语义化范围 + package-lock.json 锁定)。这个三层结构本身就是第一个值得记录的决策:声明与锁定分离。requirements-lock.txt 首行注释写明"2026-09-09 于 Python 3.12 实测通过的组合",并特别告诫"不要把共享解释器的完整 pip freeze 直接搬进来(会带入无关包)"——锁定的是直接依赖的精确版本,传递依赖由 pip 在这些约束下解析,而不是无脑 freeze 整个环境。这样 lock 文件既保证了可复现(Dockerfile、CI、本地三者装出同一套版本),又保持了可读性(一眼看清项目真正依赖什么)。
版本口径的三个锚点需要先记牢:Python 3.12(Dockerfile 的 python:3.12-slim、CI 的 setup-python 同为 3.12,且 Django 6.1.1 自身要求 3.12+);Elasticsearch 服务端 8.15.3 与客户端 8.19.3(服务端与 IK 插件严格同版,客户端允许高于服务端——ES 8.x 客户端对同主版本服务端兼容);Node 20(frontend.Dockerfile 与 CI 一致,README 标注下限 18+)。这些版本不是拍脑袋:每一组都能在源码里找到"为什么是这个"的注释痕迹(如 ES 8.15.3 与 NAS 共享 dev-es 同版本、免 1.3GB 基础镜像拉取,见《06-部署运行》4.7)。
依赖总量的第一印象:后端直接依赖 13 个(5 个后端框架 + 7 个爬虫侧 + gunicorn + pytest),前端 9 个(4 运行时 + 5 构建期)。对一个覆盖"爬虫 + 检索 + Web 服务 + SPA"全栈的项目,这个数字是克制的——它直接映射了《00-项目总览》的选型哲学:每个依赖都必须回答"不用它会怎样",答不上来就不引入。最直观的证据是前端没有状态管理库、没有 HTTP 库(裸 fetch)、没有测试框架(类型检查充当测试);后端没有 DRF(手写 JsonResponse)、没有 Celery(子进程 + APScheduler)。
| 依赖 | 锁定版本 | 用途 | 主要使用位置 | 选型理由 | 替代方案 | 升级风险 |
|---|---|---|---|---|---|---|
| Django | 6.1.1 | Web 框架/路由/配置/中间件 | xsearch/settings.py、search/api_views.py 全部视图 | 生态成熟自带 admin 与中间件体系;项目源起即 Django | Flask/FastAPI(需重写 25 视图的请求解析与异常约定) | 6.x 主版本更新快,注意 deprecation;USE_L10N 等已废弃项 |
| elasticsearch | 8.19.3 | ES 官方原生客户端 | api_views.py 全部查询/聚合/管理操作 | 官方维护、与 DSL 同族兼容 | 直接裸 HTTP(需自封装重试/序列化) | 客户端 8.x 与服务端 8.x 需同主版本;execute_suggest 等 API 在 8.x 被移除(源码注释已踩过) |
| elasticsearch-dsl | 8.18.0 | ES 文档建模(Document/mapping/分析器) | crawler/pipelines.py 的 QuoteDocument | 声明式 mapping + doc.save() upsert 是管道幂等的基础 | 手写 JSON mapping(失去类型化与 init() 建索引) | 升级需同步 elasticsearch 主包版本;_index._name 私有属性覆盖依赖其内部实现 |
| redis | 5.2.1 | Redis 客户端 | api_views.redis_cli(热搜词/缓存) | StrictRedis.from_url + decode_responses 足够;无 ORM 需求 | aioredis(Django 侧无 async 需求) | 5→6 已有Breaking 变更先例;升级需重测超时参数 |
| requests | 2.32.3 | 图片代理回源 | api_img 的 _requests.get(stream=True) | 同步场景标准库级存在 | httpx(异步/HTTP2 无需求) | 常规低风险;注意代理环境变量继承 |
| scrapy | 2.18.0 | 爬虫框架 | crawler/ 全部 | Python 爬虫事实标准;2.13+ 异步 start() 入口 | 自研调度(失去中间件/管道/限速生态) | 大版本升级常伴随中间件签名变化(源码已为 2.18 弃用预告改 process_item(spider=None)) |
| scrapy-playwright | 0.0.48 | Playwright 与 Scrapy 集成 | crawler/settings.py DOWNLOAD_HANDLERS;douyin_hot.py | meta 标记式启用渲染,替代老课程 Selenium | Selenium(重、慢、需独立进程管理) | 与 Scrapy/Playwright 双向版本耦合,三者需一起升级 |
| playwright | 1.60.0 | 浏览器自动化运行时 | Dockerfile playwright install chromium | scrapy-playwright 的底层依赖 | — | 浏览器二进制需随镜像重建;PLAYWRIGHT_DOWNLOAD_HOST 构建参数适配国内源 |
| browserforge | 未锁版 | 成套浏览器指纹头生成 | crawler/ai/fingerprint.py | 生成 UA+client hints 自洽组合;有内置降级模板,缺失不致命 | 自维护 UA 池(老课程 fake-useragent 路线已移除) | 未锁版本是有意为之(lock 注释:"本机未安装……Docker 安装最新版即可")——这是唯一无版本钉的运行时依赖,属已知取舍 |
| lxml | 5.3.0 | HTML/XML 解析(Scrapy 选择器底层) | Scrapy 内部、RSS 解析 | 性能与容错俱佳 | html.parser(慢且弱) | 需编译工具链(Dockerfile 的 gcc/libxml2/libxslt 组合就是为它) |
| Twisted | 26.4.0 | Scrapy 的事件循环引擎 | crawler/settings.py 的 TWISTED_REACTOR | Scrapy 内建依赖;AsyncioSelectorReactor 是 scrapy-playwright 共存前提 | — | 跟随 Scrapy 升级;reactor 配置错会直接起不来 |
| APScheduler | 3.11.2 | 进程内 cron 调度 | crawl_manager.py ScheduleManager | BackgroundScheduler 轻量、from_crontab 语义校验现成 | Celery beat(重)、crontab(脱离 Web 可视) | 3.x→4.x API 大改(项目锁定 3.x,misfire_grace_time 等参数语义以 3.x 为准) |
| gunicorn | 23.0.0 | WSGI 生产服务器 | Dockerfile CMD | 标准、稳定、--workers 1 --timeout 120 语义清晰 | uwsgi(配置复杂度更高) | 低风险;升级不影响单 worker 决策 |
| pytest | 9.0.3 | 测试运行器(requirements 归"开发/测试"段) | 依赖清单收录但实际测试走 manage.py test | lock 文件保留兼容层 | — | 《13-技术债务》#20:Dockerfile 全量安装带入了无用测试依赖(体积小,纯洁癖) |
scrapy 是爬虫侧唯一不可替代的依赖,选它的理由不只是"课程源起",更在于四个能力在源码里被逐一兑现。一是中间件体系:crawler/middlewares.py 三个下载中间件(543/544/545)利用的正是 Scrapy 按优先级排序的中间件链——内置 UserAgentMiddleware(500)先写 UA、指纹中间件(543)后覆盖,这个顺序本身就是架构。二是管道体系:EsArticlePipeline: 300 挂进 ITEM_PIPELINES,from_crawler 工厂从 settings 读连接信息(可测试性设计,见《09》第八节)。三是限速与合规内建:ROBOTSTXT_OBEY、DOWNLOAD_DELAY、AutoThrottle 都是框架原生开关,合规四件套因此只需要配置而非代码。四是 JOBDIR 断点恢复:CrawlManager 的 -s JOBDIR=jobs/... 直接复用框架的持久化调度状态,自研等于重造。
升级路径上有两个已被源码预告的坑:其一,pipelines.py 142-143 行注释写明"spider 参数改为可选:Scrapy 2.18 起弃用'必须接收 spider'的旧签名,未来版本不再透传该参数"——这是升级 Scrapy 3.x 时必改的一行;其二,custom_settings 里 HANDLE_HTTPSTATUS_LIST(aihot 两爬虫把 429/503 纳入正常响应流)属于框架细节语义,升级时需确认行为未变。卫星依赖中,scrapy-playwright 与 playwright 构成一组三体耦合(Scrapy 版本、集成层版本、浏览器运行时版本),升级必须三者同批进行并在镜像里重装 chromium;requirements.txt 对三者的下限注释(scrapy>=2.13 # start() 异步入口 / 新版中间件签名)就是那次升级留下的路标。
Django 在这个项目里被用得很"薄"但很深。薄在:没有 ORM 模型(search/models.py 只剩迁移说明)、没有模板页面(TEMPLATES 注释声明"页面层已由 frontend/ Vue 工程承担")、没有表单体系(每个视图手写参数夹取与白名单,见《08》第三节)。深在:settings 的分层加载(环境变量 → local_settings.py exec 覆盖,settings.py 213-217 行)、中间件链上自挂的 CorsMiddleware(排在 SecurityMiddleware 之前保证预检短路)、AppConfig.ready() 启动钩子(调度器恢复 + 弱配置告警)、LOGGING 配置(让 search logger 的 INFO 不被默认配置丢弃)。选 Django 而非 FastAPI 的决定性因素是这些"框架设施"免费:25 个视图的异常约定、路由反向解析、静态管理、以及未来若要加 Django admin 的可能性。
gunicorn 的存在感全部集中在 CMD 的三个参数上:--workers 1 是 ADR-04 的部署表达;--timeout 120 是给图片代理回源(10s)与慢查询留的上限;xsearch.wsgi:application 是唯一入口。升级风险几乎为零,但要注意 gunicorn 23 与 Django 6 的组合要求 Python 3.12+,与镜像基线一致。
elasticsearch(低层客户端)与 elasticsearch-dsl(文档建模层)是一对必须同族演进的依赖:dsl 8.18.0 建立在 elasticsearch 8.x 之上,跨大版本混装会在序列化层炸出难懂的错误。这个项目的使用面在两侧分布不均——api_views.py 几乎全用低层客户端(client.search/count/get/update/delete_by_query/indices.*,因为读侧的查询形状高度动态),pipelines.py 几乎全用 dsl(Document 声明 mapping、QuoteDocument.init() 建索引、doc.save() upsert,因为写侧形状固定)。一个已踩过的坑被注释钉在 api_suggest 里:"elasticsearch-dsl 8.x 已移除 execute_suggest,直接用原生客户端"——这是 7.x→8.x 迁移的典型 API 移除案例。另一个隐蔽耦合在《13-技术债务》#4:QuoteDocument.Index.name 硬编码 quotes,open_spider 用 _index._name(dsl 私有属性)在运行时覆盖索引名——dsl 升级若改变该内部属性,这条暗操作会静默失效。
超时与重试语义也值得记录:低层客户端构造时 request_timeout=5, retry_on_timeout=False(快速失败优于长挂,api_views.py 59 行),dsl 模块级连接 timeout=10,管道 open_spider 探测再收紧到 request_timeout=5。升级客户端大版本时,这三处超时参数的传递方式(构造参数 vs config 字典)需要逐一核对。
redis-py 5.2.1 在全项目只有一个连接点:api_views.py 60-61 行的 redis_cli = redis.StrictRedis.from_url(settings.REDIS_URL, decode_responses=True, socket_connect_timeout=3, socket_timeout=3)。两个参数都有语义:decode_responses=True 让所有返回值是 str 而非 bytes(热搜词直接进 JSON 响应,不 decode 就得处处 .decode());3 秒双超时保证 Redis 挂时接口最坏多等 3 秒而非挂死(与 ES 的 5 秒、nginx 的 300 秒构成完整的超时预算链,《10》4.2)。使用面严格限定四个键模式(search_keywords_set zset、headlines:{hours}、rankings_news:{source}:p{page}、sitemap:{base},见《04-数据模型》第五节),全部 try-except 包裹——Redis 被刻意维持在"纯加速层"定位(ADR-06),因此这个依赖的升级风险等级是全表最低的:换掉它(甚至拔掉它)系统仍然正确,只是变慢。唯一要注意的是 5→6 的大版本升级先例(响应解析与连接池行为变化),升级时重跑 LoginRateLimitTests 之外还要人工验证热搜词链路。
lxml + Twisted 是 scrapy 的"隐形地基":前者是选择器引擎(也是 Dockerfile 里 gcc/libxml2-dev/libxslt1-dev 编译链存在的原因),后者是事件循环(AsyncioSelectorReactor 配置是 scrapy-playwright 的硬前提)。两者都不直接出现在业务代码里,但升级失败的症状(起不来、selector 炸)会非常显眼。browserforge 是全表唯一未锁版本的运行时依赖——lock 注释写明原因:本机开发环境未安装,指纹中间件有内置 Chrome 124/123 降级模板(crawler/ai/fingerprint.py 的 _FALLBACK_PROFILES),Docker 里装最新版即可。这实际上是一种"软依赖"策略:能力存在时增强(成套自洽指纹头),缺失时优雅降级,依赖失败被 try-except 拦在 get_headers() 边界(middlewares.py 45-49 行:"指纹生成失败,使用上一次/内置头")。APScheduler 的锁定在 3.x 有一个明确理由:_add_to_scheduler 依赖的 misfire_grace_time/coalesce/max_instances 参数与 EVENT_JOB_MISSED 事件在 3.x 的语义是项目修"cron 静默漏跑"事故的基石(《05-核心流程》流程三),4.x 的 API 变化会迫使 ScheduleManager 重写。requests 只服务图片代理回源,allow_redirects=False + stream=True + 20MB 上限的调用方式(api_img 1104-1115 行)是 SSRF 三重防护的第三层(《08》第五节),升级 requests 不影响这套语义但需回归 api_img(该函数目前无测试,《09》缺口 #6)。
| 依赖 | 版本范围 | 类别 | 用途 | 使用位置 | 选型理由 | 替代方案 | 升级风险 |
|---|---|---|---|---|---|---|---|
| vue | ^3.5.0 | 运行时 | UI 框架(Composition API + <script setup>) | 全部 views/App.vue | 国内中小项目首选(《AI升级说明》第五节);单文件组件 + 响应式模型成熟 | React/Svelte(团队背景与生态决定) | 3.x 内低风险;<script setup> 语法已稳定 |
| vue-router | ^4.5.0 | 运行时 | SPA 路由(history 模式、懒加载、守卫) | main.ts(10 条全懒加载路由 + 守卫 + afterEach meta) | 官方配套;"URL 即状态"架构的载体 | 手写 hash 路由(失深链/守卫生态) | 4.x 稳定;createWebHistory 需 nginx try_files 兜底(已有) |
| element-plus | ^2.9.0 | 运行时 | 组件库(表格/表单/弹窗/分页全用) | 全部 views | Vue3 组件库默认选项 | Naive UI/Ant Design Vue | 整库引入是当前包体主体(《10》第六节优化建议 #1:按需引入可降首屏 30-50%) |
| @element-plus/icons-vue | ^2.3.2 | 运行时 | 配套图标 | 各表单/按钮 | 官方配套 | 独立图标库(增加学习成本) | 低风险 |
| typescript | ^5.9.3 | 构建 | 类型系统(strict 全套) | tsconfig.json + 全部 .ts/.vue | vue-tsc --noEmit 充当契约测试(《09》第六节) | JSDoc 弱类型 | 与 vue-tsc 版本需匹配 |
| vue-tsc | ^3.3.11 | 构建 | Vue SFC 类型检查 | package.json build 脚本第一段 | 官方类型检查入口 | tsc(不识别 .vue) | 跟随 vue/typescript 同步升 |
| vite | ^6.0.0 | 构建 | 开发服务器 + 打包 | vite.config.ts(5173/4173 端口、/api 代理) | 官方工具链;dev 态 /api 代理到 8000 免 CORS | webpack(配置量级完全不同) | 6→7 时插件体系可能变动;@vitejs/plugin-vue 需同步 |
| @vitejs/plugin-vue | ^5.2.0 | 构建 | Vite 的 Vue SFC 插件 | vite.config.ts | 官方配套 | — | 跟随 vite |
| @types/node | ^22.20.2 | 构建 | Node API 类型(vite.config 等使用) | 构建脚本 | 常规 | — | 低风险 |
前端运行时依赖只有四个包,且全部是 Vue 官方或事实标准——这个"最小集"策略与后端的"不引入 DRF/Celery"是同一条哲学。它换来的东西在源码里可数:package.json 无 pinia/vuex,登录态由 src/auth.ts 的两个模块级 ref + localStorage 承担,搜索状态由 URL query 承担(《02-页面设计》第五节的状态管理表);无 axios,api.ts 的 request<T>() 用裸 fetch 实现,401 处置、错误提取、token 附加共 19 行核心代码;无 vitest/jest,质量门禁押在 vue-tsc --noEmit(build 脚本的第一段,类型不过即构建失败)与 CI 的 1.2MB 单 chunk 体积守门上。三处"没有"各自对应一个显式理由,而不是省钱:状态库没有跨页共享需求、fetch 满足全部语义(无拦截器/取消等高级需求)、薄组件的测试性价比极低(《09》第六节有完整论证)。
成本也如实记录:element-plus 整库引入(import ElementPlus + 全量 CSS)是当前 chunk 的最大组成,10 条路由虽全懒加载,组件库代码却躲不开首屏——《10-性能设计与优化》第六节把"按需引入(unplugin-vue-components + unplugin-auto-import)"列为优化建议第 1 位,预期首屏降 30-50%,代价是构建配置改动加全页面回归。CI 的 1.2MB 守门阈值(1258291 字节,精确等于 1.2×1024×1024)正是在这个背景下定的:允许正常增长,但"退回单 chunk"量级的回归必被拦截(病史记录在 main.ts 注释:静态 import 时代曾打成 1.1MB 单 chunk)。
版本范围有个值得注意的细节:全部用 ^ 前缀(兼容同主版本的次版本),真正的锁定靠 package-lock.json——且 lock 的 resolved 指向 npmmirror,CI 注释给了预案(受限网络下拉取失败应改官方 registry 重锁)。frontend.Dockerfile 用 npm install 而非 CI 的 npm ci 是已登记的偏差(债务 #19:镜像内依赖可能比 lock 漂移)。前端依赖的升级风险整体低于后端:Vue 3.5/router 4.5 都处于长稳定期,唯一需要"三包同升"的是 vue + vue-tsc + typescript 的组合(类型层的三体耦合,与后端 scrapy 三体耦合结构同构)。
按"安全、维护活跃度、版本新鲜度、耦合度、许可"五个维度对全部直接依赖做一次体检,结论先行:这是一份健康度显著高于个人项目平均水平的依赖清单,当前无任何一项处于"必须立刻处理"的红色状态。
安全面。后端 13 项均为大社区核心库(Django/Scrapy/ES 官方客户端),安全公告响应快;requests 2.32.3 已含历史上几个著名 CVE(证书校验、代理泄露)的修复。需要诚实标注的两个点:browserforge 未锁版意味着构建不可完全复现(今天构建与昨天构建可能装到不同版本),属于"低危但反纪律"的例外,建议下轮维护补一个版本下限;ES 服务端 xpack.security.enabled=false 不是依赖问题但与依赖版本策略相关——升级 ES 版本时是重新评估认证能力的最佳时机(《08》第九节第 1 条)。⚠️ 待确认:仓库无自动依赖审计(无 dependabot/renovate 配置、无 pip-audit 步骤),实际 CVE 暴露面依赖人工关注。
版本新鲜度。锁定时间戳是 2026-09-09(lock 首行注释),距今约一个月,全表处于"刚更新过"状态:Django 6.1、Scrapy 2.18、ES 8.19 客户端、Vue 3.5、Vite 6 都是各生态的当期主流。唯一"旧"的是 ES 服务端 8.15.3(落后客户端 4 个次版本),但这是有意的——与 NAS 共享 dev-es 同版本省 1.3GB 拉取、与 IK 插件严格同版(deploy/es-ik.Dockerfile 注释),且 8.15→8.19 之间无必须跟进的安全公告 ⚠️ 待确认。
耦合度。三组"同升体"已在上文识别:scrapy + scrapy-playwright + playwright(+浏览器二进制)、elasticsearch + elasticsearch-dsl(+服务端/IK)、vue + vue-tsc + typescript。三体耦合是这类框架组合的常态,不是坏味道;关键是升级时把它们当一个原子单位对待,文档里三处升级路径注释(requirements.txt 下限注释、es-ik.Dockerfile、tsconfig 严格项)已经把"为什么锁在一起"讲清了。
依赖数量与卸载弹性。全栈 22 个直接依赖,且多数具备"卸载不塌方"的弹性:browserforge 缺失有降级模板、APScheduler 未装调度降级但配置仍存(_get_scheduler 返回 None 的分支)、Redis 拔线系统仍正确、Playwright 只影响抖音单源。真正刚性的是三件套:ES(业务数据主存储)、Django(全部 API)、Scrapy(全部采集)——它们也是全表里最难替换的,符合"核心依赖刚性、边缘依赖弹性"的健康分布。
给下一位维护者的三条操作纪律。其一,升级永远从 requirements-lock.txt 动手(改锁定版本 → 本机 pip install -r → 跑 python manage.py test search → 重建镜像验证),不要直接改 requirements.txt 的范围就完事——CI 与 Docker 只认 lock。其二,前端升级后必看 CI 体积守门的输出数字(它是最朴素的回归曲线,见《10》第七节),若 element-plus 按需引入落地,同步把 1.2MB 阈值下调到新基线,否则守门形同虚设。其三,新增依赖前先过一遍本篇第一节的判据——"不用它会怎样"答不上来的,就不加:这份清单之所以能保持 22 项,靠的正是这个问题的持续追问。