我写了一个 CSDN Markdown 图片助手:告别本地图片逐张上传

一、为什么要做这个工具?

平时写技术博客,我比较习惯使用 Typora 编写 Markdown。

这种方式有一个很大的优点:可以在本地管理文章,图片也可以放在项目目录下,通过相对路径引用。

例如:

markdown 复制代码
# Spring Boot 入门

这是我的第一篇文章。

![项目架构图](./images/architecture.png)

![运行效果](./images/result.png)

但是,当我们准备把文章发布到 CSDN 时,就会遇到一个比较麻烦的问题:

Markdown 正文可以直接复制过去,但是本地图片路径并不能直接在 CSDN 上正常显示。

传统的处理方式通常是:

  1. 打开 CSDN 编辑器。
  2. 复制 Markdown 正文。
  3. 找到文章中的本地图片。
  4. 逐张上传图片。
  5. 获取图片地址。
  6. 手动替换 Markdown 中的图片路径。

如果一篇文章有几十张图片,这个过程就会非常繁琐。

所以我开发了一个小工具:CSDN Markdown 图片助手(CSDN-MD-Helper)。

它的目标很简单:

把本地 Markdown 文章导入 CSDN,并自动完成本地图片上传和链接替换。

二、工具实现了哪些功能?

目前主要实现了以下功能:

功能 说明
Markdown 导入 将本地 Markdown 正文导入 CSDN
图片自动上传 自动上传文章中的本地图片
图片链接替换 将本地路径替换为 CSDN 图片地址
图片去重 同一个本地文件被多次引用时只上传一次
断点续处理 上传失败后可以继续处理剩余图片
正文备份 处理前可以备份编辑器中的原始正文
正文安全保护 检测用户操作和正文变化,避免覆盖修改
处理日志 实时展示图片上传进度和处理结果

图片识别方面,支持:

  • 普通 Markdown 图片:![描述](图片路径)
  • HTML 图片:<img src="图片路径">
  • Markdown 引用式图片。
  • 包含中文、空格和括号的路径。
  • 相对路径、绝对路径以及本地 file: 地址。

另外,代码块、行内代码中的图片示例不会被当作真实图片上传,远程图片和 data: 图片也会跳过。

三、整体架构设计

项目使用 Python 开发,是一个 Windows 桌面应用。

主要分为三个部分:

  1. app.py:负责桌面界面、用户交互、任务调度。
  2. csdn_browser.py:负责浏览器管理、CSDN 编辑器操作、图片上传和正文处理。
  3. md_images.py:负责 Markdown 图片解析、本地路径识别以及图片链接替换。

