抓取得到APP课程到本地存档
把 dedao.cn 付费课程(需用户已购)完整抓成本地 HTML 站点:每讲一个文件夹(HTML + images/),外加可搜索的 index.html。已在万维钢 7 门课(2115 讲)和卓克 7 门课(2019 讲)上全量验证,零遗漏。
何时用本 skill
- 用户给出得到课程页 URL(
https://www.dedao.cn/course/detail?id=...),要求抓取全部文章 - 用户要求备份/存档得到专栏内容到本地
- 前提:用户本机 Chrome 已登录得到(登录态通过复制 Chrome 配置复用,不需要密码)
总体流程(六步)
0. 准备: 检查依赖 → 启动调试 Chrome(复用登录态) → 验证登录
1. 配置: 按【配置规范】写 config.json(课程列表 + 两条命名规则)
2. 列表: uv run scripts/crawl_lists.py → 文章清单(与官网计数核对!)
3. 收割: uv run scripts/harvest_contents.py → 逐篇正文(可断点续跑)
4. 建站: uv run scripts/build_site.py → HTML+图片(命名模板生效)
(可选) uv run scripts/build_index.py → index.html
5. 广告: uv run scripts/triage_repeat_images.py → 重复图鉴定报告
人工复核 → 定稿 blacklist.json → uv run scripts/remove_ads.py
6. 收尾: bash scripts/teardown_chrome.sh(删Cookie副本!) → 汇报核对数字
第 0 步:环境与登录态(关键坑最多,严格照做)
依赖: google-chrome、uv、tesseract(OCR 用,可选)、curl。脚本用 PEP 723 内联依赖(uv run 自动装 websocket-client/requests/pillow/pyzbar)。
启动调试 Chrome —— 必须在单独的 Bash 调用中执行(和其它命令混在一起时后台进程会被回收):
bash scripts/setup_chrome.sh 9223 /tmp/dedao-fetch-work
它做的事:复制 ~/.config/google-chrome/Default/Cookies + Local State 到 /tmp/dedao-fetch-work/chrome-profile/,再以 headless + --remote-debugging-port=9223 --remote-allow-origins='*' 启动。Chrome 自己经 XDG Secret Portal 解密 Cookie(KWallet 里查不到密钥,别走弯路)。
验证登录(页内应出现 token cookie;未登录则让用户先在自己 Chrome 里登录得到):
uv run scripts/check_login.py --work /tmp/dedao-fetch-work
第 1 步:配置规范(config.json)
三项用户输入对应关系:①课程名+URL → courses[];②总目录命名 → root_dir(支持 {date} {year} {month} {author} 模板);③每讲文件夹命名 → folder_template。
{
"author": "万维钢",
"base_dir": "/home/desmond/Books-and-Articles",
"root_dir": "{date}-{author}",
"courses": [
{"key": "S1", "name": "精英日课", "url": "https://www.dedao.cn/course/detail?id=93N5e6Rya4ZJjQYsQgVOmGApwlo08D"}
],
"folder_template": "{course}-{module}-{date}-{title}",
"strip_module_prefix": true,
"strip_number_prefix": false,
"work_dir": "/tmp/dedao-fetch-work",
"chrome_port": 9223,
"tabs": 5
}
字段说明:
| 字段 | 说明 |
|---|---|
author |
作者名,用于 root_dir 模板与页面标题 |
base_dir + root_dir |
root_dir 为绝对路径时直接用;否则拼 base_dir/root_dir。支持模板变量 {date}(当天 YYYY-MM-DD)/{year}/{month}/{author};纯字面量(如 2026-08-25-万维钢)也合法 |
courses[].key |
短代号(缺省自动 C01/C02...),courses[].name 是用户指定的课程名,直接用于 {course};url 里的 id= 参数即 detail_id |
folder_template |
每讲文件夹名。变量:{course} 课程名、{module} 模块/章节名(自动去(N讲)后缀和书名号《》)、{date} 发布日(北京时间)、{number} 文章编号(见下)、{title} 标题 |
strip_module_prefix |
true 时标题去掉与模块同名的《书名》前缀(万维钢式书课推荐开) |
strip_number_prefix |
true 时从 NNN|标题 前缀提取 {number}(卓克科技参考式推荐开);课程内有编号体系时,无编号文章(发刊词等)回退 000,否则用序号 |
命名模板实例(已验证):
- 万维钢式:
{course}-{module}-{date}-{title}+strip_module_prefix:true→精英日课2-你能做任何工作-2017-09-18-1_"文科生"的反击_软技能的时代 - 卓克式:
{course}-{date}-{number}_{title}+strip_number_prefix:true→科技参考2-2022-01-07-005_元宇宙的一盆凉水
文件名净化规则(自动):\ / : * ? " < > | : | 丨 , , 及空白→_;连续_折叠;HTML 文件名与文件夹同名,存在 images/ 子目录;题图存为 header_*。
第 2~4 步:跑流水线
CFG=/tmp/dedao-fetch-work/config.json
uv run scripts/crawl_lists.py --config $CFG # 全局翻页+逐章节count=0 双保险去重
uv run scripts/harvest_contents.py --config $CFG # 5标签并行, ~80篇/分钟, 断点续跑
uv run scripts/build_site.py --config $CFG # 生成HTML+下载图片+宽幅广告过滤
uv run scripts/build_index.py --config $CFG # index.html 总目录(搜索/筛选)
每步都要核对:
- crawl_lists 日志里
SAVED n (claimed m)—— n 必须等于 m(官网计数),少了就重跑(幂等); - harvest 后
find <work>/contents -name '*.json' | wc -l应等于总讲数,缺的重跑补收; - build 后抽查:用户给过的示例文章文件夹名、图片引用无缺失(
scripts/audit.py --config $CFG)。
长任务用后台运行 + 轮询日志(crawl_lists.log / harvest.log 里每 25/80 篇打点)。
第 5 步:广告图过滤(两重,缺一不可)
第一重(自动,已内建在 build_site):无图注且宽高比≥3 的横幅(标准 1080×260)删除;块里没尺寸的老图下载后 PIL 实测。带图注的宽图是内容图表,不能删。
第二重(半自动,必须做):非宽幅推广图(订阅海报/公众号引流/小程序推广,每篇 URL 不同但字节相同)。内容图(书封/漫画/手写笔记)也会跨文章重复,不能见重复就删:
uv run scripts/triage_repeat_images.py --config $CFG
产出 <work>/triage/report.json(OCR+二维码自动判定)+ <work>/triage/gNNN.jpg(每组代表图)。然后:
- 自动判 AD 的直接采信;
- 自动判 CONTENT 但 n≥3 的大重复组(广告 campaign 通常几十上百次)以及你怀疑的组,逐张看图复核——推广特征:二维码、"订阅/扫码/立即购买/重磅首发/版权归得到App所有/未经许可/商务合作/等你归队/小剧场"等字样;
- 把确认的组 md5 写进
<work>/ads_blacklist.json(格式[{"md5":"...","note":"..."}]),可并入assets/known_ads_blacklist.json(已验证的 12 组通用推广图); uv run scripts/remove_ads.py --config $CFG移除(HTML 去掉对应<figure>+ 删文件),再跑scripts/audit.py确认零残留零悬空。
第 6 步:收尾(必做)
bash scripts/teardown_chrome.sh /tmp/dedao-fetch-work # 杀调试Chrome + 删除含Cookie的profile副本
向用户汇报:各课程 n/n 计数、总讲数、HTML/图片数、广告移除数、root 目录路径。提醒:抓取数据仅供个人学习。
关键坑备忘(违反任何一条都会返工)
- 正文不在页内 fetch 里:
article/info→dd_article/get/v2重放拿不到正文(服务端按页面加载时序 withholding)。唯一可靠方式 = 逐篇导航真实文章页 + CDPNetwork.getResponseBody收割 ddarticle 响应。 - 正文在响应的
c.content,不是c.article.content。 - 列表翻页用
max_id(上一页最后一篇的 id);since_id无效;每章chapter_id=<id>, count:0一次取全。 - 图片文件名冲突:不同 URL 尾段可能同名(
.../<不同id>/031519.png),必须按 URL 哈希去重,否则同图重复+原图丢失。 - 调试 Chrome 必须单独 Bash 调用启动;CDP ws URL 里
127.0.0.1要换localhost;无--remote-allow-origins='*'会 403。 - 直连 requests 会被反爬拦(h.c=104000),一切 API 调用走页内 fetch 或页面自身请求。
- 发布时间是 UTC 秒,展示日期按 北京时间(+8) 转换。
- 图片 CDN(piccdn*.umiwi.com)可直连下载,带 UA+Referer 即可。
- 任务结束必须 teardown(Cookie 副本很敏感)。
产出结构
<root>/
├── index.html # 总目录: 课程分组+搜索+筛选
├── _manifest.json # 全部文章元数据(标题/时间/摘要/相对链接)
└── <每讲文件夹>/ # 命名按 folder_template
├── <同名>.html # 标题/发布时间/摘要/题图/正文/图片
└── images/*.jpg # 正文图片(原位置)+header_题图