简介:这是一款面向网站开发者、运维人员及建站爱好者的整站下载工具,用于将目标网站的全部资源完整抓取到本地,解决离线浏览、站点备份与迁移等实际需求。工具兼容ASP、JSP、PHP等多种服务端环境,适用范围较广,对需要批量保存网页素材或研究站点结构的用户较为实用。压缩包为rar格式,整体约540KB,体积轻巧便于携带与部署。目前已有2183人学习下载,说明其在同类工具中具备一定的实用口碑。借助该工具,读者可一次性获取整站页面、图片、脚本等资源,省去逐页手动保存的繁琐,同时便于在本地还原站点目录结构,为后续的备份、分析或二次开发提供便利。对于需要快速留存网站资料或搭建本地镜像的开发者而言,这是一份可直接上手使用的辅助工具。
1. 网站整站下载器:从“手动右键另存”到一条命令搬走整个站点
你有没有遇到过这种情况:客户丢来一个老官网,服务器下个月就到期,要求你把整站内容“原样搬走”;或者你在做竞品分析,需要把对方几百个产品页的图文结构完整扒下来离线研究。手动右键另存?一个页面几十个资源,几百个页面就是上万次点击,纯属折磨。这时候真正能救命的,是网站整站下载器——一个能把整个网站的所有资源(HTML、CSS、JS、图片、字体、甚至接口返回的 JSON 数据)按目录结构批量抓取到本地的工具。它解决的核心问题不是“下载一个页面”,而是“还原一个站点的资源依赖关系”,让离线目录打开后和线上几乎一样。适合做数据归档、迁移前备份、离线文档、前端逆向学习的从业者。这篇笔记我按自己实际跑过的路径,把选型、命令、参数、坑一次讲透。
2. 整站下载器的三种技术路线:为什么我最后选了 wget + 后处理
2.1 递归抓取、无头浏览器、专用爬虫框架的取舍
整站下载这件事,底层其实就三条路,选错了后面全是返工。
第一条是递归 HTTP 抓取,代表工具是 wget、httrack。原理很朴素:拿到一个 URL,解析 HTML 里的href、src、url(),把同域链接塞进队列继续抓,同时把资源按相对路径落盘。优点是快、轻、不依赖浏览器内核,一个静态站几分钟就能拉完。缺点是遇到 JS 动态渲染的页面就抓瞎——HTML 里只有<div id="app"></div>,真正的资源全靠接口异步加载。
第二条是无头浏览器驱动,代表是 Playwright、Puppeteer 配合爬取逻辑。它真的开一个 Chromium,等页面渲染完再抓 DOM 和网络请求。动态站、SPA、懒加载图片都能拿到。代价是慢,一个页面等 2~5 秒,几百页就是半小时起步,而且内存吃紧,容器资源隔离没做好的话很容易被 OOM 杀掉。
第三条是专用爬虫框架,Scrapy 这类。灵活度最高,但要自己写 pipeline、去重、限速,属于“我要长期维护一个爬虫项目”的量级,不适合“我今晚就想把站搬下来”。
我的选择是:静态站和半动态站用 wget 打底,动态接口部分用 Playwright 补抓。原因是 wget 的--mirror和--convert-links组合能直接把站点的目录结构和链接关系还原,这是专用框架要写不少代码才能达到的效果。下面重点讲这条路线。
2.2 wget 镜像模式的最小可用命令
先给一条我压箱底的命令,抓一个静态站:
wget \ --mirror \ --page-requisites \ --convert-links \ --adjust-extension \ --no-parent \ --wait=1 \ --random-wait \ --limit-rate=500k \ --user-agent="Mozilla/5.0 (compatible; SiteArchiver/1.0)" \ --reject-regex '(\?|&)(utm_|fbclid|gclid)=' \ -e robots=off \ -P ./mirror_output \ https://example.com/逐段说清楚每个参数在干什么,因为整站下载翻车十有八九是参数没配对:
--mirror等价于-r -N -l inf --no-remove-listing,即递归、时间戳比对、无限层级。这是整站下载的总开关。--page-requisites是关键,它让 wget 把页面渲染必需的 CSS、JS、图片一起抓,否则你只拿到一堆光秃秃的 HTML。--convert-links把下载后的 HTML 里的绝对链接改写成相对链接,这样你本地双击index.html也能正常跳转,离线可用。--adjust-extension给动态 URL 补上.html后缀,避免本地文件系统认不出类型。--no-parent防止爬到上级目录,比如你抓/blog/,它不会跑去抓整个域名根。--wait=1 --random-wait每次请求间隔 1 秒并随机浮动,降低被封 IP 的概率,这是血泪经验。--limit-rate=500k限速,别把人家小站带宽打满,也算基本礼貌。--reject-regex过滤掉带追踪参数的 URL,否则同一个页面会因为?utm_source=xxx被抓几十遍,目录瞬间膨胀。-e robots=off忽略 robots.txt。注意:这条只在你有明确授权、抓自己或客户站点时用,公网抓取请遵守目标站规则。-P指定输出根目录。
跑完之后,./mirror_output里就是一棵完整的站点目录树。这一步能解决 70% 的静态站需求。
2.3 动态资源补抓:Playwright 拦截网络请求
wget 抓不到的,是那些由 JS 在运行时请求的接口数据、懒加载图片、字体。这时候用 Playwright 开一个真实浏览器,监听response事件,把响应体落盘:
from playwright.sync_api import sync_playwright import os, hashlib, mimetypes OUT = "./dynamic_assets" os.makedirs(OUT, exist_ok=True) def save_response(resp): try: url = resp.url ctype = resp.headers.get("content-type", "") # 只留我们关心的资源类型,避免把埋点、广告也存下来 if not any(k in ctype for k in ["image", "font", "javascript", "css", "json"]): return body = resp.body() # 用 URL 的哈希做文件名,避免路径冲突和非法字符 name = hashlib.md5(url.encode()).hexdigest()[:16] ext = mimetypes.guess_extension(ctype.split(";")[0]) or ".bin" with open(os.path.join(OUT, name + ext), "wb") as f: f.write(body) # 记录 URL 到本地文件的映射,方便后续替换 with open(os.path.join(OUT, "map.tsv"), "a", encoding="utf-8") as m: m.write(f"{url}\t{name + ext}\n") except Exception as e: print("skip", resp.url, e) with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.on("response", save_response) page.goto("https://example.com/", wait_until="networkidle") # 滚动触发懒加载 for _ in range(10): page.mouse.wheel(0, 2000) page.wait_for_timeout(800) browser.close()逻辑说明:page.on("response", ...)是核心,浏览器每收到一个响应就回调一次,我们按content-type过滤,只存图片、字体、JS、CSS、JSON 这几类。wait_until="networkidle"等网络空闲再继续,保证首屏资源都加载完。滚动那几行是为了触发懒加载图片——很多站的图片要滚到可视区才请求,不滚就漏。
参数上,headless=True适合服务器跑;如果你要调试,改成False能看到浏览器实际行为。map.tsv这个映射文件别小看,后面把动态资源回填进 wget 抓下来的 HTML 时,全靠它做 URL 到本地路径的对应。
3. 资源落盘后的目录还原与链接重写
3.1 为什么抓下来的站打开是白屏
很多人第一次跑完 wget,兴冲冲双击index.html,结果白屏或者样式全丢。原因通常有三个:一是--convert-links没加,HTML 里还是https://example.com/style.css这种绝对路径,离线环境请求不到;二是 CSS 里的url()引用没被改写,背景图、字体全 404;三是动态接口数据没回填,页面 JS 拿不到数据自然渲染不出来。
解决思路是分两层:wget 负责静态资源的链接改写,我们自己写一个后处理脚本,把动态资源按map.tsv回填,并统一修正 CSS 内的相对路径。
3.2 用 Python 批量重写 CSS 与 HTML 内的资源引用
import os, re, pathlib ROOT = pathlib.Path("./mirror_output") MAP = {} with open("./dynamic_assets/map.tsv", encoding="utf-8") as f: for line in f: url, local = line.strip().split("\t") MAP[url] = f"../dynamic_assets/{local}" # 匹配 url(...) 和 src/href 里的绝对地址 URL_RE = re.compile(r'url\((["\']?)(https?://[^)"\']+)\1\)') ATTR_RE = re.compile(r'(src|href)=(["\'])(https?://[^"\']+)\2') def rewrite(text): def repl_url(m): u = m.group(2) return f'url("{MAP.get(u, u)}")' def repl_attr(m): attr, q, u = m.group(1), m.group(2), m.group(3) return f'{attr}={q}{MAP.get(u, u)}{q}' return ATTR_RE.sub(repl_attr, URL_RE.sub(repl_url, text)) for p in ROOT.rglob("*"): if p.suffix.lower() in (".css", ".html", ".htm", ".js"): try: txt = p.read_text(encoding="utf-8", errors="ignore") new = rewrite(txt) if new != txt: p.write_text(new, encoding="utf-8") print("rewritten", p) except Exception as e: print("fail", p, e)逻辑说明:先读map.tsv建一个“线上 URL → 本地相对路径”的字典。URL_RE处理 CSS 里的url(...),ATTR_RE处理 HTML 里的src/href。两个正则都只匹配http(s)://开头的绝对地址,相对地址不动,避免误伤。替换时如果字典里没有对应项,就保留原 URL,方便你事后排查哪些资源没抓到。
参数上,errors="ignore"是必须的,有些老站的 HTML 编码混乱,不加会直接抛异常中断。rglob("*")递归遍历所有文件,.js也一起处理是因为有些前端会把资源路径硬编码在 JS 里。
3.3 用本地 HTTP 服务验证,而不是双击文件
改完之后别急着双击index.html,用file://协议打开会有跨域限制,JS 模块、fetch 请求全废。正确做法是起一个本地静态服务:
python -m http.server 8080 --directory ./mirror_output然后浏览器访问http://localhost:8080。这样fetch、ES module、字体加载的行为和线上一致,能真实暴露哪些资源还缺。我一般会开 DevTools 的 Network 面板,看有没有 404,有就回到map.tsv里查是不是漏抓了。
4. 整站下载避坑:五条我踩过的血泪记录
4.1 现象:抓了一半 IP 被封,后续全是 403
原因:默认 wget 请求间隔是 0,几百个请求瞬间打过去,目标站的 WAF 或 CDN 直接判定为攻击。解决:加--wait=1 --random-wait,并发数控制在 1(wget 默认就是单线程,别去开--max-connections之类)。如果站特别敏感,把--wait提到 3~5 秒,配合--limit-rate限速。真被封了就换出口 IP 或等几小时,别硬刚。
4.2 现象:目录里出现index.html?utm_source=xxx这种带参数的重复文件
原因:URL 里的追踪参数让 wget 认为是不同页面,同一个内容抓了 N 遍。解决:用--reject-regex过滤utm_、fbclid、gclid、ref=这些常见追踪参数。更彻底的做法是抓之前先用--spider探一遍,把带参数的 URL 列出来,再决定过滤规则。
4.3 现象:CSS 背景图和字体全部 404
原因:wget 的--convert-links只改写 HTML 里的链接,CSS 文件内部的url()它不管。解决:就是第 3 章那个后处理脚本,专门重写 CSS 内的url(...)。另外注意字体文件常带?v=1.2.3版本参数,--reject-regex别把这类参数也过滤了,否则字体抓不到。
4.4 现象:SPA 站点抓下来只有一个空壳 HTML
原因:内容全靠 JS 渲染,wget 拿到的是初始 HTML,里面没有实际数据。解决:这类站必须上 Playwright 路线,而且不能只抓首屏,要模拟点击、滚动、翻页。我的做法是先用 Playwright 把关键页面的networkidle后的完整 DOMpage.content()存下来,再对存下来的 HTML 做资源补抓。注意page.content()拿到的是渲染后的 DOM,比原始 HTML 完整得多。
4.5 现象:抓完发现磁盘爆了,几百 GB
原因:视频、大图、重复资源没做过滤,或者递归爬到了外域。解决:加--no-parent限制层级,用--reject排除.mp4、.zip、.iso这类大文件,用--quota设总量上限(比如--quota=2g)。另外定期du -sh看目录大小,别等磁盘满了才发现。容器里跑的话,提前做好资源隔离和磁盘配额,不然一个爬虫能把整台机器拖垮。
5. 进阶:把整站下载做成可复用的归档流水线
单次抓取只是开始,真正省事的是把它做成一条流水线:输入域名,输出一个可离线打开、带校验报告的归档包。我现在的做法是写一个archive.sh,串起四步——wget 镜像、Playwright 补抓、Python 重写、本地服务自检。
#!/usr/bin/env bash set -e DOMAIN="$1" OUT="./archive_${DOMAIN//\//_}" mkdir -p "$OUT" # 1. 静态镜像 wget --mirror --page-requisites --convert-links --adjust-extension \ --no-parent --wait=1 --random-wait --limit-rate=500k \ --reject-regex '(\?|&)(utm_|fbclid|gclid)=' \ -e robots=off -P "$OUT/static" "https://$DOMAIN/" # 2. 动态补抓(调用前面的 playwright 脚本) python fetch_dynamic.py "https://$DOMAIN/" "$OUT/dynamic" # 3. 链接重写 python rewrite_links.py "$OUT/static" "$OUT/dynamic/map.tsv" # 4. 自检:起服务并统计 404 python -m http.server 8090 --directory "$OUT/static" & SERVER_PID=$! sleep 2 # 用 curl 遍历本地页面,统计非 200 响应 grep -rl 'src=' "$OUT/static" --include='*.html' | while read -r f; do curl -s -o /dev/null -w "%{http_code} %{url_effective}\n" \ "http://localhost:8090/$(realpath --relative-to="$OUT/static" "$f")" done | grep -v '^200' > "$OUT/broken.txt" || true kill $SERVER_PID echo "归档完成,缺失资源见 $OUT/broken.txt"这条流水线的价值在于可复现。下次换个域名,bash archive.sh example.org一条命令跑完,broken.txt直接告诉你哪些资源没抓到,不用人工一个个点。参数上,set -e保证任何一步失败就停,避免半成品;realpath --relative-to把本地文件路径转成 URL 路径,方便 curl 自检。
一个具体技巧:broken.txt里如果大量出现同一类资源(比如全是.woff2字体),说明是--reject-regex误伤了带版本参数的字体 URL,回去改规则重跑即可。如果只是零星几个 404,多半是第三方 CDN 的资源,可以考虑用--span-hosts把 CDN 域也纳入抓取范围,但要小心别爬飞。
我自己现在的习惯是:任何整站归档任务,先跑一遍--spider探路,看清楚站点规模和资源类型,再决定参数。别一上来就--mirror全量开抓,翻车了重来成本很高。这套流程帮我搬过十几个老站,最慢的一次也就两小时,比手动靠谱太多。希望帮到你。
本文还有配套的精品资源,点击获取