整体架构如下:
#mermaid-svg-gczxGSjz0eIcQYQD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gczxGSjz0eIcQYQD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gczxGSjz0eIcQYQD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gczxGSjz0eIcQYQD .error-icon{fill:#552222;}#mermaid-svg-gczxGSjz0eIcQYQD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gczxGSjz0eIcQYQD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gczxGSjz0eIcQYQD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gczxGSjz0eIcQYQD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gczxGSjz0eIcQYQD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gczxGSjz0eIcQYQD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gczxGSjz0eIcQYQD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gczxGSjz0eIcQYQD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gczxGSjz0eIcQYQD .marker.cross{stroke:#333333;}#mermaid-svg-gczxGSjz0eIcQYQD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gczxGSjz0eIcQYQD p{margin:0;}#mermaid-svg-gczxGSjz0eIcQYQD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gczxGSjz0eIcQYQD .cluster-label text{fill:#333;}#mermaid-svg-gczxGSjz0eIcQYQD .cluster-label span{color:#333;}#mermaid-svg-gczxGSjz0eIcQYQD .cluster-label span p{background-color:transparent;}#mermaid-svg-gczxGSjz0eIcQYQD .label text,#mermaid-svg-gczxGSjz0eIcQYQD span{fill:#333;color:#333;}#mermaid-svg-gczxGSjz0eIcQYQD .node rect,#mermaid-svg-gczxGSjz0eIcQYQD .node circle,#mermaid-svg-gczxGSjz0eIcQYQD .node ellipse,#mermaid-svg-gczxGSjz0eIcQYQD .node polygon,#mermaid-svg-gczxGSjz0eIcQYQD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gczxGSjz0eIcQYQD .rough-node .label text,#mermaid-svg-gczxGSjz0eIcQYQD .node .label text,#mermaid-svg-gczxGSjz0eIcQYQD .image-shape .label,#mermaid-svg-gczxGSjz0eIcQYQD .icon-shape .label{text-anchor:middle;}#mermaid-svg-gczxGSjz0eIcQYQD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gczxGSjz0eIcQYQD .rough-node .label,#mermaid-svg-gczxGSjz0eIcQYQD .node .label,#mermaid-svg-gczxGSjz0eIcQYQD .image-shape .label,#mermaid-svg-gczxGSjz0eIcQYQD .icon-shape .label{text-align:center;}#mermaid-svg-gczxGSjz0eIcQYQD .node.clickable{cursor:pointer;}#mermaid-svg-gczxGSjz0eIcQYQD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gczxGSjz0eIcQYQD .arrowheadPath{fill:#333333;}#mermaid-svg-gczxGSjz0eIcQYQD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gczxGSjz0eIcQYQD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gczxGSjz0eIcQYQD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gczxGSjz0eIcQYQD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gczxGSjz0eIcQYQD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gczxGSjz0eIcQYQD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gczxGSjz0eIcQYQD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gczxGSjz0eIcQYQD .cluster text{fill:#333;}#mermaid-svg-gczxGSjz0eIcQYQD .cluster span{color:#333;}#mermaid-svg-gczxGSjz0eIcQYQD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gczxGSjz0eIcQYQD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gczxGSjz0eIcQYQD rect.text{fill:none;stroke-width:0;}#mermaid-svg-gczxGSjz0eIcQYQD .icon-shape,#mermaid-svg-gczxGSjz0eIcQYQD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gczxGSjz0eIcQYQD .icon-shape p,#mermaid-svg-gczxGSjz0eIcQYQD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gczxGSjz0eIcQYQD .icon-shape .label rect,#mermaid-svg-gczxGSjz0eIcQYQD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gczxGSjz0eIcQYQD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gczxGSjz0eIcQYQD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gczxGSjz0eIcQYQD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 任务与事件
Tkinter 桌面界面
BrowserWorker 后台线程
Queue 消息队列
csdn_browser.py
md_images.py
Playwright
Edge / Chrome
CSDN Markdown 编辑器

这里有一个值得注意的设计:

浏览器操作没有直接放在 Tkinter 主线程中执行,而是交给 BrowserWorker 后台线程处理。

原因是浏览器操作涉及页面加载、网络请求、图片上传等耗时任务。如果直接在 GUI 主线程执行,界面很容易出现卡顿甚至无响应。

因此,我使用两个 Queue:

  • commands:GUI 向后台线程发送任务。
  • events:后台线程向 GUI 发送进度、日志和处理结果。

同时,所有 Playwright 操作都由 BrowserWorker 所在线程执行,避免多个线程同时操作浏览器对象。

四、核心实现:如何自动上传本地图片?

这是整个项目最核心的部分。

我没有自己重新实现 CSDN 的图片上传协议,而是利用 Playwright 操作浏览器,让 CSDN 官方 Markdown 编辑器完成图片上传。

