项目:XSearch | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
XSearch 是一个分布式爬虫与全文检索站,基于 Scrapy + Elasticsearch + Django + Vue 3 的全栈架构,覆盖"爬虫采集 → 数据存储 → 关键词搜索 → 采集管理"的完整链路。项目源自慕课网《新版 Scrapy 打造搜索引擎》课程,随后经历了两次重大演进:一是技术栈整体升级到 2026 年的主流组合(Scrapy 2.18、Django 6.1、Elasticsearch 8.15、Vue 3.5、Vite 6);二是按公开站点的标准完成了一轮安全与合规收口(见 README.md 的"发布清单"与 AI升级说明.md 第七节的合规立场说明)。
从源码可以确认,这个站点面向的用户分为两类:
/api/search/、/api/headlines/、/api/rankings/ 等)在 XSearch/xsearch/urls.py 中均无鉴权要求。X-Admin-Token 请求头鉴权,可以使用"采集管理"页(frontend/src/views/CrawlView.vue)一键启动爬虫、管理 cron 定时任务、查看任务历史与日志尾部;使用"数据管理"页(frontend/src/views/DbAdminView.vue)浏览、编辑、删除 Elasticsearch 中的文档,或按来源批量清理数据。项目的价值定位可以概括为三句话:用合规的爬虫持续采集多个公开信源的中文内容;用 Elasticsearch + IK 中文分词提供高质量的全文检索体验(补全、纠错、分面、高亮);用单机可运维的方式(子进程爬虫 + 进程内调度器 + 文件持久化)把采集管理做成一个普通人也能操作的 Web 界面。
值得一提的是项目的合规立场,这在 crawler/settings.py 与 AI升级说明.md 中有明确记载:默认遵守 robots.txt(ROBOTSTXT_OBEY = True)、自报身份 XSearchBot/1.0、限速礼貌(DOWNLOAD_DELAY = 1 + AutoThrottle)、不采集登录后数据、不内置验证码识别工具。唯一的例外是 douyin_hot 爬虫依赖 Playwright 渲染与浏览器指纹伪装(该站对常规 UA 返回 444),项目将伪装范围收敛到 FINGERPRINT_HOSTS 白名单(目前只有抖音域名),并在文档中诚实记录了这一张力。AI升级说明.md 第七节把这类边界定性为"比技术更硬的边界"——绕过目标站技术措施的能力(如早期课程自带的知乎登录工具)宁可移除也不保留。
放到同类方案的坐标系里,这个项目的技术形态是一组环环相扣的选择:检索层选 Elasticsearch 而非 MySQL LIKE,是因为本站的搜索体验(拼音/短语纠错、completion 补全、来源/时间分面、高亮)在 LIKE 语义下都得自研,而 ES 的这些能力正是建站的核心卖点;采集层保留独立 Scrapy 进程而非塞进 Django 进程内,是因为爬虫是长耗时、可中断、需要断点恢复(jobs/ JOBDIR)的工作负载,与"毫秒级响应的 API 进程"资源画像完全不同,二者仅通过 ES 共享数据、互不拖累;运行时状态(定时任务、采集历史)用 JSON 文件而非数据库持久化,是把"单机部署、人类可直接查看与备份"置于通用性之上的选择。这些取舍的完整论证与升级路径(如换 Scrapyd、状态外置)见《01-系统架构》的关键设计决策一节。
| 功能域 | 功能点 | 前端入口 | 后端支撑 | 鉴权 |
|---|---|---|---|---|
| 今日要闻 | 跨来源按发布时间倒序混排,同题去重,热搜词引导 | frontend/src/views/HomeView.vue | api_headlines(search/api_views.py) | 公开 |
| 关键词搜索 | multi_match 检索、ES completion 补全、短语纠错、模糊容错、来源/时间分面、排序切换、分页、空页回退 | frontend/src/views/SearchView.vue | api_search / api_suggest | 公开 |
| 新闻列表 | 中新网/IT之家/Solidot 三源按时间倒序 + 分页,60 秒 Redis 缓存 | frontend/src/views/NewsView.vue | api_rankings?source=news | 公开 |
| AI 讯息 | AI 精选(LLM 摘要+评分)、AI 日报(折叠阅读)、AI 热点榜(三 Tab) | frontend/src/views/AiView.vue | api_rankings | 公开 |
| 条目详情 | 按 ES _id 精确取全文,避免"标题再搜一遍带错文" | frontend/src/views/AiDetailView.vue | api_doc | 公开 |
| 榜单 | 抖音热点榜 50 条按 rank 展示 | frontend/src/views/RankingsView.vue | api_rankings?source=douyin_hot | 公开 |
| 数据概览 | 文档总数、来源分布、热搜词(Redis zset) | frontend/src/views/StatsView.vue | api_stats | 公开 |
| 采集管理 | 一键启动 5 个内置爬虫、状态监控、日志尾部、任务历史、统计图表、cron 定时任务增删改停 | frontend/src/views/CrawlView.vue | api_crawl_* / api_schedule_*(search/crawl_manager.py) | 管理员 |
| 数据管理 | ES 文档分页浏览、单文档编辑(字段白名单)/删除、按来源批量清理 | frontend/src/views/DbAdminView.vue | api_admin/db/* | 管理员 |
| 管理员登录 | 登录签发内存态 token、按 IP 限速(5 次失败锁 15 分钟)、登出吊销 | frontend/src/views/LoginView.vue、components/LoginForm.vue | api_admin_login / api_admin_logout | 公开/管理员 |
| SEO | robots.txt 与 sitemap.xml 动态生成(AI 详情页进 sitemap) | — | search/seo_views.py | 公开 |
| 健康探针 | 真检 ES/Redis,任一不可用返回 503(供 Docker HEALTHCHECK) | — | api_health | 公开 |
| 图片代理 | 域名白名单 + Referer 伪装 + 本地磁盘缓存(防盗链图床) | frontend/src/api.ts 的 imgUrl() | api_img | 公开 |
| 采集告警 | 采集失败/0 条/misfire 邮件告警,同类事件 30 分钟冷却 | — | search/notify.py | — |
| 备份恢复 | 状态文件 + ES 快照 + 过期清理,拒绝同盘备份 | — | deploy/backup.sh | — |
这张功能表的背后是一条清晰的数据流水线:爬虫把公开信源写进 ES,所有页面都只是 ES 中同一份文档的不同视图。今日要闻是跨来源按发布时间的混排,新闻列表是按来源的过滤,AI 讯息是按内容标签的筛选,榜单是抖音源的排序展示——它们不各自存数据,只发出不同的查询。理解这一点后,两条使用旅程也容易串起来:匿名读者从首页的今日要闻或热搜词进入,搜索结果页承载"URL 即状态"的可分享检索(筛选条件全部编码在 query string 里),详情页按 ES _id 精确取文;站主的日常则是"看数据概览确认采集健康 → 采集管理页启动/排程爬虫 → 翻任务历史与日志尾部排障 → 必要时在数据管理页清理低质来源"。功能间的依赖也在这条流水线上:热搜词 zset 由读者的搜索行为实时累积、又反过来在首页引导搜索;采集告警邮件、robots/sitemap、健康探针都是围绕这条流水线的保障设施。
| 层级 | 技术 | 版本(锁定于 XSearch/requirements-lock.txt) | 选型理由(源自源码注释与 README) |
|---|---|---|---|
| 爬虫框架 | Scrapy + scrapy-playwright | 2.18.0 / 0.0.48 | Scrapy 是 Python 爬虫事实标准;2.13+ 提供异步 start() 入口与新中间件签名。Playwright 替代老课程的 Selenium 做动态渲染,meta={"playwright": True} 即可启用 |
| 浏览器指纹 | browserforge | 未锁版(有内置降级模板) | 老课程只换 UA,2025 年后的风控看整组信号(UA 与 sec-ch-ua 自洽等);browserforge 生成"成套"请求头,未安装时降级为 Chrome 124 模板(crawler/ai/fingerprint.py) |
| 检索引擎 | Elasticsearch + IK 分词 | 8.19.3 客户端 / 8.15.3 服务端 | 全文检索核心;中文场景必须 IK 插件(deploy/es-ik.Dockerfile 定制镜像),建索用 ik_max_word 细粒度、检索用 ik_smart + 停用词(crawler/pipelines.py) |
| 缓存/统计 | Redis | redis-py 5.2.1 | 三用途:热搜词 zset 统计(search_keywords_set)、headlines/新闻列表/sitemap 缓存、(历史上)去重 |
| 后端框架 | Django + gunicorn | 6.1.1 / 23.0.0 | 纯 JSON API 后端(无模板渲染页面);Django 自带 admin/session/中间件体系。gunicorn 必须 --workers 1:管理员 token、爬虫互斥锁、定时调度器都是进程内状态 |
| 定时调度 | APScheduler | 3.11.2 | BackgroundScheduler 支持 cron 表达式;项目特意配置 misfire_grace_time=3600 + coalesce=True,避免后端重启错过触发窗口后静默漏跑一整天(search/crawl_manager.py) |
| 关系存储 | SQLite | 内置 | 仅存 Django 会话等框架数据,业务数据全在 ES。生产以单文件 bind mount 持久化(docker-compose.prod.yml) |
| 前端框架 | Vue 3 + Vue Router | 3.5 / 4.5 | 国内中小项目快速落地首选(AI升级说明.md 第五节);Composition API + <script setup> |
| 构建工具 | Vite | 6.0 | 官方工具链;dev 态 /api 代理到 8000 端口免 CORS(frontend/vite.config.ts) |
| 组件库 | Element Plus | 2.9 | Vue3 组件库默认选项;配合 @element-plus/icons-vue |
| 类型系统 | TypeScript + vue-tsc | 5.9 / 3.3 | tsconfig.json 开启 strict 全套;npm run build 先跑 vue-tsc --noEmit 全量类型检查,充当"构建期测试" |
| 部署 | Docker Compose / Nginx / frp | ES 8.15.3、nginx 1.27-alpine | 开发态 compose 四容器;生产态(NAS)ES/Redis/后端不发布端口,公网唯一入口经 frp 隧道到 VPS Nginx + HTTPS |
| CI | GitHub Actions + ruff | ruff(E9+F) | 只门禁"真错误"(语法/未定义名),刻意不启用风格规则集(ruff.toml 注释解释了原因) |
表里几处"反直觉"的设定其实是同一套哲学的不同侧面,可以合并理解。gunicorn --workers 1:管理员 token、爬虫互斥锁、调度器全是进程内状态,多 worker 会各自持有一份不一致的状态——与其引入分布式协调,不如用部署约束(单 worker)锁死前提,请求并发由 gunicorn 线程/异步 IO 承接,容量不足时的升级路径(Scrapyd、状态外置)在《01-系统架构》有明确讨论。SQLite 只存框架数据:业务数据全在 ES,SQLite 若也存业务数据就成了"两处真相",刻意只留 Django 会话表避免了这个问题。ruff 只开 E9+F:个人维护的站点,风格争议交给开发者自觉,流水线只拦"必定是错误"的代码,把 CI 时间留给前端的 vue-tsc 全量类型检查——后者在这个无后端类型契约的纯 JSON API 架构里,实际上是"前后端契约"唯一自动化的守护者。IK 分词必须自建镜像:官方 ES 镜像不含 IK 插件,而建索引时 ik_max_word/ik_smart 是硬依赖,所以 deploy/es-ik.Dockerfile 把插件固化进镜像,杜绝"换台机器忘了装插件"的部署漂移。
XSearch/ # 仓库根
├── README.md # 项目自述:功能/结构/快速开始/API 表/发布清单
├── AI升级说明.md # 技术栈升级记录:移除了什么、为什么、合规立场
├── Dockerfile # 后端镜像:python:3.12-slim + Playwright + gunicorn
├── frontend.Dockerfile # 前端镜像:node:20 构建产物交给 nginx:alpine
├── docker-compose.yml # 开发部署:ES/Redis 只绑 127.0.0.1,四容器
├── docker-compose.prod.yml # 生产部署(NAS):仅前端暴露 127.0.0.1:5600,bind mount 状态
├── .env.example # 环境变量模板(22 项,含注释)
├── ruff.toml # lint 配置:仅 E9 + F,line-length 120
├── .github/workflows/ci.yml # CI:后端 lint+测试 / 前端类型检查+构建+体积守门
├── deploy/
│ ├── es-ik.Dockerfile # ES 8.15.3 + analysis-ik 插件定制镜像
│ ├── es.elasticsearch.yml # ES 配置:path.repo=/snapshots(快照备份依赖)
│ ├── nginx.conf # 前端 nginx:SPA 兜底 + /api/ 反代 + SEO 路径转发
│ └── backup.sh # 每日备份:状态文件 + ES _snapshot + 14 天保留
└── frontend/ # Vue 3 前端工程
├── index.html # SPA 入口(含兜底 meta description / og 标签)
├── package.json # 脚本:dev / build(vue-tsc + vite)/ type-check
├── vite.config.ts # 端口 5173/4173,/api 代理到 8000
├── tsconfig.json # strict 全套 + noUnusedLocals/Parameters
└── src/
├── main.ts # 入口:10 条路由(懒加载)+ 守卫 + meta 管理 + chunk 自救
├── App.vue # 根组件:导航/登录弹窗/退出登录
├── api.ts # 类型化 API 客户端:34 个接口封装 + ApiError + 高亮渲染
├── auth.ts # 登录态:localStorage token + ADMIN_PATHS 路由清单
├── style.css # 全局样式
├── components/LoginForm.vue # 登录表单(弹窗与登录页共用)
└── views/ # 9 个页面组件
├── HomeView.vue # 首页:检索入口 + 今日要闻混排
├── SearchView.vue # 搜索:URL 即状态、分面/排序/纠错/分页
├── NewsView.vue # 新闻列表(三源 Tab)
├── AiView.vue # AI 讯息(精选/日报/热点榜三 Tab,日报前端解析渲染)
├── AiDetailView.vue # AI 条目详情(按 _id 取文)
├── RankingsView.vue # 抖音榜单
├── StatsView.vue # 数据概览
├── CrawlView.vue # 采集管理(启动/状态/定时任务/统计/历史)
├── DbAdminView.vue # 数据管理(浏览/编辑/删除/清理)
└── LoginView.vue # 独立登录页
└── XSearch/ # Django 后端 + Scrapy 爬虫(合并后统一项目)
├── manage.py # Django 管理入口
├── scrapy.cfg # Scrapy 配置(指向 xsearch.settings 模块)
├── requirements.txt # 依赖范围(开发用)
├── requirements-lock.txt # 依赖锁定(Docker/CI 共用,保证可复现)
├── local_settings.py.example # 私有配置模板(管理员账号/告警邮箱,本体已 gitignore)
├── db.sqlite3 # SQLite(Django 会话等框架数据)
├── schedules.json # 定时任务持久化(ScheduleManager 写)
├── crawl_history.json # 采集历史持久化(CrawlManager 写,上限 200 条)
├── logs/ # 爬虫 stdout/stderr 日志(crawl_<时间戳>.log)
├── jobs/ # Scrapy JOBDIR 断点目录(支持中断恢复)
├── cache/images/ # 图片代理磁盘缓存(MD5 文件名)
├── static/ # 旧版服务端渲染遗留静态资源(现前端已独立)
├── xsearch/ # Django 项目配置包
│ ├── settings.py # 全局配置:安全 fail-closed / ES / Redis / 告警 / 日志
│ ├── urls.py # 路由表:29 条 path(API + admin + robots/sitemap)
│ ├── middleware.py # 白名单式 CORS 中间件(CorsMiddleware)
│ └── wsgi.py # WSGI 入口(gunicorn 指向它)
├── search/ # 搜索 + 采集管理 Django app
│ ├── apps.py # SearchConfig.ready:启动调度器 + 弱配置告警
│ ├── api_views.py # 全部 JSON API(约 1150 行,25 个视图函数)
│ ├── crawl_manager.py # CrawlManager(子进程爬虫)+ ScheduleManager(cron)
│ ├── seo_views.py # robots.txt / sitemap.xml
│ ├── notify.py # 邮件告警(后台线程 + 30 分钟冷却)
│ ├── models.py # 已迁移:mapping 统一由 crawler/pipelines.py 维护
│ ├── admin.py # 空(未注册模型)
│ └── tests.py # 22 个测试类 / 110 个测试方法(全 mock,不依赖 ES/Redis)
└── crawler/ # Scrapy 爬虫包(原 ArticleSpider,已合并)
├── settings.py # Scrapy 配置:robots 合规 / 限速 / 中间件 / playwright
├── middlewares.py # 指纹头 / 住宅代理 / Playwright 降级三个中间件
├── pipelines.py # QuoteDocument(索引 mapping)+ EsArticlePipeline
├── ai/fingerprint.py # browserforge 指纹头生成(含降级模板)
└── spiders/ # 5 个爬虫
├── news_rss.py # 新闻 RSS 聚合(中新网/IT之家/Solidot)
├── news_backfill.py # 中新网历史存档回填(默认 180 天,幂等)
├── aihot_news.py # AI 精选动态 + AI 日报(AIHOT 官方 API)
├── aihot_hot.py # AI 热点榜 Top10(含跌榜清理)
└── douyin_hot.py # 抖音热点榜 50 条(Playwright 渲染 + 文本解析)这份目录有两处"历史层积"值得先说明,避免误读。其一,仓库根的 frontend/ 与 XSearch/ 是一个仓库内的前后端两个工程:XSearch/XSearch 这个同名嵌套目录是 Django 项目包(配置)与 search(API)、crawler(爬虫)两个 app 的容器,scrapy.cfg 指向 xsearch.settings 说明爬虫与 Django 共用一套配置加载——历史上爬虫(ArticleSpider)与后端是两个项目,合并后才统一到一根 requirements-lock.txt。其二,根下的 static/ 是早期"服务端渲染模板页"时代的遗留,现前端已独立为 Vue SPA,该目录不再有页面入口;db.sqlite3、schedules.json、crawl_history.json、logs/、jobs/、cache/images/ 这组文件/目录则分别对应"会话、定时任务、采集历史、爬虫日志、断点恢复、图片缓存"六类运行时状态,生产部署以 bind mount 持久化(见 docker-compose.prod.yml),备份策略见 deploy/backup.sh。测试方面 search/tests.py 以 22 个测试类 110 个方法全量 mock 外部依赖,是这份轻量目录里最厚的一块质量资产。
以下步骤摘自 README.md 并经源码核对。本机开发态不需要配置任何环境变量即可跑通(此时 DEBUG=True,使用仅开发态的密钥,启动日志会给出提示)。
第 1 步:启动依赖服务(Elasticsearch + Redis)
docker compose up -d elasticsearch redis注意:不能用 stock elasticsearch:8.x 镜像。索引 mapping 依赖 ik_max_word/ik_smart 分词器(crawler/pipelines.py 的 QuoteDocument),缺 IK 插件时第一次采集会在建索引阶段失败。compose 会现场构建 deploy/es-ik.Dockerfile(官方 8.15.3 镜像 + infini.cloud 源安装 analysis-ik 插件)。
第 2 步:安装并启动后端
cd XSearch
pip install -r requirements-lock.txt # 与 Docker 构建一致
playwright install chromium # 仅采集抖音榜需要
python manage.py migrate # 初始化 SQLite(会话表)
python manage.py runserver 127.0.0.1:8000第 3 步:启动前端
cd frontend
npm install
npm run dev # http://localhost:5173,Vite 已把 /api 代理到 8000第 4 步:采集数据
cd XSearch
scrapy crawl news_rss # 新闻 RSS 聚合(3 源)
scrapy crawl aihot_news # AI 精选 + 当日 AI 日报
scrapy crawl aihot_hot # AI 热点榜
scrapy crawl news_backfill -a days=180 # 中新网历史回填(幂等)
scrapy crawl douyin_hot # 抖音热点榜(需 playwright)也可以在前端"采集管理"页面一键启动(需先登录)。若要使用管理功能,复制 XSearch/local_settings.py.example 为 local_settings.py 并设置管理员账号(该文件已加入 .gitignore,严禁提交真实账号)。
第 5 步:运行测试
cd XSearch
python manage.py test search # 110 个测试方法,全部 mock 依赖,本机无需 ES/Redis本套文档共 7 篇,按"总览 → 架构 → 页面 → 接口 → 数据 → 流程 → 部署"排列,对应软件系统的静态结构到动态行为的完整切面:
所有结论均来自对源码的逐文件分析,引用的文件路径相对 XSearch/ 仓库根目录。个别无法从源码确认的信息以 ⚠️ 待确认 标注。