简介面向需要离线备份、迁移或研究站点结构的开发者与运维人员这套网站整站下载器用于抓取并保存整站资源适合对ASP、JSP、PHP等常见站点做批量页面与素材归档减少逐页手工保存的成本。压缩包体积约540KB轻量易传输上游未提供文件总数及文件类型明细具体构成以实际包内为准下载后可先核对目录与说明再使用。资源页显示已有2187人学习或下载具备一定参考热度。其价值在于提供整站抓取思路与现成工具入口便于在研究页面层级、备份静态素材或搭建离线镜像时快速入手也可结合附加数据码与评论反馈判断适用性对需要批量收集页面、图片等资源的场景较友好初学者亦可借此了解站点资源组织方式但不建议用于未授权站点。1. 整站下载器搬走的是三样东西文档、资源、引用关系搜「网站整站下载器」的人脑子里大多有一个具体画面地址粘进去进度条跑完本地双击 index.html 就能点着看。真跑一遍往往不是这样——样式没了图片裂一半点第二层链接又跳回线上。问题不在工具而在于整站下载要同时完成三件事把 HTML 文档拉下来、把文档引用的静态资源补齐、把资源之间的引用关系改成本地能解析的路径。前两件决定存不存得下第三件决定跑不跑得起来而第三件恰恰是一键式工具最容易糊弄过去的地方。这份资源适合改版前要给老站点留可浏览快照的前端、做内容盘点和迁移的运维、需要离线查看站点结构的从业者。前提只有一个自有站点或已拿到明确授权的站点。2. 抓取边界怎么划入口 URL、深度、域名白名单递归抓取的本质是「从入口出发沿着 href 和 src 一直走」。走多远、走不走外链、哪些目录不进去全在参数里。边界没划清轻则抓回一堆重复页面把磁盘吃满重则把整个站点的日历页、搜索结果页、打印页全部展开URL 数量指数级膨胀。这一章把边界拆成三件事站点结构判断、命令行参数、URL 池构建。2.1 先把站点分成三种结构再决定抓多深同样是「整站」结构差别极大抓法完全不同。目录型站点最友好URL 形如/docs/guide/install.html层级就是物理目录-l 3基本能覆盖主干。参数型站点最麻烦URL 形如/list.php?cat3page2递归会顺着分页和筛选条件无限展开-l设多少都不够只能靠 URL 池精确列举。单页应用型的递归深度参数几乎失效因为路由在#后面服务端永远只返回同一个index.html抓一千次也是一份。结构类型识别特征建议深度常见翻车点目录型路径分层、无?参数-l 4到-l 6附件目录被一起拖走参数型大量?id、page不用递归改 URL 池分页组合爆炸单页应用路由带#、首屏 HTML 几乎为空递归无效需预渲染抓回来全是空壳混合型主站静态、列表页走接口静态部分递归 接口直取列表内容整块丢失判断方法很土但有效随便点开三层页面看地址栏有没有?再右键查看源码搜索页面上肉眼可见的一段文字搜得到就是服务端渲染搜不到就是客户端渲染。这一步花两分钟能省掉后面两小时的重抓。另一个常被忽略的边界是文件类型。整站快照要的是「能看」不是「能跑业务」后台接口、上传目录、日志、备份包都不该进来。常见做法是在--reject里按扩展名挡掉zip|rar|sql|log|bak|exe同时用--exclude-directories挡掉/admin、/api、/search、/tmp这类路径。2.2 一组可抄的 wget 参数与逐项解释命令行工具里wget的镜像能力最完整参数组合也比较透明。下面这组是我反复用下来比较平衡的一份改三处就能直接跑域名、排除目录、输出目录。# 整站镜像递归 页面必需资源 链接本地化 wget \ --mirror \ --page-requisites \ --convert-links \ --adjust-extension \ --no-parent \ --no-host-directories \ --restrict-file-nameswindows \ --domainsexample.com \ --exclude-directories/admin,/api,/search \ --reject zip,rar,sql,log,bak,exe,iso \ --wait1 --random-wait --limit-rate800k \ --tries3 --timeout20 \ --user-agentMozilla/5.0 (compatible; SiteArchiver/1.0) \ --directory-prefix./mirror \ https://example.com/参数说明重点是后四个--mirror是组合开关等价于-r -N -l inf --no-remove-listing即无限深度递归加时间戳比对。-l inf意味着深度不受限所以必须靠--exclude-directories和--reject兜底否则参数型站点会一直爬下去。--page-requisites负责把 HTML 引用的 CSS、JS、图片一起拉下来。它只认 HTML 里写死的引用CSS 里import再嵌套的资源可能漏后面 3.3 的脚本会补一次扫描。--convert-links做链接本地化把https://example.com/a.html改成a.html。注意它在抓取全部结束后统一处理中途中断不会改写所以别指望抓一半就能打开看。--adjust-extension给没有扩展名的 URL 补.html。少了这个参数/about/这种地址存成无后缀文件双击会被系统当成未知格式而不是用浏览器打开。--no-parent限制不往上级目录跑--no-host-directories去掉最外层的example.com/目录壳本地打开路径更短。--restrict-file-nameswindows把: * ? |这类在 Windows 上非法的字符替换掉。跨平台交付快照时这个参数不加解压那一刻就一堆报错。--wait1 --random-wait让每次请求间隔 0.5 到 1.5 秒随机--limit-rate800k限制总带宽。这两个参数决定了对面站点会不会把你当成压力源。注意不要用-e robotsoff去绕过站点在 robots.txt 里声明的目录。那些目录本来就不该进快照绕过只是把风险转移给自己。抓取前先做一次干跑确认边界对不对# 只打印不下载用来核对 URL 池规模 wget --spider --force-html -i urls.txt -r -l 1 --domainsexample.com 21 | \ grep -E ^-- | wc -l--spider只发 HEAD 请求不发正文-i urls.txt从文件读取入口输出行数大致等于首层可发现的 URL 数量。上千条就说明边界太松得回去收紧--exclude-directories。2.3 用 sitemap 和列表页反推 URL 池参数型站点和内容站靠递归不如靠清单。站点地图通常放在/sitemap.xml有的是索引文件套一层子地图写法不一样但结构固定。import re import urllib.request SEED https://example.com/sitemap.xml UA {User-Agent: Mozilla/5.0 (compatible; SiteArchiver/1.0)} def fetch(url): req urllib.request.Request(url, headersUA) with urllib.request.urlopen(req, timeout20) as r: return r.read().decode(utf-8, ignore) def parse(xml): # 索引型 sitemap 返回子地图地址 maps re.findall(rsitemap.*?loc(.*?)/loc, xml, re.S) if maps: return map, [m.strip() for m in maps] urls re.findall(rurl.*?loc(.*?)/loc, xml, re.S) return url, [u.strip() for u in urls] kind, items parse(fetch(SEED)) if kind map: pool [] for m in items: pool parse(fetch(m))[1] else: pool items # 只保留同域、去重、去掉明显的附件 pool [u for u in pool if u.startswith(https://example.com/)] pool [u for u in dict.fromkeys(pool) if not re.search(r\.(zip|pdf|rar)$, u)] open(urls.txt, w).write(\n.join(pool)) print(len(pool), urls written)逻辑说明先读入口 XML判断是索引型还是直列型索引型就再往下钻一层然后按域名前缀过滤用dict.fromkeys保序去重最后落成一行一个的urls.txt。参数调整点有三个SEED换成目标站点的地图地址附件过滤正则按站点实际情况增删如果站点的 PDF 是核心内容就别挡超时timeout20在网络差的环境调到 40 更合适。拿到清单之后用wget -i批量取配合-p补资源wget -i urls.txt \ --page-requisites --convert-links --adjust-extension \ --no-host-directories --restrict-file-nameswindows \ --wait1 --random-wait --limit-rate800k \ --directory-prefix./mirror地图的局限要说清楚它只列规范 URL不含分页、标签页、筛选结果。列表页的分页规则得单独用正则补齐比如page\d抓前两页看规律再写成循环生成别硬递归。2.4 限速、重试与请求头抓取规模一旦上百页对面站点是否配合、连接是否稳定就直接决定成败。三个参数值得单独调--wait和--random-wait控制请求间隔。固定间隔容易形成规律特征随机化后更像人类访问。--limit-rate限制总带宽多人共用一个出口时尤其要加不然你自己的其他工作也会被拖慢。--tries3配合--waitretry5处理偶发超时超过三次就跳过并在日志里留痕。别设太高重试堆积比失败更麻烦。--user-agent建议带一个能识别用途的标识比伪装成浏览器更体面也方便站点管理员在日志里认出你、放行你。真遇到 403先去看是不是 CDN 按 UA 拦了别急着换 UA 硬刷。3. 落地成本地站点链接改写、路径映射与编码抓回来一堆文件和「双击能看」之间还差一步让每个引用都能在本地文件系统里找到对应文件。这一步最容易出错因为它涉及四类链接形态、两类编码问题和一个文件名长度上限。3.1 四种链接形态与各自的改写规则链接形态例子本地化目标注意绝对 URLhttps://example.com/a/b.htmla/b.html域名带端口时容易漏判根相对/static/x.css../static/x.css相对深度取决于当前文件层级文档相对./img/a.png保持不动目录结构没乱就不用改协议相对//cdn.example.com/x.js需单独抓取不加域名判断会被跳过带查询串/list.php?id3list.phpid3.html直接存会互相覆盖哈希路由/#/about无法静态化必须预渲染见第 4 章--convert-links能处理前三类第四类和第五类是漏网常客。CDN 上的 JS、字体、图标因为域名不同被--domains挡在外面页面结构在、样式全丢。补救办法是把 CDN 域名加进白名单例如--domainsexample.com,cdn.example.com再跑一轮只抓资源的增量任务。带查询串的 URL 是覆盖重灾区。/list.php?id3和id4在--restrict-file-names处理后可能落到同一个文件名上后抓的把先抓的盖掉最后只剩一页。稳妥做法是让工具保留查询串或者干脆自己写映射表把每个 URL 算一个短哈希存成资源目录/哈希前两位/哈希.html同时生成manifest.csv记录原始 URL 和本地路径的对应关系。多花十分钟排查时能救命。3.2 文件名冲突、超长名与 URL 编码三个隐蔽问题出了都很难一眼定位。第一是大小写。Linux 上Logo.PNG和logo.png是两个文件Windows 和 macOS 默认是同一个。抓取机是 Linux、查看机是 Windows 时解压会提示「是否覆盖」覆盖完就有一半图片引用失效。解决方法是在抓取阶段统一转小写或者干脆在 Linux 上用本地静态服务器查看别解压到 Windows。第二是 URL 编码。中文路径在 URL 里是百分号编码--restrict-file-names的不同档位会解码成不同结果。用windows档能保证跨平台但中文会变成%E4%B8%AD这种形式可读性差用nocontrol档保留原始编码可读性好但换平台可能出问题。我一般选windows因为交付对象往往不是本机。第三是文件名长度。部分站点把标题直接拼进 URL中文标题转码后一个字符变九个字节很容易突破 255 字节上限工具会报File name too long然后跳过。这种情况只能靠后处理把超长名截断并加哈希后缀同时在 manifest 里回填映射。提示抓取前先在目标目录跑一次df -h整站快照的体积往往是最初估计的三到五倍图片站和视频站尤其夸张。3.3 后处理脚本扫缺失引用、出报告工具跑完不等于结束。写个脚本把 HTML 里的引用全部抽出来逐个检查本地文件是否存在缺失的记下来这份报告就是后面补抓的工单。import os import re import csv from urllib.parse import urlparse, unquote ROOT ./mirror REF re.compile(r(?:src|href)\s*\s*[\]([^\#])[\], re.I) missing, checked [], 0 for dirpath, _, files in os.walk(ROOT): for f in files: if not f.endswith((.html, .htm)): continue page os.path.join(dirpath, f) try: html open(page, encodingutf-8, errorsignore).read() except OSError: continue for ref in REF.findall(html): # 跳过外链、锚点、data URI if ref.startswith((http://, https://, //, data:, mailto:)): continue target unquote(urlparse(ref).path).lstrip(/) if not target: continue checked 1 # 相对引用按当前页面目录解析 p os.path.normpath(os.path.join(dirpath, target)) if not os.path.exists(p): missing.append([page, ref, p]) with open(missing.csv, w, newline, encodingutf-8-sig) as fp: w csv.writer(fp) w.writerow([page, ref, resolved]) w.writerows(missing) print(fchecked{checked} missing{len(missing)})逻辑说明遍历所有 HTML用正则捞出src和href过滤掉外链、锚点和 data URI剩下的按「当前页面所在目录 相对路径」解析成绝对本地路径os.path.exists判存在与否。urlparse去掉查询串unquote还原中文两步少一个都会产生大量假缺失。参数调整ROOT指向镜像目录正则只抓src和hrefsrcset、poster、内联style里的url()要另加规则直接扩成(?:src|href|poster|srcset)\s*再写个小函数拆srcset的逗号列表。输出用utf-8-sig是为了 Excel 直接打开不乱码。跑完看两个数checked太少说明 HTML 文件没抓到或编码识别失败missing里如果全是同类资源比如整目录的字体文件基本就是 CDN 域名被挡了回 3.1 加白名单重抓一轮。4. 抓不到的那部分渲染型页面与接口数据的补抓用wget抓单页应用结果通常是几十个字节数相近的 HTML打开一片空白。这不是配置错了是内容根本不在服务端返回的 HTML 里。这一章解决「抓回来是空壳」的问题思路是先判断、再补抓、最后合并。4.1 三步判断页面是不是客户端渲染不用装任何工具三条命令就能定性# 1. 看原始 HTML 体积 curl -sL https://example.com/list | wc -c # 2. 找页面上肉眼可见的一段文字 curl -sL https://example.com/list | grep -c 某条真实标题 # 3. 找框架痕迹 curl -sL https://example.com/list | grep -oE (idapp|idroot|__NUXT__|__NEXT_DATA__) | head体积小于 5KB、关键词计数为零、命中idapp或idroot三条里中两条基本可以断定是客户端渲染。反过来如果第 2 步命中说明服务端已经吐出内容wget那一套就够用不必上无头浏览器——渲染一次的开销是纯 HTTP 请求的几十倍能省则省。还有一类「半渲染」站点首屏 HTML 里有内容滚动加载的部分没有。判断方法是把页面滚到底看 DOM 里节点数是否显著增加。这类站点用递归抓只能拿到首屏后面的得靠接口。4.2 无头浏览器补抓的最小实现确认要渲染之后用无头浏览器逐个访问 URL 池等网络空闲再取 DOM。下面这段是能直接跑的最小版本from playwright.sync_api import sync_playwright import pathlib, time URLS [u.strip() for u in open(urls.txt) if u.strip()] OUT pathlib.Path(./mirror) with sync_playwright() as p: browser p.chromium.launch(headlessTrue) ctx browser.new_context( user_agentMozilla/5.0 (compatible; SiteArchiver/1.0), viewport{width: 1440, height: 900}, ) page ctx.new_page() for url in URLS: try: page.goto(url, wait_untilnetworkidle, timeout30000) # 触发懒加载滚到底再回顶 page.evaluate(window.scrollTo(0, document.body.scrollHeight)) time.sleep(1.5) page.evaluate(window.scrollTo(0, 0)) name url.rstrip(/).split(/)[-1] or index (OUT / f{name}.html).write_text(page.content(), encodingutf-8) except Exception as e: print(fail, url, e) browser.close()逻辑说明wait_untilnetworkidle表示等到网络请求静默 500ms 再取内容比load更可靠滚动到底是为了触发IntersectionObserver类的懒加载page.content()拿到的是渲染后的完整 DOM不是原始响应。参数调整timeout在弱网下调到 60000headlessTrue改成False可以肉眼观察抓取过程调试阶段很有用如果需要登录态用ctx.add_cookies()注入别把账号密码写进脚本。这套跑法的代价要提前算清单页平均 3 到 5 秒五百页就是半小时到四十分钟而且吃内存。批量跑时把浏览器实例复用别一页开一个。另外渲染出来的 HTML 里样式表还是线上绝对地址想离线看得再用第 3 章的脚本做一次本地化。4.3 接口直取比渲染更省的一条路渲染是最后手段。多数「半渲染」站点的数据来自几个固定接口直接请求 JSON 比开浏览器快一个数量级也更省资源。找接口的方法打开开发者工具的 Network 面板筛 XHR/Fetch刷新列表页找返回结构化数据的那几个请求。看请求 URL 的规律通常带page、size、category这类参数改参数就能翻页。# 循环拉取列表接口落成编号 JSON for i in $(seq 1 20); do curl -s https://example.com/api/list?page${i}size20 \ -H Accept: application/json \ -H Referer: https://example.com/list \ -o ./data/list_${i}.json sleep 1 done # 合并成一份方便后续转静态页 python -c import json, glob rows [] for f in sorted(glob.glob(./data/list_*.json)): d json.load(open(f, encodingutf-8)) rows d.get(data, d if isinstance(d, list) else []) json.dump(rows, open(./data/all.json,w,encodingutf-8), ensure_asciiFalse) print(len(rows)) 逻辑说明接口通常校验Referer缺了会返回 403所以带上sleep 1保持节奏接口限流比页面更严格。合并那一步兼容两种返回结构——有的接口是{data: [...]}有的直接返回数组。拿到 JSON 之后有两条路一是保留 JSON另写一个本地模板页读取渲染适合数据要二次处理二是直接转成静态 HTML适合只做浏览归档。两条都比无头浏览器快维护成本也低。最后一步是把三份产物合并wget抓的静态页、渲染页、接口数据。做法是在 manifest 里按 URL 去重优先级定为「渲染页 静态页 接口生成页」避免同一个 URL 出现三个版本打开时自己都分不清哪个是准的。5. 避坑与排查五类翻车现场的现象、原因与解决下面这几条都是我在实际交付快照时踩过的每条的排查顺序是固定的先看现象再定位原因最后给动作。照着顺序走比漫无目的地改参数快得多。5.1 抓回来一堆重复页面磁盘被吃满现象镜像目录里出现大量文件名相似、内容几乎一致的文件几百 MB 起步du -sh一路涨。原因站点用查询串区分筛选和排序?sortasc、?sortdesc、?fromxxx排列组合递归把它们当成不同页面全抓了或者是分页链接里带会话参数每次访问参数都变被当成新 URL 反复入队。解决第一步在--reject-regex里挡掉排序和分享类参数(\?|)(sort|order|share|replytocom|utm_)|/print/这条能挡掉大部分。第二步改用 URL 池模式别递归。第三步抓完跑一次去重按文件内容哈希分组同组只留路径最短的那份。5.2 中文文件名变乱码或者整批 404现象本地目录里出现%E4%B8%AD%E6%96%87.html这类名字双击打不开或者 HTML 里明明是中文路径本地对应文件却不存在。原因URL 里的百分号编码和文件系统编码不一致。抓取时没解码本地文件就带着%XX解码了但 HTML 里的引用没同步改引用和实际文件名对不上。解决统一在抓取阶段用--restrict-file-nameswindows让工具自己做编码转换别手工掺和后处理脚本里用unquote还原再比对。如果已经抓完用第 3.3 的脚本出一次missing.csv按报告批量改名比肉眼找快得多。5.3 CSS 里的字体和背景图整批丢失现象页面结构完整但字体回退成系统默认按钮背景、图标全变空白。控制台里一堆 404。原因--page-requisites只解析 HTML 里的直接引用CSS 里url()写的背景图和font-face的字体文件是二级引用工具默认不跟。另外字体文件常放在 CDN 域名被域名白名单挡了。解决把 CDN 域名加进--domains再对 CSS 文件单独跑一轮抓取从 CSS 里抽出url(...)拼成完整地址写进urls.txt用wget -i批量取。或者干脆让后处理脚本把 CSS 里的url()也纳入扫描范围和第 3.3 的缺失检测合并成一份报告。5.4 本地双击打开白屏控制台报跨域现象file://协议打开 HTML控制台一片红提示跨域或模块加载失败页面什么都不显示。原因现代前端打包产物多用 ES Modulefile://协议下浏览器出于安全策略拒绝加载模块另外部分脚本读location.origin做判断本地协议下取到null直接抛错。解决别用双击起一个本地静态服务器。python -m http.server 8000一行就够然后访问http://localhost:8000/。这个动作要写进交付说明不然对方打开就是白屏还会以为你抓漏了。5.5 抓到一半开始返回 403 或 429现象前几百个请求正常之后连续失败日志里全是 403、429或者连接被重置。原因请求频率触发了站点或 CDN 的限流阈值也可能是 UA 被识别、Referer 缺失、Cookie 过期。还有一种情况是并发开太高短时间打满了对方的连接数配额。解决先把--wait调到 3 到 5 秒--limit-rate降到 200k 以下重跑失败清单仍然失败就看响应头里的Retry-After按它给的时间等待再不行就换时间段、分批跑。千万别在这个阶段加并发硬刷那是把「暂时被限」变成「永久被封」的最快方式。6. 增量更新与结果校验把一次性抓取变成可重复的流程第一次抓完就交付是最容易出事的地方。第二次要更新时你会发现wget -N的时间戳比对在动态站点上几乎失效——页面每次请求都返回当前的Last-Modified等于全量重下。我后来的做法是自己维护一份清单。# 抓取后立即生成基线URL、大小、内容哈希 find ./mirror -type f -name *.html -printf %p\t%s\n baseline.tsv find ./mirror -type f -name *.html -exec md5sum {} baseline.tsv下次抓取前先跑同样一份diff两个文件变化的行就是需要复核的页面。这比信任服务端时间戳可靠因为哈希只认内容。校验清单我固定跑四项校验项命令要点判读标准空文件find ./mirror -size 0数量应为 0非 0 说明抓取被截断未改写的外链grep -rl example.com --include*.html少量可容忍成片说明--convert-links没跑完断链第 3.3 脚本的missing.csv缺失率超过 5% 就要补抓体积分布du -sh ./mirror和文件数与干跑时的 URL 数对不上就查排除规则有一个细节值得单独说--convert-links是在整个任务成功结束后才统一改写的中途按 CtrlC 或者遇到未处理异常镜像目录里的链接还是线上绝对地址。所以我习惯在抓取完成后先跑一遍grep -rl https://example.com确认数量降到个位数以下再进入校验环节。这一步花不到十秒能提前发现「抓完了但没改写」这种最难排查的状态。从那以后我每次交付快照前都强制走一遍「空文件 → 外链残留 → 断链报告 → 体积核对」这四步不管多小的站点都不跳。跳过的那次后来在客户那边打开是白屏返工成本远高于这四步。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?