整体流程如下:
#mermaid-svg-POm93rmkaDGL6EHS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-POm93rmkaDGL6EHS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-POm93rmkaDGL6EHS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-POm93rmkaDGL6EHS .error-icon{fill:#552222;}#mermaid-svg-POm93rmkaDGL6EHS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-POm93rmkaDGL6EHS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-POm93rmkaDGL6EHS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-POm93rmkaDGL6EHS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-POm93rmkaDGL6EHS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-POm93rmkaDGL6EHS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-POm93rmkaDGL6EHS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-POm93rmkaDGL6EHS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-POm93rmkaDGL6EHS .marker.cross{stroke:#333333;}#mermaid-svg-POm93rmkaDGL6EHS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-POm93rmkaDGL6EHS p{margin:0;}#mermaid-svg-POm93rmkaDGL6EHS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-POm93rmkaDGL6EHS .cluster-label text{fill:#333;}#mermaid-svg-POm93rmkaDGL6EHS .cluster-label span{color:#333;}#mermaid-svg-POm93rmkaDGL6EHS .cluster-label span p{background-color:transparent;}#mermaid-svg-POm93rmkaDGL6EHS .label text,#mermaid-svg-POm93rmkaDGL6EHS span{fill:#333;color:#333;}#mermaid-svg-POm93rmkaDGL6EHS .node rect,#mermaid-svg-POm93rmkaDGL6EHS .node circle,#mermaid-svg-POm93rmkaDGL6EHS .node ellipse,#mermaid-svg-POm93rmkaDGL6EHS .node polygon,#mermaid-svg-POm93rmkaDGL6EHS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-POm93rmkaDGL6EHS .rough-node .label text,#mermaid-svg-POm93rmkaDGL6EHS .node .label text,#mermaid-svg-POm93rmkaDGL6EHS .image-shape .label,#mermaid-svg-POm93rmkaDGL6EHS .icon-shape .label{text-anchor:middle;}#mermaid-svg-POm93rmkaDGL6EHS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-POm93rmkaDGL6EHS .rough-node .label,#mermaid-svg-POm93rmkaDGL6EHS .node .label,#mermaid-svg-POm93rmkaDGL6EHS .image-shape .label,#mermaid-svg-POm93rmkaDGL6EHS .icon-shape .label{text-align:center;}#mermaid-svg-POm93rmkaDGL6EHS .node.clickable{cursor:pointer;}#mermaid-svg-POm93rmkaDGL6EHS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-POm93rmkaDGL6EHS .arrowheadPath{fill:#333333;}#mermaid-svg-POm93rmkaDGL6EHS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-POm93rmkaDGL6EHS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-POm93rmkaDGL6EHS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-POm93rmkaDGL6EHS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-POm93rmkaDGL6EHS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-POm93rmkaDGL6EHS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-POm93rmkaDGL6EHS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-POm93rmkaDGL6EHS .cluster text{fill:#333;}#mermaid-svg-POm93rmkaDGL6EHS .cluster span{color:#333;}#mermaid-svg-POm93rmkaDGL6EHS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-POm93rmkaDGL6EHS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-POm93rmkaDGL6EHS rect.text{fill:none;stroke-width:0;}#mermaid-svg-POm93rmkaDGL6EHS .icon-shape,#mermaid-svg-POm93rmkaDGL6EHS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-POm93rmkaDGL6EHS .icon-shape p,#mermaid-svg-POm93rmkaDGL6EHS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-POm93rmkaDGL6EHS .icon-shape .label rect,#mermaid-svg-POm93rmkaDGL6EHS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-POm93rmkaDGL6EHS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-POm93rmkaDGL6EHS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-POm93rmkaDGL6EHS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
读取 Markdown 正文
解析图片引用
解析本地路径并检查文件
临时保护未上传的图片标记
粘贴一张本地图片
CSDN 编辑器触发图片上传
监听网络响应并提取 imageUrl
校验图片地址
替换正文中的图片路径
还有未处理的图片?
完成,用户检查文章

1. 首先解析 Markdown 图片

项目通过 md_images.py 识别文章中的图片引用。

例如:

markdown 复制代码
![项目截图](./images/test.png)

解析后会得到图片引用信息,包括:

  • 图片在原始 Markdown 中的位置。
  • 图片原始路径。
  • 图片描述。
  • 图片引用类型。

为什么要记录原文位置?

因为后面可能需要替换多张图片。如果每替换一次就直接修改字符串,前面内容长度发生变化后,后续图片的位置也会发生变化。

所以这里采用记录原始文本位置、最后根据原文和替换结果重新构建正文的方式,避免位置偏移问题。

2. 处理相对路径

假设文章目录结构如下:

text 复制代码
MyBlog/
├── article.md
└── images/
    ├── test.png
    └── result.png

Markdown 中引用:

markdown 复制代码
![测试图片](./images/test.png)

工具会以 article.md 所在目录为基准解析图片路径,而不是以程序启动目录为基准。

这样无论从哪个目录启动程序,都可以正确找到图片。

3. 为什么要临时保护图片标记?

这里是一个比较有意思的细节。

如果直接把下面的正文写入 CSDN:

markdown 复制代码
![测试图片](./images/test.png)

CSDN 编辑器可能会提前处理本地图片路径,将其转换为图片加载失败的占位内容。

这样后续就很难再从原始正文中找到正确的本地路径。

因此,我设计了一个临时保护机制:

在尚未上传的图片标记中插入不可见占位字符,让编辑器暂时不要识别这些图片。

