用Python打造CSDN博客下载器:从爬虫到PDF导出全解析

用Python打造CSDN博客下载器:从爬虫到PDF导出全解析 简介CSDNBlogDownloader 是一款基于 Java 的 CSDN 博客下载工具面向需要批量备份博客文章、用户主页或分类内容的博主、内容运营及本地归档用户。工具提供 V2.0 与 V1.0 两个版本V2.0 支持通过指定用户名和下载地址抓取用户文章、输入文章链接单篇下载、输入分类链接批量下线分类并支持导入/保存配置与链接V1.0 则提供简单的用户名下载地址的快捷下载方式。压缩包共 176 个文件大小 8.47MB包含 27 个 Java 源码、111 个 HTML 页面、可执行 exe/jar 程序以及 txt/ini/xml 等配置与说明文件便于直接运行或二次开发。目前已有 4583 人学习下载。读者可从中获得完整工具源码、GUI 界面资源、可执行程序以及配置示例既能快速解决 CSDN 内容本地备份需求也可参考其解析与下载逻辑进行个性化扩展。1. 项目概述与核心需求拆解CSDN博客下载器这个项目说白了就是一个能把CSDN站内文章完整抓取下来、保存成离线文件的小工具。做这个工具的初衷很朴素CSDN平台上沉淀了大量技术博客尤其是嵌入式、通信、AI落地这类领域的实战经验帖很多内容在其他平台根本找不到同款。但CSDN的阅读体验一直不算友好广告多、页面重、排版拥挤想要把一篇好文章保存下来慢慢看或者整理成自己的知识库靠浏览器自带的“另存为”根本不好使——保存下来的是整页HTML广告和侧边栏全在里面稍不注意还会因为懒加载漏掉图片。我最初的想法是做一个命令行小工具输入博主的文章列表页URL自动把该博主所有公开文章抓下来统一转成Markdown和PDF两种格式方便本地归档和二次编辑。后来用着用着发现需求比想象中大就稍微扩展了一下支持单篇文章下载、支持按日期范围筛选、支持把多篇文章合并成一个PDF这才有了一个能正经用的版本。如果你属于下面这几类人这个工具大概率对你有用平时大量收藏CSDN技术帖的开发者、需要做技术资料归档的团队、写博客想批量备份自己内容的博主、以及单纯受够了网页排版想离线阅读的人。整个项目用Python实现依赖很少核心就requests、BeautifulSoup、markdownify、weasyprint这四件套跑起来非常轻量。2. 整体设计与技术选型逻辑2.1 为什么不用现成的爬虫框架写这个工具之前我认真对比过Scrapy和requests直连两条路。Scrapy功能强但配置起来太笨重为了下载一个博客文章去维护整个爬虫工程属于杀鸡用牛刀。而且CSDN的文章页结构不算复杂没有复杂的JS渲染大部分内容是服务端直接输出在HTML里的用requests就能拿全。使用requests的好处在于逻辑直白、调试方便。你发一个GET请求拿回来的就是完整的HTML文档把内容提取这一步单独抽出来做后续想换数据源或者改解析规则都很容易。我这个项目里请求模块和解析模块是完全解耦的后来加了一个“下载其他技术社区文章”的适配器结构只是新增一个解析类的事。提示如果目标页面是纯前端渲染的SPArequests就无能为力了那种场景要上Playwright或者Selenium。CSDN不在这个范围内这也是我选requests的底气之一。2.2 整体架构分三层这个项目的代码组织分三层抓取层、解析层、导出层。抓取层负责发HTTP请求、处理分页、管理请求头。CSDN的文章列表有两种形态一种是博主主页的极简列表页里面能拿到所有文章的标题、发布时间、阅读量、链接另一种是文章详情页本身。两类页面的请求参数不太一样但都不需要登录就能访问公开内容。解析层是核心负责从HTML里提取正文信息、标题、标签、发布时间、作者名同时清理掉无关的脚本、样式、广告模块。这里有个细节CSDN文章正文的HTML结构里代码块是用pre包裹的提取时需要保留pre标签否则代码块的缩进和换行会乱。导出层接收解析结果输出Markdown、HTML或者PDF。Markdown走的是markdownify库一个HTML到Markdown的转换器PDF走的是weasyprint它能把HTMLCSS渲染成PDF文件对中文支持也OK。2.3 为什么选Python而不是Node.js理论上用Node.js的Cheerio也能实现同样功能但Python在这个场景下有几个优势文本处理生态成熟markdownify和BeautifulSoup做HTML转Markdown比Node生态的同类库顺手weasyprint渲染PDF的质量比Node的pdfkit或者puppeteer方案稳定字体处理也省心再加上Python写脚本本身就快几千行以内的小工具开发效率不在一个量级。3. 核心细节解析与实操要点3.1 文章列表页的抓取逻辑CSDN博主主页的文章列表地址格式是这样https://blog.csdn.net/{username}这个页面会渲染博主最近发布的文章但默认只加载前几页滚动到页面底部才会触发剩余加载。直接请求这个URL拿到的HTML里第一页的文章数据是完整的后续的数据靠一个异步接口提供https://blog.csdn.net/{username}/article/list/{page}这个接口返回的是HTML片段不是JSON但结构比完整页面干净很多每一篇文章对应一个div classarticle-item-box里面能直接用CSS选择器提取标题、链接、发布时间、阅读量和评论数。实际抓取时我就循环请求这个列表接口直到返回的文章列表为空为止。这里要注意翻页请求需要带一个Referer头指向博主主页URL否则CSDN会拒绝请求返回403。3.2 文章详情页的解析细节进入单篇文章页面后正文内容位于一个div idcontent_views的节点里。我用BeautifulSoup定位到这个节点后先做两轮清理第一轮删除不需要的元素script、style、.hide-article-box这是CSDN的折叠阅读框如果不删掉Markdown导出的文章会缺尾巴这是最大的坑。第二轮是格式标准化把img标签的懒加载属性>pip install requests beautifulsoup4 markdownify weasyprintweasyprint在Linux下需要系统库支持如果运行时报libpango相关的错误要装一下系统依赖apt-get install libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0macOS用户直接用brew install pango就行。Windows上weasyprint的支持稍差如果折腾不明白可以直接用Chrome的无头模式打印PDF效果也不差。4.2 核心代码实现列表抓取下面这段代码是抓取博主全部文章链接的核心实现完整跑通后就已经拿到了元数据库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/120.0.0.0 Safari/537.36, Referer: https://blog.csdn.net/ } def fetch_all_articles(username, max_pages100): articles [] for page in range(1, max_pages 1): url fhttps://blog.csdn.net/{username}/article/list/{page} resp requests.get(url, headersHEADERS, timeout10) if resp.status_code ! 200: print(fPage {page}: HTTP {resp.status_code}) break soup BeautifulSoup(resp.text, html.parser) items soup.select(div.article-item-box) if not items: print(fPage {page}: no article item, stop.) break for item in items: link_tag item.select_one(a) if not link_tag: continue title link_tag.get_text(stripTrue) href link_tag.get(href) # 发布时间在拆分后的div里标题栏后面通常有span date_tag item.select_one(span.date) date date_tag.get_text(stripTrue) if date_tag else articles.append({title: title, url: href, date: date}) print(fPage {page}: collected {len(items)} article(s).) return articles跑一遍这个函数返回的列表里包含了标题、链接、日期三个字段打印出来就形成了一个索引。4.3 核心代码实现正文提取与Markdown转换拿到文章链接后进入解析阶段。这里把核心的提取函数贴出来重点在于清理动作的顺序def parse_article(url): resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 # CSDN页面是utf-8编码但保持显式声明更稳 soup BeautifulSoup(resp.text, html.parser) # 标题 title_tag soup.select_one(h1.title-article) if not title_tag: return None title title_tag.get_text(stripTrue) # 发布时间 time_tag soup.select_one(span.time) pub_time time_tag.get_text(stripTrue) if time_tag else # 正文容器 content soup.select_one(#content_views) if not content: return None # 清理1移除折叠阅读框 for hidden in content.select(.hide-article-box): hidden.decompose() # 清理2移除隐藏的广告、脚本和样式 for tag in content.select(script, style, .article-attach): tag.decompose() # 清理3修复懒加载图片 for img in content.select(img): original img.get(data-original) or img.get(data-src) if original: img[src] original markdown_text markdownify(str(content), heading_styleATX) return { title: title, pub_time: pub_time, markdown: markdown_text, url: url }需要提一下heading_styleATX这个参数markdownify默认生成的是Setext风格标题用和---表示但CSDN文章不少标题是##这种层级结构用ATX风格转出来更干净后续如果要合并成PDF标题层级也更容易映射成带编号的目录。4.4 PDF导出从Markdown到HTML再到PDF到了PDF导出这一步我没有直接把Markdown文本塞给转换器而是先经过一个“Markdown转HTML再渲染成PDF”的中间层。原因是weasyprint接受HTML输入而Markdown直接转PDF容易丢样式。我的做法是用Python标准库markdown把Markdown转成HTML片段然后套上一层CSS模板主要设置字体的衬线体优先级、代码块背景色、表格边框等import markdown from weasyprint import HTML HTML_TEMPLATE !DOCTYPE html html langzh-CN head meta charsetutf-8 style body {{ font-family: Noto Serif CJK SC, Source Han Serif SC, serif; line-height: 1.8; padding: 40px; }} pre {{ background: #f6f8fa; padding: 16px; border-radius: 6px; overflow-x: auto; }} code {{ font-family: JetBrains Mono, Consolas, monospace; font-size: 14px; }} h1, h2, h3 {{ margin-top: 24px; }} table {{ border-collapse: collapse; width: 100%; }} th, td {{ border: 1px solid #ddd; padding: 8px 12px; }} /style /head body{body}/body /html def md_to_pdf(md_text, output_path): html_body markdown.markdown(md_text, extensions[fenced_code, tables, codehilite]) html_doc HTML_TEMPLATE.format(bodyhtml_body) HTML(stringhtml_doc).write_pdf(output_path)代码高亮在离线PDF里是个加分项我加了codehilite扩展配合fenced_code三种反引号包裹的代码块执行了高亮。不过这要求环境中装了Pygments库没装的话这个扩展会静默跳过。4.5 多篇文章合并成一个PDF单篇文章下载没难度真正实用的是批量合并。比如你要下载某个博主的一整个系列教程二十篇文章合成一个PDF带去通勤路上看。实现思路就是先把每篇文章转成HTML片段然后按时间顺序拼到一个HTML文档里每篇文章之间加上hr分隔符和页脚信息最后统一输出PDF。关键点是图片路径如果图片是外链的img-blog.csdnimg.cnPDF里直接在img srchttps://...能渲染但速度慢。更稳妥的做法是先下载图片到本地临时目录再把src改成本地相对路径最后生成PDF时图像就能离线嵌进去。5. 常见问题与排查技巧实录5.1 CSDN返回403错误这是抓取时最常遇到的问题。首次写好脚本去跑大概率会遇到403原因基本是请求头太少被CSDN的WAF识别为爬虫。解决办法是补全User-Agent头和Referer头我上面给的HEADERS配置是按浏览器真实请求复刻的加了之后基本能绕过基础风控。如果还是403检查一下是不是访问频率太高。我踩过一次坑循环抓取时没加延时连续发了几百个请求后来IP被限了断断续续持续了两个小时。这个工具里后来加了一个全局time.sleep控制每抓完一篇文章就睡0.5~1秒实测下来既不触发风控速度也能接受。5.2 下载下来的Markdown里图片全部是空的这个问题原因是懒加载。CSDN正文里的图片标签通常长这样img src...>apt-get install fonts-noto-cjk然后CSS里明确指定font-family使用这个字体。开发调试的时候本地macOS没这个问题但部署到Linux容器里就经常翻车这是我最后悔没早点注意到的坑——环境差异排查成本比想象中高。5.5 触发CSDN的人机验证高频抓取到一定程度CSDN会弹出滑块验证或者验证码。说实话到这个节点就别再硬刚了单纯的爬虫强度已经贴到了网站的利益线。我的处理策略是降速到每篇间隔2~3秒拉长总任务时长同时限制一次性抓取的上下文窗口尽量模拟真人阅读节奏。如果你要抓的文章量大到必须高频请求那就换个思路手动打开网页把所有URL粘贴到一个文本文件里让工具只负责解析和导出不负责批量发现链接。这个模式避免了持续高频请求项目的压力大大降低。6. 项目扩展与后续思路这个工具目前只解决了“把CSDN文章变成离线文件”这一件事但以它为基础可以延伸出不少有意思的方向其一做成一个本地知识库的接入层。抓下来的Markdown文件天然适配Obsidian、Logseq这类双链笔记工具可以直接作为知识库的原料。如果再写一个小脚本把标签、发布时间映射成笔记的front matter那价值又上了一个台阶。其二做定时备份工具。对博主个人来说定期把自己的全部文章备份到本地防止内容被误删或者平台调整导致文章消失是个不可忽视的诉求。把下载逻辑封装成定时任务每周自动跑一次增量备份本地归档整理负担很小。其三多站点迁移。现在整个解析层只是针对CSDN定制的但如果你在自己的服务器上用Hexo或者Hugo搭建了静态博客完全可以把CSDN的文章批量转成Hexo的Post格式导入到自己的站点。对我来说这个需求反而更现实——平台是别人的数据才是自己的。另外我在设计的时候预留了一个“解析器插件”入口每个平台的解析逻辑配置化后续要是想支持掘金、InfoQ或者微信公众号只需要新增一个解析器类。项目的核心价值不在代码量而在它把“网页内容本地化”这个通用需求做到了闭环这也是我在周末花几个小时维护这个工具的动力所在。本文还有配套的精品资源点击获取