4KHD Downloader
migmg.ssuu.uk(4KHD 系采集站)画廊批量下载器。浏览器里用油猴脚本把画廊推给本地程序,
程序解析分页页、拿到全部图片直链、多线程下载到本地。
程序结构和交互沿用 eh-downloader(E-Hentai 版), 但站点协议层是照着 4KHD 的真实页面重写的。
快速上手
1. 构建
前置:Go 1.24+,以及 Wails CLI v2 和 WebView2 运行时(Win10/11 一般已自带)。
# 装 Wails CLI
go install github.com/wailsapp/wails/v2/cmd/wails@v2.16.0
# 构建
cd hd4k-downloader
wails build
产物在 build\bin\4KHD-Downloader.exe。
本机 Go 装在
%USERPROFILE%\.local\go,不在系统 PATH 里, 调用前需要$env:PATH = "$env:USERPROFILE\.local\go\bin;$env:PATH"。
2. 装油猴脚本
把 userscript/hd4k-to-downloader.user.js
拖进 Tampermonkey 即可,无需额外配置(端口和 token 已写死在内)。
- 画廊详情页:标题下面出现「📤 发送画廊地址」按钮
- 列表页:每条画廊前面出现复选框,右下角浮出批量工具条
3. 下图
点推送 → 程序窗口自动弹到前台 → 「开始下载」。
也可在程序里「导入链接」,粘贴一批 URL(每行一条)手动加任务。
它是怎么工作的
浏览器 (油猴脚本)
│ GM_xmlhttpRequest → http://127.0.0.1:37778/add
▼
本地桥口 (Go, 只监听 127.0.0.1)
│ {url, title} → 解析成 GalleryRef,URL 域名归一化,入队
▼
下载引擎
│ 抓第 1 页 → 从 .page-link-box 读总页数 → 并发抓 2..N 页
│ 从 HTML 抽 i0.wp.com 直链 → 过滤推荐位 → 按页去重
│ 多线程下载 → .part 临时文件 → 原子改名
▼
下载目录/<模板>/xxx-001.webp … + .hd4k.json(清单,用于续传)
和 eh-downloader 的关键差异
| EH Downloader | 4KHD Downloader | |
|---|---|---|
| 任务主键 | gid + token | id(主域名 + 路径) |
| 图片地址 | hath 签名链接,会过期 | 稳定 URL 哈希,永久有效 |
| 续传方式 | 清单存直链,重启 0 请求 | 同左(4KHD 更彻底) |
| 抓取请求数 | 2N+1(列表 + 详情 ×2) | N(只有分页页) |
| 分辨率 | 可选 xres=org 等 |
走源站直链(不用 Photon 代理,否则免费额度打爆会被 429 封) |
| 目录占位符 | {title} {gid} {artist} {group} {series} {category} {lang} {tag1} {tags:3} |
{title} {name} {slug} {host} {category} {id} |
| 桥口端口 | 37777 | 37778 |
4KHD 没有 EH 那套 artist/group/series/tags 标签体系,站点真实提供的元数据只有 标题、路径名、域名和分类数字,所以占位符和预设都相应变少了。
站点适配(重要)
这类采集站随时换域名。实测 www.4khd.com、m.4khd.com 现在全部 404,
只有 migmg.ssuu.uk 活着。所以域名做成了设置项,不用改代码:
- 设置 → 站点域名 → 加一行即可
- 保存后,油猴脚本下次加载会从桥口
/info读到最新域名列表
域名归一化
页面上散布的链接未必指向主域名。实测 migmg.ssuu.uk 首页里 90 个
/content/ 链接全部指向已 404 的 www.4khd.com。
所以 4khd.com、www.4khd.com、m.4khd.com 都配成了 migmg.ssuu.uk 的
别名:不管用户从哪个域名复制链接,后端都会归一到主域名。
不归一的话,同一个画廊会因为「从哪个域名复制的链接」变成两个不同任务,
而且引擎会去请求一个死域名。
分页后缀(…/x.html/2)也会被剥掉,避免 /2 和 /1 变成两个任务。
协议层是怎么定下来的
全部基于 2026-10-11 对真实页面的实测,不是照着 HTML 猜的。几个关键点:
图片地址:页面给的是 Jetpack Photon 代理,但我们不直接用
页面里的图片地址长这样:
https://i0.wp.com/pic.4khd.com/{seg1}/{seg2}/{seg3}/w1300-rw/{name}.webp?w=1300
去掉 https://i0.wp.com/ 前缀、丢掉 ?w=1300 查询串,就是源站直链:
https://pic.4khd.com/{seg1}/{seg2}/{seg3}/w1300-rw/{name}.webp
解析时一律转成源站直链(OriginURL())。原因见下面的「为什么不用 Photon」。
- 路径里的
w1300-rw是原站自己的缩略图目录,必须保留; Photon 的?w=1300是它的 resize 参数,源站不认,要去掉 - 实测去掉查询串后字节数与 Photon 正常时完全一致,画质没变
- 源站零防盗链:裸 GET(无 Referer / Cookie / UA)直接 200
为什么不用 Photon(2026-10-11 实测教训)
Photon 是 WordPress 的免费图片 CDN。批量下载会打爆它的免费额度, 被封后返回:
HTTP 429
Access has been disabled due to excessive usage.
关键点:这跟请求头无关。实测对照同一张图、同一时刻:
| 请求方式 | 结果 |
|---|---|
| 我们的头(Chrome UA + Referer) | 429 |
| 完整浏览器头(+ Sec-Fetch-* / avif) | 429 |
| 裸请求(一个头都不发) | 429 |
源站直连(pic.4khd.com) |
200,128056 字节 |
被封后连裸请求都 429,说明是按 IP 的额度封禁,不是指纹识别。 而浏览器还能正常加载,只是因为它那点量远没到限额。
源站压测:20 并发全部成功,零限流;连续下 50 张也全部成功。
遇到限流怎么办
即便换了源站,如果将来源站也开始限流,引擎有全局冷却机制:
- 认出 429/503/403 后,所有 worker 一起停(而不是各自退避重试)
- 优先遵守服务器给的
Retry-After,没给就按 10s→20s→40s→80s 指数退避 - 冷却期满一起放行
为什么必须「一起停」:40 个 worker 各自独立退避会错峰重试, 结果是持续往已限流的服务器打请求,永远缓不过来。
每页必须去重:每张图在 HTML 里出现两次 —— 一次 <a href>、一次 <img src>,
内容完全相同。不去重的话 20 张会解析成 40 张(这个 bug 是被真实样本夹具测出来的)。
推荐位图片怎么认:正文图和推荐图走的是同一个 CDN,域名区分不了,只能靠特征:
4KHD-beautifulGirls.webp(推荐位统一封面)/w1090-h1500-p-k-no-rw/(推荐位尺寸段)?resize=(推荐位查询参数)
路径第 3 段的时间戳区分不了两者 —— 实测两边形式完全一样,都是 base36 时间戳。
分页:.page-link-box 里每个页码是独立的 <li class="numpages">,
取最大值即总页数。
不能追 rel=next:页面里的 rel="next" 和 canonical 都指向已 404 的
www.4khd.com。所以引擎只按总页数自己拼 URL,不跟这条链。
标题:优先 h3.wp-block-post-title。
⚠️ 别抓
<h1>—— 那是站点名 "4KHD",不是画廊标题。 h3 里是双空格(Myu_a(뮤아)␣␣SWEETBOX,分享链接里的%20%20证实双空格才是原文), 而<title>/og:title被 WordPress 规范化成了单空格。解析时会折叠空白。
项目结构
main.go Wails 入口
app.go 绑定层:把 Go 能力暴露给前端
wails.json Wails 配置
internal/hd4k/ ← 站点协议层(换站点只动这里)
site.go 站点配置、URL 解析、域名归一化
hd4k.go 抓取、图片 URL 提取、下载
limiter.go 限流器
testdata/ 真实页面样本(p1/p2/p3)
hd4k_test.go
manual_real_test.go 连真实站点的手动验证(-tags manual)
internal/core/ ← 与站点无关的通用引擎
store.go 任务表 + 设置(原子落盘)
engine.go 调度、并发、重试、断点续传
manifest.go .hd4k.json 清单读写
paths.go 目录模板与文件名清洗
bridge.go 127.0.0.1 本地桥口(给油猴用)
version.go 版本号、桥口 token
core_test.go
engine_test.go 端到端集成测试(本地假站点)
frontend/ 静态界面(无框架、无打包)
userscript/ 油猴脚本
测试
$env:PATH = "$env:USERPROFILE\.local\go\bin;$env:PATH"
go test ./internal/... -v
56 个顶层测试 + 3 个参数化子测试,全部离线跑(本地 httptest 假站点)。
关键覆盖:
- 真实页面夹具:p1/p2/p3 三页真实 HTML,断言 20/20/13 张、共 53 张
- 解析器边界:标题提取与实体解码、分页后缀剥离、别名域名归一
- 桥口:token 校验(无 token → 403)、CORS 只放行已知站点、只绑回环地址
- 引擎端到端:解析 → 下载 → 落盘 → 写清单 → 状态机整条链路
- 断点续传:清单完整时重启 0 网络请求;清单信息不全时老实回退重抓
- 改目录模板:复用老目录里已下好的文件,不重下
连真实站点验证(需网络,默认不跑)
$env:HD4K_URL = "https://migmg.ssuu.uk/content/09/djawa-photo-vol0411-ddiddi-yummy-delivery.html"
$env:HD4K_N = "6"
go test -tags manual -run TestRealSiteDownload -v ./internal/hd4k/
应用图标
用的是站点自己的图标(/wp-content/uploads/2023/02/4khd-1.png,200×200)。
注意它在页面上挂的是已 404 的 www.4khd.com,所以从 migmg.ssuu.uk 取。
环境里没有 ImageMagick / ffmpeg / Python,所以 ICO 是自己生成的
(tools/mkico,10 个尺寸,小尺寸 BMP + 大尺寸 PNG-in-ICO):
go run ./tools/mkico -in build/icon-source.png -out build/windows/icon.ico
go run ./tools/icocheck build/windows/icon.ico <导出目录> # 校验 + 导出各尺寸看效果
go run ./tools/alphaprobe <某张图> # 打印采样点的 RGBA 与 alpha 分布
缩放必须用 box filter + premultiplied alpha。少做 premultiplied 那一步的话, 缩放结果会反色(橙色变纯白)—— 这就是
alphaprobe存在的理由, 光看缩略图分不清是图错了还是预览器把透明当白底。
清理过小的图片
「清除已完成」按钮除了清任务列表,还会顺带删掉画廊目录里小于 10KB 的图片。
门槛 MinImageBytes = 10KB(internal/core/paths.go)取自实测:4KHD 正文图
最小也在 90KB 以上,而占位图 / 损坏图 / CDN 错误页伪装的图通常只有几 KB。
实测 50 张真实下载的图,最小 90.2 KB —— 10KB 门槛不会误伤正常图。
清扫规则:
- 只删图片扩展名的文件(
.webp .jpg .jpeg .png .gif .bmp .avif .jfif) - 清单(
.hd4k.json)不动 —— 要留着续传 .part未完成的下载不动 —— 它可能正好小于门槛但其实没下完- 非图片文件(
.txt等)不动 - 单个文件出错不中断整个清扫
没有已完成任务时也能用 —— 这时按钮只做「清理小图」这一步。
已知限制
- 解析器首选
i\d+.wp.com精确匹配;一张都匹配不到时会退到宽松匹配 (任何图片扩展名 + 推荐位特征过滤),以防站点换 CDN 就全线失败 - 单个画廊最多抓 2000 页,防止异常页码把机器跑满
- 桥口 token 是明文常量,仅靠「只监听 127.0.0.1」防外部访问 —— 别把它暴露到公网
声明
本工具仅供个人学习与备份自己有权保存的内容使用。 使用者需自行遵守目标站点的条款与所在地区的法律, 因使用本工具产生的一切后果由使用者自行承担。