等图片上传成功,拿到 CSDN 图片地址之后,再恢复图片标记并替换路径。

这样就可以避免编辑器提前破坏本地图片引用。

4. 利用 Playwright 模拟图片粘贴

这是整个上传流程的关键。

程序首先读取本地图片文件,将图片字节编码为 Base64,然后在浏览器页面中构造:

  • File
  • DataTransfer
  • ClipboardEvent

最后向 CSDN Markdown 编辑区域派发 paste 事件。

核心逻辑可以简化为:

javascript 复制代码
const file = new File(
    [bytes],
    data.name,
    { type: data.mime }
);

const transfer = new DataTransfer();
transfer.items.add(file);

editor.dispatchEvent(
    new ClipboardEvent('paste', {
        clipboardData: transfer,
        bubbles: true,
        cancelable: true
    })
);

这样 CSDN 编辑器就会按照自身的图片粘贴流程处理文件。

5. 监听上传结果并替换链接

图片粘贴之后,程序通过 Playwright 监听页面的网络请求和响应。

获取响应后,需要进行几项检查:

  1. 请求是否属于本轮图片上传。
  2. 响应是否成功。
  3. 响应内容是否符合预期 JSON 结构。
  4. 是否存在 data.imageUrl。
  5. 图片地址是否使用 HTTPS,且属于允许的 CSDN 图片域名。

拿到有效地址之后,还会继续检查该地址是否真正出现在编辑器正文中。

只有确认上传完成并成功插入正文,才会执行替换。

例如:

上传前:

markdown 复制代码
![测试图片](./images/test.png)

上传后:

