资讯动态

博客下载工具:批量归档博文到本地Markdown知识库

发布时间:2026/10/17 5:18:12 来源:尧图企业网站定制
简介这份资源是面向需要批量备份CSDN博文的开发者与内容创作者的博客下载工具针对博客搬家、长文归档等场景解决逐篇手动保存效率低的问题。压缩包共7个文件约272KB包含Java源码、编译后的class文件、Eclipse工程配置以及htmlparser.jar爬虫工具包结构精简便于在本地IDE中直接导入调试。作者在本地实测30多篇长文、单篇近1G的内容均能完整下载说明工具在长文场景下具备一定稳定性。需要注意的是源码中部分位置写死了CSDN域名若想抓取其他博客站点需自行替换全部域名同时博文源码存在小bug可能与htmlparser.jar版本更新有关可作为二次开发的排错切入点。目前已有470人学习下载适合具备Java基础、希望研究爬虫抓取与博客迁移方案的读者参考借鉴。1. 博客下载工具把散落的博文归档成可检索的本地知识库你有没有遇到过这种情况某个技术博客上收藏了几十篇实战笔记结果某天再点进去要么文章被作者删了要么平台改版后链接全部失效收藏夹变成一片灰色。更麻烦的是有些内容你当时没记笔记等到项目里真要用的时候只能凭记忆去搜搜半天找不到原文。博客下载工具就是解决这个问题的——它把在线博文批量抓取下来转成 Markdown 或 HTML 存到本地让你拥有一份不依赖平台存活的离线副本。这类工具的核心用户是技术写作者、知识管理爱好者和需要长期跟踪某个博主更新的人。常见形态有两种一种是浏览器插件边浏览边保存单篇另一种是命令行脚本输入博主主页或专栏地址自动翻页抓取全部文章。今天要拆的这套博客下载工具走的是第二种路线适合需要批量归档、定期同步的场景。它不依赖平台官方导出功能而是通过解析页面结构提取正文、标题、发布时间和标签最终输出结构清晰的本地文件树。2. 博客下载工具的核心机制从 URL 到本地 Markdown 的完整链路2.1 抓取策略选型为什么用请求模拟而不是浏览器自动化这类工具最关键的决策点是抓取方式。常见做法有三种直接 HTTP 请求加 HTML 解析、无头浏览器渲染、调用平台开放接口。无头浏览器最重启动一个 Chromium 实例动辄几百兆内存抓几十篇还行上千篇就容易崩。开放接口最稳但多数博客平台并不提供完整的文章列表接口或者需要申请密钥对个人用户不友好。这套工具采用的是第一种用 requests 或 httpx 发请求拿到 HTML 后用解析库提取内容。它的前提是目标博客的正文是服务端渲染的也就是查看网页源代码时能直接看到文章文字。如果目标站点是纯前端渲染这套方案会抓不到内容需要换无头浏览器方案。判断方法很简单在浏览器里右键查看页面源代码搜索文章标题里的一个词如果能搜到说明服务端渲染这套工具直接可用。选请求模拟的另一个理由是速度。单篇请求加解析通常在 0.5 到 1 秒之间一千篇文章十几分钟能跑完。无头浏览器单篇要 3 到 5 秒同样的量级要跑一个多小时而且中途容易因为内存泄漏翻车。对于定期同步场景速度差异直接决定你能不能把它放进每日定时任务里。2.2 页面解析标题、正文、时间、标签怎么定位解析环节是整个工具最脆弱的部分因为博客平台的页面结构随时可能改版。这套工具把解析规则做成了可配置的不写死在代码里。配置文件里定义每个字段的 CSS 选择器或 XPath抓取时按配置去匹配。# config/parser.yaml 示例定义某博客平台的解析规则 site: example_blog article_list: # 文章列表页的每篇文章链接容器 item_selector: div.article-item h2 a # 从列表页提取文章 URL 的属性 url_attr: href article_detail: # 文章标题 title: h1.article-title::text # 正文容器取内部 HTML content: div.article-content # 发布时间 publish_time: span.publish-date::text # 标签列表 tags: div.tag-list a::text # 需要从正文中剔除的元素广告、推荐阅读 remove_selectors: - div.ad-container - div.related-posts这份配置的逻辑是先定义列表页怎么找到每篇文章的链接再定义详情页怎么提取各个字段。remove_selectors是关键参数博客正文里常混着平台插入的广告位和推荐模块不剔除的话导出的 Markdown 里会夹带一堆无关内容。参数调整时用浏览器开发者工具选中目标元素右键复制选择器粘贴到配置里即可。如果某天抓取结果里标题为空大概率是title选择器失效了改这一行就行不用动代码。2.3 正文转 Markdown图片、代码块、表格的处理边界抓到 HTML 正文后需要转成 Markdown。这一步用 html2text 或 markdownify 这类库能完成大部分工作但有几个边界情况需要手动处理。图片是第一个坑。博客图片通常用相对路径或 CDN 链接直接转 Markdown 后本地打开时图片加载不出来。工具的做法是把图片下载到本地images/目录然后把 Markdown 里的图片地址替换成相对路径。代码块是第二个坑有些博客的代码高亮用div加行号实现直接转会变成一堆乱码需要先识别出代码容器提取纯文本再包上 Markdown 代码块标记。表格是第三个坑复杂合并单元格的表格转 Markdown 后结构会乱工具的策略是保留原始 HTML 表格不强行转 Markdown。import re from markdownify import markdownify as md def html_to_markdown(html: str, image_map: dict) - str: # 先移除配置里指定的广告、推荐模块 for sel in remove_selectors: html remove_element_by_selector(html, sel) # 处理代码块把高亮容器替换成纯文本 pre/code html normalize_code_blocks(html) # 转 Markdown text md(html, heading_styleATX, bullets-) # 替换图片链接为本地相对路径 for remote_url, local_path in image_map.items(): text text.replace(remote_url, local_path) # 清理多余空行 text re.sub(r\n{3,}, \n\n, text) return text这段代码里normalize_code_blocks是自定义函数负责把各种博客平台五花八门的代码高亮结构统一成标准pre/code。image_map是抓取阶段就建好的远程 URL 到本地路径的映射表。heading_styleATX保证标题用#而不是下划线形式兼容性更好。最后那个正则把三个以上连续换行压成两个避免导出的 Markdown 里出现大片空白。3. 从零跑通一次批量归档环境、命令与参数调优3.1 环境准备与依赖安装这套工具是 Python 写的建议用 3.9 以上版本。依赖不多核心是 requests、beautifulsoup4、markdownify、pyyaml 这几个。为了避免污染系统环境我一般会建一个虚拟环境。# 创建并激活虚拟环境 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装依赖 pip install requests beautifulsoup4 markdownify pyyaml lxml # 验证安装 python -c import requests, bs4, markdownify, yaml; print(ok)lxml是解析器比 Python 自带的 html.parser 快很多抓大量页面时差距明显。如果安装 lxml 报错通常是缺少系统级的 libxml2 开发库Linux 上装libxml2-dev和libxslt1-dev即可Windows 上直接用预编译 wheel 一般不会出问题。3.2 配置文件编写与首次试跑工具的主配置文件分三块目标站点信息、抓取参数、输出设置。第一次跑建议把抓取范围限制在前 5 篇确认解析规则正确后再放开全量。# config/settings.yaml target: # 博主主页或专栏列表页地址 start_url: https://example-blog.com/user/column/123 # 列表页翻页参数常见是 ?pageN page_param: page max_pages: 3 # 首次试跑只抓 3 页 crawl: delay: 1.5 # 每篇间隔秒数别设太小 timeout: 15 # 单请求超时 retry: 3 # 失败重试次数 user_agent: Mozilla/5.0 (compatible; BlogArchiver/1.0) output: dir: ./archive format: markdown # 可选 markdown / html download_images: true image_dir: images filename_template: {date}-{title} # 文件名模板delay这个参数值得多说一句。设成 0 虽然快但容易触发目标站点的频率限制轻则返回 429 错误重则临时封 IP。我一般设 1 到 2 秒一千篇文章也就多花二十分钟换来的是稳定跑完不中断。filename_template里的{date}和{title}是占位符工具会自动替换。标题里如果有斜杠、冒号这类文件系统不支持的字符工具会自动替换成下划线。配置写好后跑一次试抓python blog_archiver.py --config config/settings.yaml --dry-run--dry-run表示只解析不写文件输出每篇文章的标题、时间和正文长度。检查输出里标题是否正常、正文长度是否合理太短说明正文选择器没匹配到。确认无误后去掉--dry-run正式跑。3.3 增量同步与断点续传全量抓取只跑一次后续需要的是增量同步——只抓新发布的文章。工具用一个本地 JSON 文件记录已抓取的文章 ID 或 URL每次启动时先加载这个记录跳过已存在的。import json import os STATE_FILE .archive_state.json def load_state() - set: if os.path.exists(STATE_FILE): with open(STATE_FILE, r, encodingutf-8) as f: return set(json.load(f)) return set() def save_state(done_urls: set): with open(STATE_FILE, w, encodingutf-8) as f: json.dump(sorted(done_urls), f, ensure_asciiFalse, indent2) # 抓取循环里判断 state load_state() for url in article_urls: if url in state: continue # 已抓过跳过 # ... 抓取和保存逻辑 ... state.add(url) save_state(state) # 每篇都存防止中途崩溃丢进度每抓完一篇就写一次状态文件看起来有点频繁但这是断点续传的关键。如果抓了 800 篇时网络断了或者程序崩了重启后能从第 801 篇继续不用从头再来。状态文件用 URL 做键而不是标题因为标题可能重复或被修改URL 更稳定。4. 避坑与排查抓取失败、乱码、图片丢失的常见原因4.1 现象抓取结果为空或只有导航栏文字原因通常是正文选择器失效。博客平台改版后正文容器的 class 名变了配置里的选择器匹配不到任何元素工具就把整个页面的可见文字都抓了下来结果全是导航和页脚。解决办法用浏览器打开一篇目标文章按 F12 打开开发者工具用元素选择器点一下正文区域看实际的标签和 class。把新的选择器更新到配置文件里。如果平台改版频繁可以考虑用更宽松的匹配策略比如按article标签或id包含content的div来定位比精确 class 名更抗改版。4.2 现象导出的 Markdown 里中文变成乱码原因是响应内容的编码判断错了。requests 库会根据 HTTP 头里的Content-Type猜编码但有些博客平台返回的头信息不准确导致中文被按 Latin-1 解码。解决办法在请求后强制指定编码。常见做法是response.encoding response.apparent_encoding让 chardet 库去探测实际编码。如果探测也不准直接看页面源代码里的meta charset声明手动设成utf-8或gbk。这个坑在抓一些老博客时特别常见血泪经验是宁可手动指定也别信自动判断。4.3 现象图片下载了一堆但 Markdown 里显示不出来原因通常是图片链接替换时路径没对上。工具下载图片时按 URL 的哈希值命名但替换 Markdown 时用的可能是原始文件名两边不一致。解决办法检查image_map的构建逻辑确保下载时用的键和替换时用的键是同一个。另一个常见原因是图片有防盗链直接请求返回 403。这种情况需要在请求图片时带上Referer头值设成文章所在页面的 URL。如果还是不行说明图片走了 CDN 鉴权只能放弃下载在 Markdown 里保留原始链接。4.4 现象抓了几百篇后程序卡死或报内存错误原因是所有文章内容都堆在内存里最后才统一写文件。一千篇文章的 HTML 加起来可能几百兆加上解析后的对象内存占用很容易过 G。解决办法改成流式处理抓一篇写一篇写完就释放。状态文件也是每篇更新一次。这样内存占用始终维持在一篇文章的量级跑一万篇也不会崩。另外图片下载也建议用流式写入不要先读进内存再存盘。4.5 现象目标站点返回 403 或验证码页面原因是请求频率太高被识别为爬虫。除了调大delay还可以轮换User-Agent或者在请求头里补上Accept-Language、Referer这些浏览器常带的字段。如果目标站点有 Cloudflare 之类的防护纯请求模拟基本过不去会一直返回挑战页面。这种情况要么换无头浏览器方案要么放弃这个站点。我的习惯是先用curl手动请求一次看返回的是正常 HTML 还是挑战页提前判断可行性别写半天代码才发现根本抓不了。5. 进阶技巧把归档结果接入本地搜索与定期同步5.1 用 ripgrep 给归档目录建全文索引抓下来的 Markdown 文件按日期-标题.md命名放在archive/目录下。要找某个关键词出现在哪些文章里用 ripgrep 比任何笔记软件的搜索都快。# 在当前目录及子目录的所有 md 文件中搜索关键词显示文件名和行号 rg -n --type md 增量同步 ./archive # 只列出包含关键词的文件名不显示具体行 rg -l --type md 断点续传 ./archive # 统计每个文件里关键词出现的次数 rg -c --type md 选择器 ./archive-n显示行号-l只列文件名-c统计次数。这三个组合基本覆盖日常检索需求。如果归档量很大可以配合fzf做交互式筛选或者把archive/目录加到编辑器的工作区里用编辑器自带的全局搜索。5.2 用 cron 或计划任务做每日增量同步增量同步跑一次通常几分钟适合放进定时任务。Linux 上用 cronWindows 上用任务计划程序。# 编辑当前用户的 crontab crontab -e # 每天凌晨 3 点跑一次增量同步日志追加到 sync.log 0 3 * * * cd /path/to/blog_archiver /path/to/venv/bin/python blog_archiver.py --config config/settings.yaml sync.log 21关键点是命令里要用虚拟环境的 Python 绝对路径因为 cron 的环境变量和登录 shell 不一样直接写python可能找不到。日志重定向 sync.log 21把标准输出和错误都记下来出问题时能回溯。跑一段时间后可以定期检查sync.log里有没有连续的失败记录如果有说明目标站点可能改版了需要更新解析配置。5.3 验证归档完整性的两个检查点第一个检查点是文章数量。抓完后统计archive/下的文件数和列表页显示的总数对比。如果差得多说明有些文章没抓到可能是列表页翻页逻辑有问题或者某些文章被置顶导致重复计数。第二个检查点是正文长度分布。写个小脚本统计每篇 Markdown 的字符数正常文章一般几百到几千字。如果出现大量几十字的文件说明这些文章的正文选择器没匹配到需要单独排查。import os def check_archive(archive_dir: str): files [f for f in os.listdir(archive_dir) if f.endswith(.md)] short_files [] for f in files: path os.path.join(archive_dir, f) size os.path.getsize(path) if size 200: # 小于 200 字节的基本是空壳 short_files.append((f, size)) print(f总文件数: {len(files)}) print(f疑似空壳文件: {len(short_files)}) for name, size in short_files[:10]: print(f {name} ({size} 字节)) check_archive(./archive)这个检查脚本我每次批量抓取后都会跑一遍。小于 200 字节的文件基本可以确定是解析失败打开看一眼就能定位是哪个选择器出了问题。从那以后我养成了习惯任何批量抓取任务跑完先看空壳文件比例超过 5% 就停下来查配置不带着问题往下跑。希望这套流程能帮到你把散落的博文真正变成自己能掌控的本地知识库。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