C# + WPF 编写的网页克隆小工具:输入网址,把页面的 HTML 源码及引用的 CSS、JS、 图片、字体等资源全部下载到本地,自动处理文件名合法性并把绝对路径改写为相对路径, 让克隆结果可以离线打开、不跳外站。
对应 wget 的两种用法:
| 模式 | 等价 wget 命令 | 说明 |
|---|---|---|
| 单页模板 | wget -r -E -np -c -k --ignore-tags=a <url> |
只抓当前页与它引用的资源,不递归 a 链接 |
| 整站克隆 | wget -r -np -c -k <url> |
递归追踪站内链接(不向上级目录),抓全站 |
WebClone/
├── src/
│ ├── WebClone.Core/ 核心引擎(net10.0,零反射,可被 NativeAOT 完整编译)
│ ├── WebClone.Cli/ 命令行版(PublishAot,产出原生 exe,是 AOT 兼容性的验证载体)
│ └── WebClone.Wpf/ 图形界面(net10.0-windows,单文件自包含发布)
└── tests/
├── make_test_site.py 生成功能验证站点(含 GBK 页面、查询参数资源、CSS 嵌套引用等)
├── test_server.py 支持 ETag / 304 / Range 续传的本地测试服务器
└── test_proxy.py 极简 HTTP / SOCKS5 双模代理,用于验证代理功能
GUI 运行实况 —— 单页克隆 go.dev,68 项资源全部完成,无失败:
克隆产物 —— 按 URL 目录结构归档,引用已改写为相对路径:
离线渲染效果 —— 本地静态服务打开克隆结果,样式、图片、字体完整加载:
- 两种模式:单页模板 / 整站递归,整站模式遵循
-np语义(不下载上级目录)。 - 文件名处理:URL 带查询参数时生成
原名_8位哈希.ext(如style_78fe4a82.css), 合法且可读;同时处理 Windows 保留名、非法字符、路径穿越。 - 路径改写:所有资源引用改写为相对路径;
<base href>重定向为.; 跨域 CDN 资源按各自域名归档后同样以相对路径引用。 - 重定向归一:下载时跟随服务端重定向,最终地址(如
/doc/重定向自/doc) 与请求地址共享同一本地文件,整站克隆不会因链接写法差异存两份。 - 类型修正:下载完成后按响应的真实 Content-Type 修正落盘扩展名 ——
像 Google Fonts 的
/css?family=...这类无扩展名动态样式表会被正确存为.css, 避免浏览器因 MIME 类型不符拒绝应用样式(Material Icons 图标失效的根因)。 - 单页模式防跳转:指向未下载页面的 a 链接统一替换为
#;整站模式保留外站原链。 - 代理:HTTP(S) 代理与 SOCKS5 代理。SOCKS5 为手写实现(RFC 1928/1929),
通过
SocketsHttpHandler.ConnectCallback只接管 TCP 建连,TLS 与 HTTP 仍由框架处理, HTTPS 天然走 CONNECT 隧道。支持用户名密码认证。 - 增量与断点续传:
- 图片、字体等二进制资源用
If-None-Match/If-Modified-Since条件请求,304 直接跳过; - 中断的下载用
Range+If-Range续传(资源变化时服务端自动降级为 200 全量); - 一律先写
.part再原子改名,不留残缺成品; - HTML / CSS 因为落盘前会被改写,始终取原始内容,避免把改写后的文件误当服务器响应。
- 图片、字体等二进制资源用
- 编码:按 BOM → HTTP charset → meta charset / @charset → 严格 UTF-8 嗅探 → GB18030 兜底的顺序识别,统一转 UTF-8 保存并改写编码声明,GBK 页面不乱码。
- 解析器自研:HTML / CSS 扫描器为零依赖状态机(约 700 行),记录原文下标做原地替换,
不引入 HtmlAgilityPack / AngleSharp 等反射大户,这是能过 NativeAOT 的关键。
覆盖
<link>、<script>、<img>、srcset、<video>/<audio>/<source>、<object>/<embed>、<iframe>、内联 style、<style>正文、CSSurl()/@import/@font-face,以及data-src等懒加载属性。
WPF 与 NativeAOT 不兼容(SDK 硬性报 NETSDK1168:启用剪裁时不支持 WPF)。 因此采用分层方案:
WebClone.Core+WebClone.Cli:PublishAot=true,产出免安装、秒启动的原生 exe, 全程零 IL2xxx / IL3xxx 裁剪警告;WebClone.Wpf:单文件自包含发布(ReadyToRun),业务逻辑全部在 Core 里, Core 的 AOT 干净度由 CLI 项目实际编译验证。
一键构建脚本(Windows PowerShell):
.\build.ps1 # CLI + GUI 全部构建并打包到 dist\
.\build.ps1 -SkipCli # 只构建 GUI
.\build.ps1 -SkipGui # 只构建 CLI(AOT)
.\build.ps1 -Clean # 先清理 dist\ 与 obj\bin 再构建
.\build.ps1 -Configuration Debug产物:
| 文件 | 说明 |
|---|---|
dist\webclone-cli.exe |
命令行版,NativeAOT 原生可执行(约 8 MB,免安装) |
dist\WebClone-GUI.exe |
图形界面版,单文件自包含(约 150 MB,首次启动自解压稍慢) |
手动构建(等效于脚本内部行为):
# 需要 .NET 10 SDK + VS C++ 工具链(NativeAOT 依赖)
dotnet publish src/WebClone.Cli -c Release -r win-x64
dotnet publish src/WebClone.Wpf -c Release在 Git Bash / Cygwin 会话里构建,若报
Value cannot be null. (Parameter 'path1')或Cross-OS native compilation is not supported,是会话缺少ProgramFiles/OS等环境变量所致,source ~/.workbuddy/bin/env-fix.sh后用runenv dotnet ...即可。
应用图标位于 assets/app.ico(自绘,无版权问题),由 tests/make_icon.py 生成,
CLI 与 GUI 共用。
webclone <网址> [选项]
# 单页模板,输出到指定目录
webclone https://example.com -o D:\site
# 整站递归,深度 3,并发 16
webclone https://example.com -m full -d 3 -c 16 -o D:\site
# 走 SOCKS5 代理(支持 user:pass 认证)
webclone https://example.com --proxy socks5://user:pass@127.0.0.1:1080
# 走 HTTP 代理
webclone https://example.com --proxy http://127.0.0.1:8080常用选项:-m single|full、-o 目录、-d 深度、-c 并发、--max-pages、
--no-cross-origin(不抓 CDN)、--no-incremental、--keep-absolute、
--keep-external-links、--ignore-cert-errors、-v 详细日志。
一键回归(自动起服务器、跑五组场景断言、汇总 PASS/FAIL):
.\tests\regression.ps1 # 默认用 dist\webclone-cli.exe
.\tests\regression.ps1 -PythonPath py # 指定 python 命令覆盖场景:单页克隆(含哈希文件名与资源落盘)、增量二跑、整站三页递归、
-np 不越界、GBK 转码。引擎改动后跑一遍即可确认无回归。
手动验证步骤:
# 1. 生成测试站点并启动本地服务器(支持 304 / Range)
python tests/make_test_site.py tests/site
python tests/test_server.py 8765 tests/site
# 2. 启动双模代理(可选,验证代理功能)
python tests/test_proxy.py http 8899
python tests/test_proxy.py socks5 8898
# 3. 克隆并对比
webclone http://127.0.0.1:8765/ -m full -o out已验证项:单页/整站递归、CSS 内 url()/@import/@font-face、内联 style、srcset、 带查询参数的文件名重命名、GBK 编码转码、增量 304 跳过、断点续传(206 拼接后 与源文件逐字节一致)、HTTP/SOCKS5 代理克隆、AOT 原生发布、重定向归一、 按 Content-Type 修正扩展名。
GUI 每次启动自动回显上次的选项,配置存放在 exe 同目录的 webclone-gui.json:
- 首次运行自动生成默认配置;点"开始克隆"或关闭窗口时自动保存当前选项
- 单个字段损坏只跳过该字段用默认值;整个文件损坏自动重建
- 配置写失败(如放在无写权限目录)静默跳过,不影响克隆