markdown 复制代码
![测试图片](https://i-blog.csdnimg.cn/xxx.png)

如果同一个本地文件在文章中被引用了多次,程序只上传一次,然后将得到的地址应用到所有对应引用。

五、如何避免处理过程中覆盖用户的文章?

自动化工具不仅要能完成任务,还需要考虑误操作问题。

例如,程序正在上传图片,用户突然手动修改了文章。如果程序继续写入,就有可能覆盖用户刚才的修改。

因此,我专门增加了一套正文安全保护机制。

主要包括:

1. 原正文备份

处理前可以将编辑器原始正文备份到本地:

text 复制代码
%LOCALAPPDATA%\CSDN-MD-Helper\recovery

2. 用户交互检测

通过监听页面的可信键盘、鼠标、粘贴、剪切和拖放事件,判断用户是否在处理期间操作了编辑器。

一旦检测到用户操作,就停止后续处理。

3. 正文快照校验

每次重要写入前后都会读取编辑器正文,与预期内容进行比较。

发现正文发生变化时,不继续覆盖。

4. 写入结果验证

正文写入后,不会直接认为操作成功,而是重新读取正文,等待内容稳定并验证是否与目标一致。

如果写入失败,会尝试恢复上一步正文。

5. 失败后继续处理

假设一篇文章有 10 张图片,前 6 张已经上传成功,第 7 张上传失败。

工具会保留已经完成的图片链接,并尽可能恢复到上一轮正文。

解决图片问题后,可以选择「仅修复图片」,继续处理剩余图片,而不必重新导入整篇文章。

六、桌面应用与工程化处理

除了核心的浏览器自动化,我还处理了一些桌面应用开发中的细节。

  • 使用 Tkinter 构建图形界面。
  • 使用后台线程和 Queue 实现任务调度。
  • 支持 Microsoft Edge 和 Google Chrome。
  • 使用独立的持久化浏览器 Profile 保存登录状态。
  • 处理 Windows DPI 缩放,避免高分辨率屏幕下界面显示异常。
  • 使用 PyInstaller 将程序打包为独立 exe。

项目提供两种运行方式:

源码运行:

powershell 复制代码
py -3 -m venv .venv

.\.venv\Scripts\python.exe -m pip install -r requirements.txt

.\.venv\Scripts\pythonw.exe app.py

打包运行:

双击:

text 复制代码
dist/CSDN-Markdown-Helper-v6.exe

打包程序包含 Python 运行环境和 Playwright 驱动,但电脑仍需安装 Edge 或 Chrome。

七、如何使用?

界面左侧用于选择文档、打开浏览器和选择处理方式;右侧显示进度和处理日志。窗口较窄时会自动改为上下布局,内容超出窗口时可用滚动条或鼠标滚轮查看。

程序启动时按 Windows 的系统 DPI 绘制,并按缩放比例调整默认窗口和图标大小。使用缩放比例不同的多个显示器时,窗口移到另一块屏幕后仍可能被 Windows 缩放;目前不支持跨屏动态调整 DPI。

  1. 选择一篇本地 .md 或 .markdown 文件。保留它与图片之间原有的相对目录关系。
  2. 选择 Edge 或 Chrome,点击"打开 CSDN"。浏览器使用助手自己的独立配置目录,首次请自行登录;不会读取现有浏览器的登录资料。
    浏览器关闭后可以重新打开或选择另一种浏览器。若当前助手浏览器仍打开,切换时请先保存文章并确认关闭。
  3. 在助手打开的浏览器中打开或新建一篇 Markdown 编辑器文章,并关闭助手浏览器中其他 Markdown 编辑标签页。
  4. 回到助手,选择处理方式:
    • 导入所选 Markdown:用所选本地文档替换正文。如果当前已有正文,工具会先询问。
    • 仅修复编辑器当前正文中的本地图片:保留你在 CSDN 正文中已有的文字修改,只替换可识别的本地图片引用。
    • 处理前备份已有正文:默认勾选,可自行取消;例如编辑器中只有 CSDN 示例文字时可以不备份。
  5. 点击"开始处理正文与图片"。界面会显示已处理图片的进度;处理期间不要编辑 CSDN 正文。
  6. 完成后在 CSDN 中检查文章预览,自行保存草稿或发布。

需要注意:工具不会自动点击 CSDN 的保存草稿或发布按钮,最终仍然由用户检查和确认。

八、项目的一些不足

  1. 目前这个工具主要面向 Windows 环境。
  2. 另外,图片上传依赖 CSDN 官方网页编辑器的行为和页面结构。如果 CSDN 更新编辑器DOM、粘贴逻辑或者上传响应格式,可能需要调整对应的自动化代码。
  3. 浏览器登录信息保存在助手自己的独立配置目录中,不会读取用户平时使用的浏览器登录资料。这个目录也不要随意分享。
  4. 用了PyInstaller 的单文件模式。双击时,exe会把运行所需的一些文件解压到系统临时目录,再启动程序;正常退出后,临时文件会被清理。可能会导致启动略慢。

九、项目地址

GitHub: https://github.com/zjyOverQuartzer/CSDN-MD-Helper

核心代码:

  • app.py:桌面界面和后台任务调度。
  • csdn_browser.py:浏览器自动化和文章处理流程。
  • md_images.py:Markdown 图片解析和路径处理。
  • tests/:图片解析、浏览器流程和界面测试。

其中,浏览器流程测试使用本地模拟编辑器和模拟上传服务,不需要连接真实 CSDN 账号。

最后

如果这个工具有什么bug或者好的建议,欢迎在评论区指出或者在github上提出一个issue,或者你也可以提交一个pr。

我还打算适配更多的博客平台,比如公众号,稀土掘金等,已经一键发布到多个平台中的功能,像CSDN 的一键发布还要会员。

相关推荐
刘马想放假3 小时前
CANviz 使用指南|Linux 安装、DBC 解码、实时曲线与 J1939 实战
linux·python·websocket
2601_967987374 小时前
python判断变量名不要调用任何函数实现
python·函数·语法·判断·变量名
卷无止境5 小时前
SALib敏感性分析的python库学习笔记
后端·python
duansamve6 小时前
Python3+Flask+PostgreSQL18+Vue3项目生产环境用docker如何部署?
python·docker·flask
特立独行的猫A6 小时前
少 3 个环节、少 1 次 ABI、少 3500 行代码:cj-tauri 与 Ionic/Capacitor 的鸿蒙桥接机制对比
java·后端·python
特立独行的猫A7 小时前
cj-tauri vs Rust/Tauri 同款播放器实测:编译 21 秒 vs 11 分钟,磁盘 12 MB vs 1.2 GB
java·后端·python
duansamve7 小时前
详解Flask中Session和Cookie
python·flask
威哥爱编程8 小时前
模拟面试智能体开发实战 04:面试聊到哪算哪?用状态机把整场面试编排起来
python·agent·harmonyos
威哥爱编程8 小时前
模拟面试智能体开发实战 03:AI 面试官太客气?人设 Prompt 让它像个真考官
python·agent·harmonyos