
简介CSDNBlogDownloader 是一款面向博客备份与内容归档场景的下载工具基于 Java 开发适合需要批量保存 CSDN 用户文章、单篇文章或分类下资源的个人博主与内容运营者。包内同时提供 V1.0 与 V2.0 两个版本V2.0 在基础用户名下载之外新增文章链接与分类链接下载并支持配置导入、保存实用性更强。资源共 176 个文件包含 Java 源码、HTML 界面页面、打包好的 jar/exe 可执行程序并附有配置文件、说明文档与图片资源压缩包整体约 8.47MB结构较为精简便于直接运行或二次开发学习。目前已有 4583 人学习/下载说明该工具在博客迁移与离线阅读场景中有一定参考价值。通过源码可了解 GUI 搭建、HTML 解析、多线程下载与配置持久化等常见技术点对 Java 桌面应用开发者或有批量抓取需求的读者是一份不错的实战样例。 我最早做这个CSDN博客下载器纯粹是被一次次手动保存逼急了。那阵子我在整理自己的技术知识库光CSDN上值得留档的文章就有上百篇每篇都要打开浏览器、按打印、选另存为PDF碰到页面里有好几段代码的还要反复调整分页折腾一晚上也存不了几篇。更麻烦的是有些文章只在列表页能看到摘要正文要点进详情页还有一部分图片默认懒加载直接另存出来的HTML打开全是裂图。一气之下我就决定自己写一个小工具输入一个CSDN用户名或者文章链接剩下的事情自动完成抓正文、清洗排版、批量导出PDF。这个工具就是标题里说的CSDNBlogDownloader。如果你也经常在CSDN上刷技术文章想把自己收藏夹里的内容做离线备份或者想给团队沉淀一份可检索的资料库这篇文章应该能帮上忙。我会把整个下载器的设计思路、核心实现、PDF导出方案以及我实际踩过的坑都拆开讲清楚。代码部分不追求花哨够用、能改、不依赖一堆重型框架这是我自己用下来的主要标准。1. 为什么放着“另存为”不用非要自己写一个下载器1.1 浏览器另存和在线转换的三大痛点先说浏览器自带功能。你在CSDN文章页按CtrlS保存下来的其实是一个“带着各种广告位和推荐模块”的完整页面正文只占中间一小块。如果只是偶尔存一篇勉强能看一旦批量保存问题就暴露了。第一个痛点是页面里的图片几乎都是懒加载源码里只有>div idarticle_content在CSDN当前的前端结构里正文主体都在这个id下面里面有段落、标题、图片、代码块、表格偶尔还有提示块。我选择直接用BeautifulSoup定位这个容器然后把它内部的HTML取出来再做清洗和转换。清洗规则很重要。正文里经常出现一些干扰项比如文章末尾的“点赞”“收藏”按钮区域还有嵌入的公告卡片。我的做法是先移除article_content下面的div.hide-article-box、.more-toolbox这类专属模块再把script、style标签全部剔除剩下的才是干净正文。如果页面结构换了只要定位逻辑稍微改一下工具还能继续用。这也是我坚持用“语义化容器”而不是“正则硬匹配”的原因正则今天能跑明天页面一改就崩。3. 动手实现用Python快速搭一个能跑的版本3.1 环境准备与请求头设置我用的Python 3.10依赖只有四个requests、beautifulsoup4、lxml、html2text。PDF导出单独用wkhtmltopdf的命令行工具Python端只是调用它。项目的目录结构很简单csdn-blog-downloader/ ├── downloader.py ├── config.ini └── output/ ├── markdown/ └── pdf/请求头是一个需要认真对待的东西。CSDN对User-Agent的校验不算严但直接用Python默认UA访问次数多了容易被限制。我从自己浏览器的请求里复制了一份完整的UA同时在requests里设置了超时和重试。核心代码如下import requests from bs4 import BeautifulSoup HEADERS { User-Agent: ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 ), Referer: https://blog.csdn.net/, } session requests.Session() session.headers.update(HEADERS) def fetch_html(url, timeout10, retries3): for attempt in range(retries): try: resp session.get(url, timeouttimeout) if resp.status_code 200: resp.encoding resp.apparent_encoding or utf-8 return resp.text except requests.RequestException: if attempt retries - 1: raise return Noneresp.encoding这一行是必须的CSDN页面虽然标注了UTF-8但某些历史文章在传输时会被截断或附加上其他编码声明不修正解析出来会是一堆乱码。3.2 抓列表、解析正文、清洗内容的核心代码列表页解析很简单把所有a[href*/article/details/]的链接提取出来用正则取数字ID。然后进入正文页定位article_content把里面的内容清洗干净再用html2text转成Markdown。我实际跑通的最小代码如下import re from html2text import HTML2Text ID_PATTERN re.compile(r/article/details/(\d)) def get_article_ids(page_url): html fetch_html(page_url) soup BeautifulSoup(html, lxml) ids set() for a in soup.select(a[href*/article/details/]): m ID_PATTERN.search(a[href]) if m: ids.add(m.group(1)) return ids def get_article_markdown(article_url): html fetch_html(article_url) soup BeautifulSoup(html, lxml) content soup.select_one(#article_content) if not content: return None for tag in content.select(script, style, .hide-article-box): tag.decompose() h HTML2Text() h.body_width 0 h.ignore_links False h.protect_links True markdown_text h.handle(str(content)) return markdown_text.strip()HTML2Text的参数里body_width 0表示不主动换行防止代码块里的行被拦腰切断protect_links True可以保留链接地址避免导出PDF时链接变文字。3.3 图片处理懒加载和防盗链一起解决图片是CSDN文章里最容易出问题的地方。CSDN的图片地址通常长这样img srchttps://img-blog.csdnimg.cn/xxx.png>import subprocess import tempfile import pathlib def html_to_pdf(html_text, output_pdf, css_textNone): workdir tempfile.mkdtemp(prefixcsdn_pdf_) html_path pathlib.Path(workdir) / article.html html_path.write_text(html_text, encodingutf-8) cmd [ wkhtmltopdf, --encoding, utf-8, --page-size, A4, --margin-top, 12mm, --margin-bottom, 12mm, --margin-left, 14mm, --margin-right, 14mm, --footer-center, [page]/[topage], str(html_path), output_pdf, ] if css_text: css_path pathlib.Path(workdir) / style.css css_path.write_text(css_text, encodingutf-8) cmd.insert(-2, f--user-style-sheet{css_path}) subprocess.run(cmd, checkTrue)实际用的时候我会在CSS里给pre块加浅灰背景和边框给code设置等宽字体再给表格加细边框。这样导出的PDF看起来清爽代码也压缩在一页以内不至于因为默认样式导致代码长时间占据整页空白。5. 实战中踩过的坑与对应解法5.1 需要登录才能看全文的文章怎么处理CSDN有一部分文章要求登录后才能阅读全文。文章页面在未登录状态下只给开头部分正文后面被遮挡。我的下载器在设计时就考虑到了这一点如果页面里出现了“登录后查看更多”的提示就说明当前会话没有权限。此时可以把自己浏览器里的Cookie复制到配置文件里让requests.Session带着Cookie去请求。COOKIE your_cookie_string session.headers.update({Cookie: COOKIE})这里有一个非常容易忽略的点不要直接复制整个Cookie字段到代码里写死因为CSDN的会话Cookie会过期。我一般把Cookie放在config.ini里面过期后只需要替换一次。需要特别说明的是这个方法只适用于下载你自己账号有权限访问的文章或者公开的免费文章不要去碰付费专栏的解锁逻辑版权和合规的边界必须守住。5.2 频繁请求触发风控的应对刚开始写这个下载器的时候我为了测试一口气抓了上百篇文章结果请求到第50篇左右CSDN返回了一个安全验证页面里面是一段JavaScript动态生成的内容和正常文章页完全不同。这个问题本质上是因为请求频率太高。我的解决办法是两条腿走路。第一在每篇文章请求之间加随机延时比如time.sleep(random.uniform(1.5, 3.5))列表页之间的延时可以稍长一些。第二如果某次请求返回的HTML里检测不到article_content就暂停30秒再重试连续失败三次就放弃这篇并记录日志而不是无限重试。实测下来这个策略能让下载器稳定地跑完一个大部分用户的所有文章不会触发风控。5.3 代码块和特殊字符在导出时被打乱的问题代码块是CSDN导出PDF时最容易翻车的地方。我遇到过三种情况。第一种是代码里的尖括号、、这类字符被HTML实体转换直接看Markdown源文件没问题但转成PDF时容易被双重转义导致代码看起来多了很多amp;。解决方法是清洗HTML时不要对pre和code内部的文本做额外处理交给html2text时它会自动处理实体。第二种是长代码行在PDF里被折行缩进全乱我通常给PDF的pre块设置white-space: pre-wrap; word-break: break-all;虽然会打破某些严格的分行习惯但至少代码不会溢出页面。第三种是代码里包含反引号直接转成Markdown时会让代码块提前结束我通过html2text的protect_links和自定义转义逻辑规避。还有一个小坑是文件名。文章标题里经常包含/、:、?这些在Windows下不合法的字符我保存文件时会先把标题替换一遍否则脚本会直接报错。这个方法虽然简单但在批量下载时能省下很多调试时间。6. 把脚本变成顺手的小工具增量下载与命令行封装6.1 增量更新避免重复下载如果只是下载一遍脚本写完就能收工。但我的收藏文章会持续更新同一个博主过两周又发了新文章这时候重新全量下载就太浪费了。我在工具里加了一个“已下载ID”记录机制每次下载成功之后把文章ID追加到本地archive.json文件里。下次运行的时候先读取这个文件列表页里已经存在的ID直接跳过。import json def load_done(patharchive.json): try: with open(path, r, encodingutf-8) as f: return set(json.load(f)) except FileNotFoundError: return set() def save_done(done, patharchive.json): with open(path, w, encodingutf-8) as f: json.dump(sorted(done), f, ensure_asciiFalse, indent2)这个增量逻辑让我可以用一个系统定时任务每周自动跑一次把关注博主的新文章同步到本地做到知识库的持续更新。6.2 命令行封装与日常使用模板为了让下载器真正配合日常使用我在外面套了一层argparse命令行入口。支持三个参数--url指定用户主页或文章链接--format选择md、pdf还是all--output指定输出目录。这样我可以在终端里直接执行python csdn_downloader.py --url https://blog.csdn.net/username --format all --output ./output用起来就和我们平常用的下载工具一样输入地址剩下的交给脚本。我甚至还加了一个--since参数只下载最近30天发布的文章配合自己的RSS阅读习惯把CSDN关注列表变成了一条可控的信息流。整套工具做完之后我最大的体感是离线知识库终于不再依赖在线平台的存续了。技术文章这种东西今天在搜索结果里排第一明天可能就404与其每次临时搜缓存不如用这个下载器趁早备份。如果你也想给自己的CSDN收藏做一次“搬家”完全可以直接照着这套思路抄作业先从抓一篇文章开始再慢慢扩展成自己的批量备份工具过程中遇到任何页面结构调整改改选择器就能继续跑。本文还有配套的精品资源点击获取