很多爬虫是「命令行一把梭」——自己用没问题,但给非技术同事用就头疼:要配环境、要敲命令、要看黑框框。
MediaCrawler 附带的 WebUI 解决了这个痛点:网页上配参数、点启动、看实时日志。今天拆它的前后端,重点看一个通用范式——怎么把长任务的日志实时推到浏览器。
一、整体架构┌────────────┐ HTTP/REST ┌──────────────┐ subprocess ┌──────────┐
│ React 前端 │ ───────────▶ │ FastAPI 后端 │ ───────────▶ │ main.py │
│ (webui/) │ ◀─────────── │ (api/) │ stdout 读取 │ 爬虫进程 │
│ 配置/数据 │ WebSocket │ CrawlerManager│ ◀──────────── │ │
└────────────┘ 实时日志 └──────────────┘ └──────────┘启动:
uvicorn api.main:app --port 8080 --reload二、三个路由,各司其职app.include_router(crawler_router, prefix="/api") # 启动/停止爬虫
app.include_router(data_router, prefix="/api") # 浏览已采集数据
app.include_router(websocket_router, prefix="/api") # WebSocket 实时日志三、核心:CrawlerManager子进程管理这是精髓——它不直接 import 调用爬虫,而是用 subprocess 启动独立进程,再异步读 stdout:
class CrawlerManager:
def __init__(self):
self.process: Optional[subprocess.Popen] = None
self.status = "idle"
self._logs: List[LogEntry] = []
self._log_queue: Optional[asyncio.Queue] = None
def get_log_queue(self):
if self._log_queue is None:
self._log_queue = asyncio.Queue()
return self._log_queue为什么用子进程而不是直接调用?
隔离——爬虫崩了不影响 Web 服务;可控——能随时 terminate 进程(对应「停止」按钮);日志自然流出——直接读子进程 stdout。CrawlerManager.start()
├─ Popen(["uv","run","main.py"]) 启动爬虫子进程
├─ 异步任务读 stdout → 逐行塞入 log_queue
└─ WebSocket 路由从 log_queue 取日志 → 推给前端 技巧:「Web 面板 + 独立子进程 + stdout 管道」是 CLI 工具「Web 化」的通用范式。核心就一句:别在 Web 进程里跑长任务,用子进程 + 日志管道。
四、WebSocket 实时日志:生产者-消费者解耦前端 <──websocket── 后端(websocket_router) ▲ │ asyncio.queue 消费 crawlermanager._log_queue 技巧:日志用 asyncio.Queue 做「生产者-消费者」解耦——爬虫进程是生产者,WebSocket 是消费者,中间靠队列缓冲,快慢不互相拖累。
五、前端:React 组件化webui/src/
├── components/
│ ├── config/ # CrawlerConfigPanel 配置面板
│ ├── console/ # Terminal 日志终端
│ └── data/ # DataExplorer 数据浏览
├── store/ # 状态管理
└── i18n/ # 国际化前端构建后由 FastAPI 静态托管,一条命令即部署。
结尾「命令行 → Web 面板」的工程化思路拆完了:子进程隔离 + 日志队列 + WebSocket 推送。这套组合拳几乎是所有「后台任务可视化」的标准答案。
最后两篇进入实操收官:下一篇《像搭积木一样加平台!5步法》教你照着现有平台「抄」出一个新平台。
你会给自己的脚本加个 Web 面板吗?评论区聊聊,点个关注,收官篇更干货。
⚠️ 免责声明:本文仅供学习研究,请遵守平台条款与 robots.txt,控制请求频率,勿用于商业或非法用途。