简介: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文章页按Ctrl+S,保存下来的其实是一个“带着各种广告位和推荐模块”的完整页面,正文只占中间一小块。如果只是偶尔存一篇,勉强能看;一旦批量保存,问题就暴露了。第一个痛点是页面里的图片几乎都是懒加载,源码里只有><div id="article_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, timeout=10, retries=3): for attempt in range(retries): try: resp = session.get(url, timeout=timeout) 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 src="https://img-blog.csdnimg.cn/xxx.png">import subprocess import tempfile import pathlib def html_to_pdf(html_text, output_pdf, css_text=None): workdir = tempfile.mkdtemp(prefix="csdn_pdf_") html_path = pathlib.Path(workdir) / "article.html" html_path.write_text(html_text, encoding="utf-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, encoding="utf-8") cmd.insert(-2, f"--user-style-sheet={css_path}") subprocess.run(cmd, check=True)实际用的时候,我会在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时容易被双重转义,导致代码看起来多了很多&。解决方法是清洗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(path="archive.json"): try: with open(path, "r", encoding="utf-8") as f: return set(json.load(f)) except FileNotFoundError: return set() def save_done(done, path="archive.json"): with open(path, "w", encoding="utf-8") as f: json.dump(sorted(done), f, ensure_ascii=False, indent=2)这个增量逻辑让我可以用一个系统定时任务,每周自动跑一次,把关注博主的新文章同步到本地,做到知识库的持续更新。
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收藏做一次“搬家”,完全可以直接照着这套思路抄作业,先从抓一篇文章开始,再慢慢扩展成自己的批量备份工具,过程中遇到任何页面结构调整,改改选择器就能继续跑。
本文还有配套的精品资源,点击获取