资讯动态

整站下载器实战:从wget到Python增量更新与避坑指南

发布时间:2026/10/18 11:52:11 来源:尧图企业网站定制
简介这是一款面向站长、运维人员及Web开发者的网站整站下载工具用于将目标站点的页面与资源完整抓取到本地便于离线浏览、站点备份或迁移分析。工具兼容ASP、JSP、PHP等常见服务端环境对动态站点同样具备较好的适配能力适合需要批量留存网站内容的中高级用户使用。压缩包为rar格式整体约540KB体积轻巧便于携带与快速部署。目前已有2183人学习下载说明其在整站抓取场景中具备一定实用价值。借助该工具读者可一次性获取整站页面与配套资源省去逐页手动保存的繁琐流程为后续的本地调试、内容归档或结构梳理提供便利也能在无网络环境下对照原始站点进行查阅与比对提升站点维护与资料整理的效率。1. 整站下载器到底能扒下来什么从一次离线归档需求说起有个做企业内训的朋友找我说他们内部学习平台上有几百个 HTML 页面、配套的 PDF 和产品图平台年底要下线问我能不能把整站“搬”到本地断网也能翻。这种需求其实很常见老官网改版前留档、竞品公开资料做离线分析、教学站点打包给学生。手动右键另存几百个页面能点到手抽筋而且 CSS、JS、图片的相对路径一断页面直接变白板。网站整站下载器就是干这个的——它从入口 URL 开始顺着页面里的链接一层层爬把 HTML、样式表、脚本、图片、字体甚至部分媒体文件按原目录结构落到本地并自动把链接改写成相对路径让离线浏览和在线几乎一样。它适合做归档、做离线文档、做前端资源逆向参考的人但不适合想批量抓取受保护内容或高频请求压垮对方服务器的场景。2. 整站下载器的核心机制链接发现、资源过滤与路径重写2.1 爬取队列与深度控制它怎么知道还有哪些页面没下整站下载器不是“打开一个页面就完事”它内部维护一个待抓取队列frontier。从种子 URL 入队开始每处理完一个页面就把页面里符合规则的链接追加到队列尾部直到队列为空或达到你设的深度上限。这里有两个参数直接决定结果--level或-l控制递归层数--page-requisites-p决定是否把页面渲染所需的 CSS、JS、图片一并拉下来。很多人第一次用只给了入口 URL结果只下到一个首页就是因为没开递归或深度设成了 0。常见做法是用wget的镜像模式做第一轮验证# --mirror 等价于 -r -N -l inf --no-remove-listing # -p 下载页面所需资源-k 把绝对链接转成相对链接 # -E 把 .html 后缀补全--convert-links 让离线浏览可用 wget --mirror --convert-links --adjust-extension \ --page-requisites --no-parent \ --wait1 --random-wait \ -e robotsoff \ https://example.com/docs/这段命令里--mirror是递归加时间戳的快捷方式--no-parent防止爬到上级目录--wait1 --random-wait是给服务器留喘息时间避免被当成攻击。-e robotsoff表示忽略 robots.txt这个参数有争议只建议在你有明确授权或归档自己站点时用。执行后本地会生成一个以域名命名的目录里面按 URL 路径分层存放文件。2.2 资源类型过滤别把整站下成几个 G 的垃圾场整站下载最怕无差别抓取。一个页面可能挂着自动播放的视频、几百兆的压缩包、甚至埋点脚本产生的动态 URL。如果不做类型过滤磁盘很快爆掉而且大量文件对离线浏览毫无价值。wget支持--reject和--accept按扩展名筛选--exclude-directories按目录排除。# 只接受网页和常见静态资源拒绝视频、压缩包和动态接口 wget --mirror --convert-links --page-requisites \ --accept html,htm,css,js,png,jpg,jpeg,gif,svg,woff,woff2 \ --reject mp4,avi,zip,rar,exe,dmg \ --exclude-directories/api,/admin,/tmp \ --wait0.5 --random-wait \ https://example.com/--accept和--reject同时出现时先匹配 accept 再排除 reject。--exclude-directories里的路径是相对于域名的写/api能挡住大部分动态请求。参数值用逗号分隔不要加空格。如果站点用了大量无扩展名的资源 URL这套过滤会漏需要换用支持 MIME 嗅探的工具比如httrack的--assume选项。2.3 路径重写与离线可用性为什么下载完页面还是乱的下载器把文件存到本地后原始 HTML 里的链接还是https://example.com/css/main.css这种绝对地址。离线打开时浏览器会去请求真实域名断网就加载失败。--convert-links的作用就是在下载完成后把所有能对应到本地文件的链接改成相对路径比如../css/main.css。但有个坑如果某个资源因为过滤规则没被下载链接不会被改写页面依然会缺样式。所以路径重写和资源过滤要配合着调先放宽过滤跑一遍确认页面完整后再收紧。httrack在这方面更直观它默认就会重写链接并且提供一个镜像日志# 用 httrack 镜像站点-O 指定输出目录*.css 等是过滤规则 httrack https://example.com/ -O /data/mirror/site \ *.example.com/* \ *.css *.js *.png *.jpg \ -*.mp4 -*.zip \ --sockets4 --pause200--sockets4限制并发连接数--pause200是每次请求后暂停 200 毫秒。这两个参数是防止把对方服务器打挂的关键别为了快把并发开到几十。3. 从零跑一遍整站下载环境准备、命令拆解与结果验证3.1 环境准备与工具选型wget、httrack 还是专用 GUI在 Linux 和 macOS 上wget和httrack都能通过包管理器直接装。Windows 下可以用wget的编译版或者用httrack的图形界面。选哪个取决于你的场景wget轻量、脚本化好、适合一次性归档httrack对复杂站点大量 JS 渲染、框架路由的兼容性更好自带缓存和断点续传还有一类基于浏览器内核的下载器能执行 JavaScript 后再抓取适合单页应用但资源占用高不适合大规模镜像。我一般先用wget跑一个小深度测试确认站点结构和过滤规则没问题再换httrack做全量。测试命令如下# 只抓两层限制总下载量快速验证规则 wget --mirror --convert-links --page-requisites \ --level2 --no-parent \ --accept html,css,js,png,jpg \ --quota50m \ --wait1 --random-wait \ https://example.com/docs/--level2表示从入口算起只跟两层链接--quota50m是硬性总量上限超过就停。这两个参数能在几分钟内告诉你入口页面是否完整、资源是否被正确过滤、有没有意外的重定向。3.2 命令拆解与参数调优并发、超时与重试怎么设整站下载的稳定性取决于三个参数组并发数、超时和重试。并发太高容易被封 IP太低又慢得离谱。wget本身是单线程的要并发得用xargs配合分段或者直接用httrack的--sockets。超时用--timeout设默认是 900 秒对慢站点可以调到 30 秒避免卡死。重试用--tries默认 20 次对不稳定站点可以保留但对返回 404 的页面重试没意义可以配合--retry-connrefused只重试连接层错误。# 分段并发下载把 URL 列表切成多份每份用一个 wget 进程 # 先生成 URL 列表这里假设已有 sitemap cat sitemap_urls.txt | split -n l/4 - urls_part_ for f in urls_part_*; do wget --page-requisites --convert-links \ --timeout30 --tries3 --wait0.5 \ -i $f -P /data/mirror/part_${f##*_} done wait这段脚本把 URL 列表均分成 4 份每份独立下载到不同目录最后合并。-i从文件读 URL-P指定前缀目录。注意--page-requisites在分段模式下可能导致资源重复下载适合页面间资源耦合不强的站点。如果站点资源高度共享还是用单进程httrack更省事。3.3 下载结果验证怎么确认整站真的“整”了下载完不是看文件数量就完事。验证分三步第一检查入口页面在本地打开是否样式完整、图片显示、链接可点第二用grep扫一遍 HTML 里还有没有残留的绝对 URL第三对比站点地图或爬取前后的 URL 列表看有没有大面积遗漏。# 查找本地 HTML 中残留的 http(s) 绝对链接 grep -rEo https?://[^\ ] /data/mirror/site --include*.html \ | sort | uniq -c | sort -rn | head -20 # 统计下载文件类型分布确认没有异常大文件 find /data/mirror/site -type f -printf %s %p\n \ | sort -rn | head -20第一条命令列出出现频率最高的绝对链接如果大量指向同一域名说明--convert-links没生效或资源没下全。第二条找出最大的 20 个文件如果出现几百兆的意外文件说明过滤规则有漏。这两步做完基本能判断这次镜像的质量。4. 整站下载避坑实录五条血泪经验4.1 现象下载到一半磁盘满了原因没设配额和类型过滤第一次帮人归档一个技术博客没加--quota也没拒绝视频结果站点里嵌了几个会议录像直接写满 40G 磁盘下载进程被系统杀掉已下文件还处于半损坏状态。解决方法是始终加--quota500m之类的硬上限并用--reject挡掉mp4,webm,zip等大文件。如果已经写满先清理临时文件再用httrack的断点续传接着下不要直接重跑。4.2 现象页面能打开但样式全丢原因CSS 里的字体和背景图没被识别为页面必需资源wget --page-requisites只解析 HTML 里的直接引用CSS 文件里通过url()引用的字体、背景图有时不会被自动加入队列。解决方法是把--accept放宽到包含woff,woff2,ttf,eot,svg或者用httrack的--near选项它会解析 CSS 内的资源引用。另一个办法是下载完后用脚本扫一遍 CSS 文件把缺失的url()目标手动补下。4.3 现象被目标服务器封 IP原因并发过高或没加延迟有次用httrack默认参数跑一个中小型站点--sockets没改默认值偏高跑了不到十分钟就收到 403换 IP 后才恢复。后来我固定用--sockets2到4并且加--pause500即每次请求后停半秒。wget这边用--wait1 --random-wait让间隔在 0.5 到 1.5 秒之间随机比固定间隔更不容易触发风控。如果已经被封等一段时间再试别换着花样重试。4.4 现象下载的 HTML 里链接还是绝对地址原因--convert-links只在下载完成后执行中途中断不生效wget的链接转换是最后一步如果下载中途因为超时或手动 CtrlC 中断已经落地的文件里链接还是原始的。解决方法是让下载完整跑完或者用httrack它的链接重写是增量的中断后重新运行会继续处理。如果必须中断记下已完成的层级下次用--continue接着下但链接转换仍需最后统一跑一次。4.5 现象单页应用SPA下载后只有空壳原因内容由 JavaScript 动态渲染静态爬虫拿不到用wget或httrack抓 React、Vue 这类前端框架做的站点经常只得到一个div idapp/div因为真实内容在浏览器执行 JS 后才生成。解决办法是换用能执行 JS 的下载器比如基于无头浏览器的方案先渲染再保存 DOM。但这类工具资源消耗大不适合整站批量。折中方案是找到站点的 API 接口直接抓 JSON 数据再本地组装不过这需要针对具体站点分析没有通用命令。5. 进阶用 Python 做可控整站下载与增量更新5.1 为什么还要自己写现成工具的边界在哪wget和httrack能覆盖八成场景但遇到需要登录态、需要按自定义规则过滤、需要把下载结果直接入库或做增量对比时它们就不够灵活了。比如你想只下载最近 7 天更新的页面或者想把 HTML 里的正文抽出来存成 Markdown这些都得自己写。Python 的requests加BeautifulSoup是最小可行组合再配上urljoin处理相对路径几十行就能搭一个可控的爬虫。import os import time from urllib.parse import urljoin, urlparse import requests from bs4 import BeautifulSoup # 基础配置入口、保存目录、允许的域名和资源后缀 START_URL https://example.com/docs/ SAVE_DIR /data/mirror/custom ALLOWED_DOMAIN example.com ALLOWED_EXT {.html, .css, .js, .png, .jpg, .svg, .woff2} visited set() queue [START_URL] def save_file(url, content): 按 URL 路径在本地创建目录并写入文件 path urlparse(url).path if path.endswith(/): path index.html local_path os.path.join(SAVE_DIR, path.lstrip(/)) os.makedirs(os.path.dirname(local_path), exist_okTrue) with open(local_path, wb) as f: f.write(content) while queue: url queue.pop(0) if url in visited: continue visited.add(url) try: resp requests.get(url, timeout15) resp.raise_for_status() except Exception as e: print(f跳过 {url}: {e}) continue # 只保存允许的后缀HTML 额外解析链接 ext os.path.splitext(urlparse(url).path)[1].lower() if ext in ALLOWED_EXT or ext : save_file(url, resp.content) if text/html in resp.headers.get(Content-Type, ): soup BeautifulSoup(resp.text, html.parser) for tag in soup.find_all([a, link, script, img]): attr href if tag.name in (a, link) else src link tag.get(attr) if not link: continue full urljoin(url, link) if urlparse(full).netloc ALLOWED_DOMAIN and full not in visited: queue.append(full) time.sleep(0.5) # 固定延迟保护目标服务器这段代码的核心逻辑用队列做广度优先遍历visited去重urljoin把相对链接转成绝对链接save_file按 URL 路径建目录。ALLOWED_EXT控制资源类型time.sleep(0.5)是硬编码延迟。它比wget灵活的地方在于你可以在save_file里加任何处理比如把 HTML 转 Markdown、把图片上传到对象存储、把元数据写进数据库。5.2 增量更新与去重用哈希判断文件是否变化整站下载不是一次性的站点会更新。每次全量重下浪费带宽和时间。增量更新的思路是下载前先发 HEAD 请求拿Content-Length和Last-Modified和本地记录对比没变就跳过。更稳妥的是下载后算 SHA256和上次的哈希表比对变了才覆盖。import hashlib import json import os HASH_DB /data/mirror/hashes.json def load_hashes(): if os.path.exists(HASH_DB): with open(HASH_DB) as f: return json.load(f) return {} def save_hashes(hashes): with open(HASH_DB, w) as f: json.dump(hashes, f, indent2) def file_sha256(path): h hashlib.sha256() with open(path, rb) as f: for chunk in iter(lambda: f.read(8192), b): h.update(chunk) return h.hexdigest() # 在 save_file 之后调用对比哈希决定是否覆盖 hashes load_hashes() local_path /data/mirror/custom/docs/index.html new_hash file_sha256(local_path) if hashes.get(local_path) ! new_hash: hashes[local_path] new_hash save_hashes(hashes) print(f更新: {local_path}) else: print(f未变: {local_path})HASH_DB是一个 JSON 文件键是本地路径值是 SHA256。每次下载后更新下次运行前对比。这个机制配合requests的If-Modified-Since头能进一步减少无效请求。注意哈希计算对大文件有 IO 开销可以只对 HTML 和 CSS 做图片和字体用文件大小加修改时间粗判。5.3 验证下载完整性的三个硬指标自己写的下载器最容易出两个问题链接解析漏了、编码搞错了。验证时盯三个指标第一入口页面的所有img、link、script标签的src或href在本地是否存在对应文件第二HTML 文件的编码是否和原始一致中文站点常见 GBK 被误存成 UTF-8 导致乱码第三总文件数和站点地图或爬取列表的差异是否在可解释范围内。# 检查 HTML 中引用的本地资源是否存在 from bs4 import BeautifulSoup import os def check_page(page_path, base_dir): with open(page_path, r, encodingutf-8, errorsignore) as f: soup BeautifulSoup(f.read(), html.parser) missing [] for tag in soup.find_all([img, link, script]): attr src if tag.name in (img, script) else href ref tag.get(attr) if ref and not ref.startswith((http, //, data:)): target os.path.normpath(os.path.join(os.path.dirname(page_path), ref)) if not os.path.exists(target): missing.append(ref) return missing # 对入口页面跑一遍输出缺失资源列表 missing check_page(/data/mirror/custom/docs/index.html, /data/mirror/custom) print(f缺失 {len(missing)} 个资源) for m in missing[:10]: print(m)这个检查脚本能快速定位是哪些资源没下到。如果缺失列表里全是同一类文件比如全是.woff2说明过滤规则漏了字体如果全是某个子目录的图片说明那个目录被--exclude-directories误伤了。编码问题则要在save_file里根据resp.encoding或Content-Type里的 charset 显式解码再写入不要直接写resp.content到文本文件。从那以后我每次做整站归档都强制先跑一遍小深度测试确认过滤规则和链接重写没问题再放开全量。增量更新和哈希对比也成了固定动作省下的重复下载时间够喝好几杯咖啡。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