简介:博客下载工具是面向 CSDN 博主与需迁移站点的开发者的 Java 命令行/Eclipse 小工具,可批量抓取博文与附件,解决手动保存费时、站点关闭或改版后内容难找回的问题。作者在本地调试后,已成功完成 30 多篇长文、约 1GB 规模的完整下载;源码基于 htmlparser.jar 实现正文解析,但因解析库版本更新存在小 bug,且部分域名被写死,抓取其他平台博客时需全局替换域名后重新编译。压缩包为 zip 格式,共 7 个文件,主要由 DoBackup.java 源码、编译后的 class、Eclipse 工程配置(.project、.classpath、prefs)及 htmlparser.jar 组成,体积仅 272KB,结构简洁,适合有一定 Java 基础的读者阅读排错。该资源已有 470 人学习,对希望二次开发或自制博客备份工具的人有直接参考价值。
1. 博客下载工具:比爬虫更省心的离线备份方案
你可能遇到过这种场景:想把自己写了三年的博客完整备份一份,或者把某个技术博主的一系列文章离线收藏,结果发现浏览器“另存为”只能存一页,还得手动点下一页、翻评论、存图片。博客下载工具就是专门解决这类需求的——它能自动识别博客的文章列表、正文容器和图片资源,把整站内容批量拉取到本地,输出成 HTML、Markdown 或 PDF。和通用爬虫比,它的优势在于“懂博客”:知道 RSS、sitemap、分页规则,也知道怎么过滤导航栏和广告。它适合三类人:定期备份自己博客的站长、维护本地知识库的读者、需要为搜索引擎或语义分析准备语料的开发者。下面我会从架构选型讲到避坑,最后给出一套可以照着复现的流程。
2. 拆解博客下载的三种主流架构:静态抓取、动态渲染与API优先
2.1 静态博客(Hexo/Hugo)怎么抓:直接下 HTML 还是克隆仓库
静态博客的全站都是预生成的 HTML 文件,目录结构基本就是public/目录的完整映像。这类站点抓起来最省心:你先拿到文章列表页,解析出正文页链接,下载 HTML 后,图片和 CSS 要么是相对路径,要么有固定的 CDN 前缀。但“直接下 HTML”和“克隆生成仓库”是两条完全不同的路线。如果博客源码本身是公开的 Git 仓库,我更推荐git clone——不仅拿到成品 HTML,还拿到 Markdown 源文件,后续转格式完全不依赖网页结构。缺点是很多博主只部署了生成器产物,没开源源码;这时候只能用下载器按页面抓取。
实际判断方法很简单:在文章页右键查看源码,如果能看到完整的文章纯文本(而不是 JS 变量里的数据),就是静态渲染。用curl看一眼响应头,如果server字段是GitHub Pages、Vercel或者没有任何动态渲染痕迹,基本就可以按静态处理。
curl -I "https://example.com/posts/hello-world/" | head -n 10返回里没有x-powered-by: PHP、Set-Cookie这类动态站特征,就可以走静态抓取流程。参数上要注意:curl -I是只取请求头,head -n 10控制输出行数,避免头信息太长刷屏。如果手头没有 Linux 命令,用浏览器开发者工具的网络面板看响应头也是一样的效果。
2.2 动态博客(WordPress/Typecho)如何定位正文:RSS、sitemap 还是解析 DOM
动态博客的文章列表通常是数据库查询结果拼出来的,分类、标签、分页能生成无数个 URL。最可靠的做法不是硬解析首页,而是先找 RSS 或 sitemap。WordPress 默认有/feed/,Typecho 也支持/feed/,很多程序还提供/sitemap.xml。你只需要从 RSS 里解析<link>节点,就能拿到全部文章地址,而且 RSS 里通常带了摘要和发布时间,可以直接用来做索引。
但 RSS 不一定完整:有的作者只输出摘要,有的 feed 地址做了改写,有的站点干脆没开。此时用 sitemap 更稳妥,因为它会列出所有公开页面。我一般先用一个脚本把 sitemap 解析成 URL 清单,再过滤出包含文章特征路径的地址(如/archives/、/posts/)。DOM 解析只作为兜底方案——当 RSS 和 sitemap 都拿不到时,才去解析首页和分页的<a>标签。优先级建议是:sitemap > RSS > 首页分页解析,这样能最大程度避免漏抓。
curl -s "https://example.com/sitemap.xml" | grep -o '<loc>[^<]*</loc>' | sed 's/<\/\?loc>//g'这条命令把 sitemap 里的所有 URL 提取出来。grep -o只输出匹配部分,sed去掉标签。注意有些博客的sitemap.xml是索引文件,里面嵌套了子 sitemap,你需要先解析出子文件地址,再逐个下载。这一步是最容易漏的,我见过有人抓了 500 个 URL 就以为全站备份完了,实际上博客有 8000 篇文章,就是因为只扫描了外层索引。
2.3 单页应用(Ghost/自研前端)为什么需要渲染引擎:无头浏览器的取舍
Ghost 博客的官方前端是服务端渲染的,直接抓 HTML 没问题。但很多自研博客用了 React 或 Vue,文章正文是 JS 从接口拉回来再渲染的,请求 HTML 只能拿到一个空壳<div id="app">。这时必须用无头浏览器,比如 Playwright 或 Puppeteer,等页面渲染完成后抓取 DOM。代价是内存占用高、抓取速度慢、容易触发目标站点的性能防护。取舍标准是:先查有没有公开 API,比如 Ghost 有/ghost/api/,自研站点可能暴露 JSON 数据端点。有 API 就优先走 API,数据干净还自带字段;没有 API 才上无头浏览器,而且只对文章详情页用渲染,列表页尽量用普通请求。
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page(user_agent="Mozilla/5.0 BlogDownloader") page.goto("https://example.com/posts/hello/", wait_until="networkidle", timeout=20000) html = page.content() browser.close()这段代码用 Playwright 打开页面,等网络空闲后取整个 HTML。wait_until="networkidle"是个双刃剑:对于依赖多个接口的页面,它能保证数据加载完;但如果页面有长轮询或一直有请求,这个参数会卡到超时。我建议优先用domcontentloaded配合固定等待page.wait_for_timeout(1500),对大多数博客更稳。参数headless=True是无头模式,timeout=20000是导航超时 20 秒。无头浏览器这块属于“黑匣子”环节,容易翻车在页面是否真的渲染完,需要先手动在浏览器里确认接口响应时间,再来设等待值。
| 博客类型 | 典型特征 | 第一推荐 | 兜底方案 |
|---|---|---|---|
| 静态博客 | HTML 自带全文 | git clone | 批量下载 HTML |
| 动态博客 | 响应头有 PHP/Python | sitemap.xml | RSS / 分页解析 |
| SPA 博客 | JS 渲染空壳 | 找后端 API | 无头浏览器渲染 |
这张表的价值在于,你在接到一个“下载这个博客”的需求时,先用 5 分钟判断类型,就能决定整个方案的方向。方向错了,后面做得再精细都会白费。
3. 用 Python + Requests + BeautifulSoup 跑通最小下载流程
3.1 准备工作:确认目标 URL 规则与 robots.txt
动手前一定要看robots.txt,这不是形式主义,而是为了知道哪些路径被允许抓取。同时确认 URL 规则是否规整,比如日期前缀/2024/01/、ID 后缀_123.html。规整的 URL 让后续构造分页几乎是白送的。这一步用代码做最快。
import requests headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) BlogBackupBot/1.0", "Accept-Language": "zh-CN,zh;q=0.9", } robots = requests.get("https://example.com/robots.txt", headers=headers, timeout=10) print(robots.text)headers里的User-Agent建议写成带联系方式的爬虫标识,很多站点对无 UA 的请求直接拒绝。timeout=10是连接和读超时,不设的话一旦服务器不响应,脚本会挂到默认的几分钟。如果 robots 里写着Disallow: /wp-admin/,你在后面解析链接时要主动过滤这类路径。另外,robots.txt 只告诉你哪些路径被爬虫协议限制,不代表服务器真的不会拒绝你,后面还要靠请求频率控制。
3.2 从列表页解析文章链接:选择器与去重
以常见的ul.article-list > li > a结构为例,用 BeautifulSoup 提取链接并做归一化。归一化指的是把相对路径拼接成绝对路径、去掉 URL 里的统计参数、转成统一协议。这一步骤最容易被忽略的是重复链接,比如?utm_source=rss和?from=home指向同一篇文章,需要去重。
from bs4 import BeautifulSoup from urllib.parse import urljoin, urlparse html = requests.get("https://example.com/page/1/", headers=headers, timeout=10).text soup = BeautifulSoup(html, "html.parser") links = [] for a in soup.select("ul.article-list > li > a"): href = a.get("href") if not href: continue full_url = urljoin("https://example.com/page/1/", href) parsed = urlparse(full_url) if parsed.path.count("/") < 3: # 太浅的路径多半是栏目页,不是文章 continue links.append(full_url) links = list(dict.fromkeys(links)) # 保留顺序去重 print(len(links))urljoin是处理相对路径的标准方法,第二个参数可以是./hello.html这种形式。parsed.path.count("/") >= 3是个启发式过滤,能挡掉大量标签、分类页,比如/tag/python/的路径段不够深,通常不是文章。dict.fromkeys(links)用字典键去重,同时保留首次出现的顺序,比set更稳。注意这里只抓了第 1 页,后面还要处理翻页。
3.3 下载文章并保存正文与图片:并发控制在个位数
对每篇文章,先请求 HTML,再用选择器切出article节点,最后把正文中的图片 URL 下载到本地文件夹。并发控制是关键,concurrent.futures的ThreadPoolExecutor我一般只用 3 到 5 个 worker,避免打爆小站。
import re import os from concurrent.futures import ThreadPoolExecutor def save_post(url): html = requests.get(url, headers=headers, timeout=10).text soup = BeautifulSoup(html, "html.parser") article = soup.select_one("article.post-content") or soup.select_one(".entry-content") if not article: return url, False title = soup.select_one("h1").get_text(strip=True) if soup.select_one("h1") else url # 清理脚本、样式和 iframe for tag in article(["script", "style", "iframe"]): tag.decompose() os.makedirs("backup", exist_ok=True) file_name = re.sub(r'[\\/:*?"<>|]', "_", title) + ".html" with open(os.path.join("backup", file_name), "w", encoding="utf-8") as f: f.write(f"<h1>{title}</h1>{article.prettify()}") return url, True urls = ["https://example.com/post/1/", "https://example.com/post/2/"] with ThreadPoolExecutor(max_workers=3) as pool: results = list(pool.map(save_post, urls))这里if not article比assert article更安全,因为有些页面解析失败会返回空,不应该让脚本整体崩溃。tag.decompose()把干扰元素移除,注意它会连同子节点一起销毁,别误删正文里的代码块。写文件时encoding="utf-8"必须显式指定,否则 Windows 下会用 GBK 写入,之后再转换会乱码。re.sub把标题里的非法字符替换成下划线,这是跨平台写文件的必修课。这个版本刻意不并发下载图片,因为图片请求多,和文章 HTML 并发叠加容易触发限流。
3.4 第一次跑通的验收标准
跑完上面的脚本,至少要检查三件事:文章数是否和列表页一致、HTML 是否可以本地打开、图片是否完整。我通常会用一条命令统计文件数。
find backup -name "*.html" | wc -l如果数量明显少于预期,优先去日志里看哪些 URL 抛了异常。下载器不怕慢,怕静默失败——Requests 默认不会把 HTTP 404、500 当时的响应体打出来,建议在save_post里加一句print(url, resp.status_code),先确保每个请求状态都是 200,再谈解析。这一步是很多人跳过的“后悔药”,后面调试时能省一半时间。
4. 处理分页、图片与正文提取:必须调好的三个环节
4.1 分页类型的判断与翻页终止条件
博客的分页看起来简单,实际上有四种形态。URL 参数型长这样:/?paged=2,路径型长这样:/page/2/,这两种用for循环就能翻页。第三种是“加载更多”按钮,点击后通过 POST 请求返回 HTML 片段,拼接路径的逻辑完全不同。最麻烦的是第四种无限滚动,正文和列表页都在同一页面里追加渲染,URL 不变,这时必须抓包找到 JSON 接口,或者用无头浏览器模拟滚动。
判断方法很直接:打开浏览器开发者工具的网络面板,点击第二页,看新的请求 URL 和方式。如果 URL 规律变化,就是第一或第二种;如果只有 XHR 请求,就是第三或第四种。拿到规律后写循环,但一定要设终止条件。最常见的错误是循环里请求一个不存在的页号,服务器返回 200 和空列表,脚本傻傻地一直翻下去。我一般会预取一个上限页数,比如先从第一页解析出“下一页”的链接,如果下一页等于当前页就停止;或者在连续两个空列表后强制 break。
page = 1 while True: url = f"https://example.com/page/{page}/" soup = fetch_soup(url) items = soup.select("li.article a") if not items: break for item in items: process_item(item) page += 1 if page > 100: # 安全上限 breakfetch_soup是封装好的抓取函数,里面要带 headers 和 timeout。if not items是空页终止,page > 100是防御性上限,避免坏数据让脚本跑到天荒地老。如果博客真的有一百多页,你就把上限抬高,但要确保不是死循环。更优雅的做法是检查link[rel=next]标签,不过有些主题不输出它,所以我用空列表判断为主、上限兜底。
4.2 图片懒加载与防盗链:Header、Referer 和重试机制
懒加载是博客下载的一大障碍。不同主题写法不一样,常见属性有这些。
| 属性名 | 出现位置 | 真实地址含义 |
|---|---|---|
| >import time import random def download_image(img_url, referer, local_path): for attempt in range(3): try: resp = requests.get(img_url, headers={"User-Agent": headers["User-Agent"], "Referer": referer}, timeout=10) if resp.status_code == 200 and len(resp.content) > 100: with open(local_path, "wb") as f: f.write(resp.content) return True except requests.RequestException: pass time.sleep(random.uniform(1, 3)) return False 重试循环里 4.3 正文提取的两种路线:Readability 算法与手动 DOM 清理正文提取决定下载下来的是“干净文章”还是“网页垃圾箱”。Mozilla 的 Readability 算法在 Python 里有
验证提取效果有个土办法:把提取出来的文本字符数和文章在页面上的肉眼字数对比,如果差了一倍以上,多半是漏了某些懒加载区块。另外,有些博客把正文放在 JSON-LD 结构化数据里,比如 5. 博客下载避坑指南:5 个最容易翻车的问题与排查方法5.1 现象:下载的 HTML 用浏览器打开全是乱码原因:服务器返回的是 GBK 编码,你的脚本按 UTF-8 解析了;或者写文件时系统默认用了 GBK。解决:先让 Requests 自动猜测编码, 5.2 现象:文章正文一半能抓到,一半是空原因:目标站点用了懒加载的正文分块,或者正文是多个接口拼接的,Readability 算法只等到第一条数据返回。解决:不要用 5.3 现象:本地打开备份的 HTML,图片全是裂图原因:图片 URL 是相对路径,但你的备份目录结构和原站不一致;或者图床防盗链只认博客域名。解决:下载图片到本地后,用 5.4 现象:重复跑下载脚本,文件越来越多原因:没有做增量更新,每次都是全量抓取,覆盖策略不对。解决:记录每篇文章的 URL 和最后修改时间(可以从 5.5 现象:连续抓几十页后突然全部 403原因:请求频率太高,触发了站点的反爬策略。解决:先停半小时,然后把每次请求的间隔从 0.2 秒拉到 2 秒以上,并关闭并发。用
6. 把下载结果转成 Markdown、PDF 与索引页:最后的整理技巧下载下来的原始 HTML 通常还带着主题样式,不便阅读和检索。我习惯用 Pandoc 统一转成 Markdown,再按需生成 PDF。Pandoc 的命令很简单,但有几个参数值得注意。
生成索引页是让备份真正“可用”的一步。我用一个简单的 Python 脚本扫描所有 Markdown 文件,提取 我自己的习惯是:下载脚本跑完后,先随机抽 3 篇文章手工检查,再跑一遍总数统计,最后用 博客下载工具这块,最容易翻车的永远不是代码逻辑,而是对目标站点结构的理解。先把类型判断做对,再追求速度,最后再考虑格式转换。这个顺序反了,就是在垃圾数据上盖高楼。希望这套流程能帮你少踩几个坑,顺利把喜欢的博客搬回家。 本文还有配套的精品资源,点击获取
版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设
2026/10/29 9:41:23
工业级电源路径保护:TPS259483AYWPR与STM32F767ZG协同设计/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …
网站建设
2026/10/29 9:41:19
HC32系列MCU量产烧录:J-Flash定制化配置与产线避坑指南/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …
网站建设
2026/10/29 9:40:15
工业级电源路径保护:TPS259483AYWPR与PIC18F45K50软硬协同设计/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …
网站建设
2026/10/29 9:39:04
OpenShell 开放外壳设计:从能力注册到插件化扩展的工程实践1. OpenShell 是什么:从一个“壳”字说起第一次看到 OpenShell 这个名字,很多人会下意识把它和 Linux 的 shell、终端、命令行工具联系在一起。这个直觉不算错,但也不完全对。OpenShell 的核心定位,是给一个已有的系统或程序套上一…
网站建设
2026/10/29 9:38:22
ESP-IDF 上跑 AI Agent:端云协同的物联网智能交互实战/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …
网站建设
2026/10/29 9:38:10
STM32+TPS259483智能电源路径保护设计/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … |