第三方爬虫与存档站,长期保存洛谷社区的文章 / 剪贴板 / 犇犇 / 陶片放逐 / 题目信息,并对用户名、难度、题解开放状态等做版本与时间序列追踪。
| 层 | 选型 |
|---|---|
| 后端 | Python 3.11+ / FastAPI / SQLAlchemy 2.0 (async) |
| 数据库 | MySQL 8(aiomysql 异步 + pymysql 同步兜底) |
| 任务队列 | Redis Lua 资源队列(3 条严格优先级队列) |
| 定时调度 | APScheduler |
| 爬虫 | httpx (HTTP/2) + lentille-context / _feInjection SSR 提取 |
| 渲染 | 前端 markdown-it + KaTeX;后端 markdown-it-py 管线 + 洛谷语法插件 |
| 前端 | Nuxt 3(Vue 3 + SSR)+ Pinia |
| 认证 | argon2(密码)+ TOTP(管理员 2FA)+ JWT(站内用户) |
| 邮件 | Resend HTTP API(推荐)/ SMTP |
| 反代 | Nginx(宝塔或裸机均可) |
| 进程管理 | start.sh / stop.sh(后台进程 + pid 文件)或 systemd |
luogu-archive/
├── backend/ # FastAPI + 爬虫 + 任务队列
│ ├── app/
│ │ ├── api/v1/ # save / content / user / auth / admin_auth / admin_panel / takedown
│ │ ├── auth/ # argon2 / TOTP / JWT
│ │ ├── core/ # 配置 / DB / Redis / 限流 / 锁 / 日志 / 邮件 / 验证码 / 异常
│ │ ├── crawler/
│ │ │ ├── nodes/ # 爬虫节点(anon/authed × 海外/主站,独立限流+熔断)
│ │ │ ├── sources/ # article/paste/user/feed/judgement/problem/discovery/image
│ │ │ ├── http.py # 统一 HTTP 客户端(限流/熔断/lentille 提取)
│ │ │ ├── cookies.py # Cookie 账号池(Redis INCR 轮询)
│ │ │ └── lentille.py # SSR JSON 提取
│ │ ├── models/ # SQLAlchemy ORM
│ │ ├── render/ # Markdown 渲染 + 洛谷专有语法插件
│ │ ├── tasks/ # 资源队列 broker + 任务定义
│ │ ├── main.py # FastAPI 入口
│ │ └── scheduler.py # APScheduler 入口
│ ├── alembic/ # 数据库迁移
│ ├── scripts/ # CLI 工具(create_admin 等)
│ ├── pyproject.toml
│ └── .env.example
├── frontend/ # Nuxt 3
│ ├── pages/ # 首页/文章/剪贴板/用户/陶片/犇犇/题目/登录注册/邮箱验证/管理后台
│ ├── components/ # 原文归属横幅+保存按钮 / 洛谷用户名 / 关注按钮 等
│ ├── composables/ # useApi / useAdminApi / useMarkdown / useTime
│ ├── layouts/ # default(侧边栏)/ admin
│ ├── public/ # favicon / robots.txt
│ └── .env.example
├── docs/
│ ├── nginx.conf.example # Nginx 反代模板
│ └── systemd/ # systemd unit 模板(可选部署方式)
├── start.sh / stop.sh # 进程管理(宝塔兼容)
└── scripts/backup-mysql.sh # MySQL 备份
爬虫按「单 worker × 目标域名」限速;同一 worker 内 anon/authed 共用完成冷却门,但熔断状态仍按节点身份记录:
| 节点 ID | 用途 | 速率 |
|---|---|---|
local-anon |
海外镜像 luogu.com:犇犇接口、发现页 | 与 local-authed 共用:请求完成后冷却 1 秒 |
local-authed |
海外镜像:带 Cookie 认证请求 | 与 local-anon 共用:请求完成后冷却 1 秒 |
local-anon-cn |
主站 luogu.com.cn:题目、陶片、标签字典 | 与 local-authed-cn 共用:请求完成后冷却 10 秒 |
local-authed-cn |
主站:认证请求 | 与 local-anon-cn 共用:请求完成后冷却 10 秒 |
/judgement、/problem、/_lfe 等路径在 crawler/http.py:_resolve_url 里强制走主站,对应也必须用 cn=True 的节点取得方式(get_default_node(kind, cn=True))。节点与域名错配会导致海外节点的限流计数被主站 0.1 req/s 污染、被错误熔断 —— 这是历史上"陶片保存不了"的根因。
熔断:单节点遇 429 / 明确反爬信号 → 该节点冷却 CRAWLER_BREAKER_COOLDOWN_SEC;10 分钟内连续 3 个节点被封 → 全局冷却。403/404 不直接熔断,走累计阈值 + 同类资源探针确认。
等待方式:任务依赖未满足时始终留在 pending 集合,不占 worker 执行槽,也不会反复出队、重新入队。
Cookie 账号池:需要认证的任务在领取时从 Redis 有序集合中选择最早可用账号。同一账号严格串行,请求完成后冷却 CRAWLER_AUTH_ACCOUNT_INTERVAL_SEC,并执行滚动一小时请求上限;账号失效自动禁用。
资源队列使用 3 条优先级。worker 始终按 crawler.hi、crawler.mid、crawler.low 顺序尝试原子领取;高优先级有可运行任务时绝不领取低优先级任务。若高优先级任务正在等待域名、账号或熔断恢复,则继续寻找下一优先级中可以立即执行的任务。
| 队列 | 内容 |
|---|---|
crawler.hi |
用户正在等待的任务:手动保存、首次访问未收录内容、管理员强制爬取 |
crawler.mid |
普通后台任务:发现、访问触发刷新、级联、定时犇犇 / 陶片 |
crawler.low |
所有题目相关任务:题目列表页轮询、题解开放状态检测 |
用户手动保存用户主页时,crawl_user_manual 会在同一条 hi 任务里连续刷新用户主页和犇犇第一页,避免主页已更新但犇犇还卡在后台队列。题目任务即使由手动保存触发也统一走 low,避免一次列表刷新污染 hi 队列。
题解开放状态按难度分档轮询,避免一次性把上千题塞爆主站 0.1 req/s 队列:
- tier1(入门 / 普及-):每小时,派距上次检查 ≥1h 的题
- tier2(普及/提高-):每天,≥24h
- tier3(其他档 + 暂无评定):每天派全档 1/7,7 天滚动一轮
列表页 cascade 派题解检测带 30 分钟 Redis NX 去重;scheduled / cascade 触发只派新题,老题交给分层轮询。这套机制是为了解决曾经队列堆积到 4 万+ 任务、把全站爬取拖死的问题。
站内用户注册走邮箱验证:
注册 → 发验证邮件(24h token)→ 点链接打开前端 /auth/verify
→ 调 GET /api/v1/auth/verify?token=xxx → 标记 email_verified → 才能登录
支持「重发验证邮件」(POST /auth/resend-verification,同邮箱 60s 冷却 + 同 IP 每小时 5 次,不泄露邮箱是否存在)。邮件后端可选 Resend HTTP API(推荐)或 SMTP。
- 文本字段(文章 / 剪贴板 / 用户介绍)逐版本保留,按内容 SHA-256 去重;超长内容用
LONGTEXT - 数值字段(follower / Elo / 咕值)存时间序列,不算版本
- 用户名违规级联隐藏:命中(陶片关键词 / 系统格式
_user_\d+/ 管理员手动)后,该时刻之前所有历史名永久隐藏、解封不恢复;改回合规名后新名正常显示
需要本地 MySQL 8 + Redis 7。
mysql -uroot -p -e "CREATE DATABASE luogu_archive DEFAULT CHARSET utf8mb4;"
mysql -uroot -p -e "CREATE USER 'luogu_archive'@'localhost' IDENTIFIED BY 'dev_password';"
mysql -uroot -p -e "GRANT ALL ON luogu_archive.* TO 'luogu_archive'@'localhost';"cd backend
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
cp .env.example .env
# 最少填:DB_PASSWORD / JWT_SECRET / ADMIN_TOTP_ENCRYPTION_KEY
# JWT_SECRET=$(python -c "import secrets;print(secrets.token_hex(32))")
# ADMIN_TOTP_ENCRYPTION_KEY=$(python -c "from cryptography.fernet import Fernet;print(Fernet.generate_key().decode())")
alembic upgrade head
python -m scripts.create_admin # 交互式创建管理员,记下 TOTP secret(只显示一次)
# 三个进程分别起:
uvicorn app.main:app --reload --port 8000
python -m scripts.priority_worker
python -m app.scheduler # 可选,开发期可手动触发cd frontend
npm install # 或 pnpm install
cp .env.example .env
# 编辑:NUXT_PUBLIC_API_BASE_URL=http://localhost:8000
npm run dev # http://localhost:3000目标:境外 Ubuntu/Debian 服务器(2 vCPU / 4 GB+),已装 MySQL 8 + Redis + Nginx + Node 20 + Python 3.11+。代码放在 /data/luogu-archive(宝塔默认)或自定义路径。
CREATE DATABASE luogu_archive DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'luogu_archive'@'localhost' IDENTIFIED BY '<强密码>';
GRANT ALL ON luogu_archive.* TO 'luogu_archive'@'localhost';
FLUSH PRIVILEGES;cd /data/luogu-archive/backend
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
cp .env.example .env
# 关键字段(见下方“配置项参考”):
# APP_ENV=production / APP_DEBUG=false
# DB_PASSWORD / JWT_SECRET / ADMIN_TOTP_ENCRYPTION_KEY
# WEB_PUBLIC_ORIGIN=https://你的域名 ← 邮件验证链接靠它,务必填对
# MAIL_PROVIDER=resend / RESEND_API_KEY / MAIL_FROM
# CAPTCHA_SITE_KEY / CAPTCHA_SECRET
alembic upgrade head
python -m scripts.create_admin生成密钥:
python -c "from cryptography.fernet import Fernet;print(Fernet.generate_key().decode())" # ADMIN_TOTP_ENCRYPTION_KEY
python -c "import secrets;print(secrets.token_hex(32))" # JWT_SECRETcd /data/luogu-archive/frontend
npm install
cp .env.example .env
# NUXT_API_INTERNAL_URL=http://127.0.0.1:8000 (或后端实际端口)
# NUXT_PUBLIC_API_BASE_URL=https://你的域名
npm run buildcd /data/luogu-archive
./start.sh # 启动 backend / worker / scheduler / frontend
./start.sh status # 查看状态start.sh 把进程放后台,pid 写 run/,日志写 logs/。后端默认监听 127.0.0.1:8000(或 .env 的 WEB_PORT),前端 127.0.0.1:3000。
参考 docs/nginx.conf.example:把域名反代到前端 3000,/api/ 反代到后端。宝塔可在面板里配反代 + 一键 SSL;裸机用 certbot。
如果不用 start.sh 而用 systemd,docs/systemd/ 下有 unit 模板(web / frontend / scheduler / worker)。
cd /data/luogu-archive
./stop.sh backend && ./start.sh backend # 改了后端代码/.env 后
./stop.sh worker && ./start.sh worker # 改了爬虫/actor 后
./stop.sh scheduler && ./start.sh scheduler # 改了定时任务后
# 前端改动需重新 build
cd frontend && rm -rf .nuxt .output && npm run build
cd .. && ./stop.sh frontend && ./start.sh frontendtail -f /data/luogu-archive/logs/backend.log
tail -f /data/luogu-archive/logs/worker.log
grep -iE "judgement|RateLimit|Blocked" /data/luogu-archive/logs/worker.log | tail -30# 查看三条队列的 pending + inflight 总数
cd /root/luogu-archive/backend
.venv/bin/python -c "from app.tasks.broker import *; b=get_broker(); print({q:b.queue_size(q) for q in QUEUE_ORDER})"
# 只读检查 / 确认后清理犇犇积压
.venv/bin/python -m scripts.clean_feed_queue
.venv/bin/python -m scripts.clean_feed_queue --apply
# 清节点熔断状态
redis-cli -a <密码> --scan --pattern 'crawler:breaker:*' | xargs -r redis-cli -a <密码> DEL
# 清保存去重锁(保存按钮“没反应”时)
redis-cli -a <密码> DEL save:pending:judgement:all lk:crawl:judgement:all登录 /admin/login(管理员账号 + 密码 + TOTP)→「爬取账号」→「录入新账号」,填 _uid + __client_id(浏览器登录洛谷后 F12 → Application → Cookies 取)。保号原则:别在控制台狂点测试。
scripts/backup-mysql.sh 做 mysqldump,建议挂 cron 每天跑一次。
后端 .env(完整见 backend/.env.example):
| 变量 | 说明 |
|---|---|
APP_ENV / APP_DEBUG |
生产填 production / false |
DB_* |
MySQL 连接 |
REDIS_URL |
Redis(含密码,如 redis://:pass@127.0.0.1:6379/0) |
CRAWLER_BASE_URL |
默认 https://www.luogu.com.cn |
CRAWLER_ANON_RATE_PER_SEC / CRAWLER_AUTH_RATE_PER_SEC |
兼容旧配置;单 worker 在请求完成后按域名冷却:luogu.com 1 秒,luogu.com.cn 10 秒 |
CRAWLER_AUTH_ACCOUNT_INTERVAL_SEC |
同一 Cookie 账号请求完成后的冷却时间,默认 5 秒;Redis 跨线程、进程和 worker 共享 |
NODE_ID |
多机部署时每台 worker 填唯一值,单机留空 |
JWT_SECRET |
站内用户 JWT 密钥(256bit hex) |
ADMIN_TOTP_ENCRYPTION_KEY |
管理员 TOTP secret 加密用 Fernet key |
WEB_PUBLIC_ORIGIN |
对外域名,邮件验证链接靠它拼,务必填对 |
WEB_CORS_ORIGINS |
允许的前端来源,逗号分隔 |
MAIL_PROVIDER |
resend 或 smtp |
RESEND_API_KEY / MAIL_FROM |
Resend:MAIL_FROM 须为已验证域名地址 |
SMTP_* |
SMTP 配置 |
CAPTCHA_PROVIDER / CAPTCHA_SITE_KEY / CAPTCHA_SECRET |
Turnstile / hCaptcha |
SAVE_IP_WINDOW_* |
保存按钮 IP 限流 |
IMAGE_MIRROR_* |
图片镜像存储 |
前端 .env:
| 变量 | 说明 |
|---|---|
NUXT_API_INTERNAL_URL |
SSR 服务端访问后端的内网地址(如 http://127.0.0.1:8000) |
NUXT_PUBLIC_API_BASE_URL |
浏览器访问后端的地址(生产填对外域名) |
NUXT_PUBLIC_CAPTCHA_PROVIDER / NUXT_PUBLIC_CAPTCHA_SITE_KEY |
人机验证 |
MIT(代码)。存档内容版权归洛谷原作者所有。