一行命令发布 CSDN 博客:Claude Code + Playwright 自动化实战指南

前言

写技术文章的流程通常是这样的:在 IDE 里写完 Markdown,切到浏览器,打开 CSDN 创作中心,粘贴标题,粘贴正文,选标签,选分类,点发布。

一篇文章还好,十篇呢?二十篇呢?

这篇文章记录了我如何用 Playwright 浏览器自动化 把这个机械流程变成一行命令。核心代码不到 300 行,之后每次发文章就是一条命令的事。

你会学到什么

读完这篇文章,你将掌握:

  1. 如何用 Playwright 自动化 CSDN 的 Markdown 编辑器
  2. 如何处理 CSDN 特有的页面结构(隐藏标题栏、左右栏标签选择、遮罩层拦截)
  3. 如何将自动化脚本封装成 Claude Code Skill,实现 AI 驱动的博客发布

技术方案概览

整体架构非常简单:

CLI 命令 → Python 脚本 → Playwright → Chrome 浏览器 → CSDN 编辑器

没有用 CSDN 的 API(因为没有公开 API),也没有写爬虫,而是直接操控浏览器——和你手动操作一模一样,只是换成了代码。

为什么选 Playwright 而不是 Selenium

对比项 Playwright Selenium
安装 pip install playwright 一行搞定 需要额外下载 ChromeDriver
速度 快,CDP 协议直连 相对慢
API 设计 locator 链式调用,更优雅 find_element 较繁琐
异步支持 原生 async 需要额外封装
等待机制 内置智能等待 需要手动 WebDriverWait

关键设计决策

  1. Cookie 复用:首次手动登录后保存 cookies,后续自动加载,避免每次都要扫码
  2. 多重选择器 fallback:对每个页面元素准备多个 CSS 选择器,CSDN 改版后容错性更强
  3. 鼠标坐标点击:CSDN 的 Vue 框架不响应 JS click(),必须用真实鼠标事件
  4. 遮罩层移除:CSDN 弹窗的 mark-mask-box-div 会拦截点击,直接从 DOM 移除

第一步:环境搭建

安装依赖

pip install playwright
playwright install chromium

首次登录

from playwright.sync_api import sync_playwright
from pathlib import Path

COOKIE_FILE = Path.home() / ".csdn_cookies.json"

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    context = browser.new_context()
    page = context.new_page()
    page.goto("https://passport.csdn.net/login")

    # 手动扫码登录,登录成功后保存 cookies
    page.wait_for_url("**/my.csdn.net/**", timeout=120_000)
    context.storage_state(path=str(COOKIE_FILE))
    browser.close()

之后每次运行都加载这个 cookie 文件,无需再次登录。

第二步:编辑器页面操作

切换到 Markdown 模式

CSDN 编辑器默认打开是"比对"模式,需要先点击 “Markdown” 标签切换:

page.locator("button.nav-tab-btn", has_text="Markdown").first.click()

填写标题(隐藏 input 问题)

这是踩的第一个坑。CSDN 在 Markdown 模式下,标题的 input 元素是 hidden 的:

<input class="article-bar__title article-bar__title--input"
       placeholder="请输入文章标题"
       aria-hidden="true" />

页面上实际显示的是一个 div.article-bar__title-display,点击它之后,隐藏的 input 才会变成可见:

# 点击显示区域,激活隐藏的 input
page.locator(".article-bar__title-display").click()
time.sleep(0.5)

# 等待 input 可见后填入标题
title_input = page.locator("input.article-bar__title--input")
title_input.wait_for(state="visible", timeout=3000)
title_input.fill("")
title_input.type(title, delay=30)

注入 Markdown 正文

这是第二个坑。编辑器是 contenteditablepre 元素:

<pre class="editor__inner markdown-highlighting" contenteditable="true">

直接用 innerText 注入会丢失 Markdown 格式(代码块、标题层级等全没了)。正确做法是用 剪贴板粘贴

# 先点击编辑器聚焦
editor.click()
time.sleep(0.3)

# 全选原有内容并删除
page.keyboard.press("Control+a")
time.sleep(0.2)

# 用 ClipboardEvent 粘贴 Markdown(保留格式)
page.evaluate("""(text) => {
    const dt = new DataTransfer();
    dt.setData('text/plain', text);
    const el = document.querySelector('pre.editor__inner');
    el.dispatchEvent(new ClipboardEvent('paste', {
        clipboardData: dt, bubbles: true, cancelable: true
    }));
}""", markdown_content)

这样粘贴进去的内容,CSDN 编辑器会自动解析 Markdown 语法,代码块、表格、引用等格式全部保留。

第三步:发布弹窗操作

打开发布弹窗

点击编辑器右上角的"发布文章"按钮,弹出发布设置弹窗:

page.locator("button.btn.btn-publish").first.click()
time.sleep(3)

标签选择(左右栏结构)

这是最复杂的部分。CSDN 的标签选择是 左右两栏 结构:

┌──────────────┬────────────────────────┐
│  推荐         │  python  django  pygame │
│  Python  ←   │  tornado  flask        │
│  Java         │  fastapi  scrapy       │
│  编程语言     │  numpy  pandas         │
│  ...         │  ...                   │
└──────────────┴────────────────────────┘
  • 左栏 (ul.mark_add_tag_left > li):标签分类,如 Python、Java、人工智能
  • 右栏 (div.mark_add_tag_right > span.el-tag):该分类下的具体标签

选择流程:

# 1. 点击左侧 "Python" 分类
py_category = page.evaluate("""() => {
    const lis = document.querySelectorAll('.mark_add_tag_left li');
    for (const li of lis) {
        if (li.innerText.trim() === 'Python') {
            const r = li.getBoundingClientRect();
            return {x: r.x + r.width/2, y: r.y + r.height/2};
        }
    }
    return null;
}""")
page.mouse.click(py_category["x"], py_category["y"])
time.sleep(1)

# 2. 点击右侧 "python" 标签
tag = page.evaluate("""() => {
    const tags = document.querySelectorAll('.mark_add_tag_right span.el-tag');
    for (const t of tags) {
        if (t.innerText.trim() === 'python') {
            const r = t.getBoundingClientRect();
            return {x: r.x + r.width/2, y: r.y + r.height/2};
        }
    }
    return null;
}""")
page.mouse.click(tag["x"], tag["y"])

点击发布按钮(遮罩层拦截问题)

这是最大的坑。CSDN 弹窗有一个 mark-mask-box-div 遮罩层,会拦截所有鼠标事件。不管是 Playwright 的 locator.click() 还是 JS 的 click(),都会被挡掉。

最终解决方案:

# 1. 从 DOM 彻底移除遮罩层
page.evaluate("""() => {
    document.querySelectorAll('.mark-mask-box-div').forEach(m => m.remove());
}""")

# 2. 滚动发布按钮到视口中心(避免按钮在视口边缘导致点击无效)
page.evaluate("""() => {
    const btn = document.querySelector('button.btn-b-red.ml16');
    if (btn) btn.scrollIntoView({block: 'center'});
}""")

# 3. 用鼠标坐标点击(不用 locator.click(),Vue 不响应)
pub_rect = page.evaluate("""() => {
    const btn = document.querySelector('button.btn-b-red.ml16');
    const r = btn.getBoundingClientRect();
    return {x: r.x + r.width/2, y: r.y + r.height/2};
}""")
page.mouse.click(pub_rect["x"], pub_rect["y"])

为什么用 page.mouse.click() 而不是 locator.click() 或 JS click()

  • JS click():Vue 框架通过事件代理监听,原生 JS click() 不会触发 Vue 的事件处理
  • locator.click():Playwright 的智能点击会被遮罩层拦截,force=True 也不行
  • mouse.click():模拟真实鼠标事件,触发完整的 mousedown → mouseup → click 链,Vue 能正确响应

第四步:封装成命令行工具

把上面所有步骤整合到一个 Python 脚本里,支持三个子命令:

# 登录
python3 csdn_publish.py login

# 发布
python3 csdn_publish.py publish --title "标题" --file article.md --tags "Python" --category "人工智能"

# 保存草稿
python3 csdn_publish.py publish --title "标题" --file article.md --draft

# 调试页面元素
python3 csdn_publish.py inspect

第五步:封装成 Claude Code Skill

Claude Code 的 Skill 机制可以把这个脚本变成 AI 可以直接调用的工具。

安装方式

~/.claude/skills/csdn-publish/
├── SKILL.md          # Skill 定义文件
└── csdn_publish.py   # 自动化脚本

SKILL.md 定义了 Skill 的名称、描述和用法,Claude Code 启动时会自动加载。

使用方式

在 Claude Code 中,只需说:

“帮我把这篇 Markdown 发到 CSDN”

Claude Code 就会读取文件、调用 Skill、执行发布流程。

踩坑总结

# 问题 原因 解决方案
1 标题填不进去 input 是 hidden 先点击 .article-bar__title-display 激活
2 正文格式丢失 innerText 丢失 MD 语法 改用 ClipboardEvent 粘贴
3 JS click() 无效 Vue 不响应原生 JS click 改用 page.mouse.click() 坐标点击
4 遮罩层拦截 mark-mask-box-div 覆盖弹窗 remove() 从 DOM 移除
5 标签找不到 标签是 span.el-tag 不是 label 检查实际 DOM 结构
6 发布按钮被挡 标签下拉没关 + 视口外 关闭下拉 + scrollIntoView
7 按钮点击无反应 按钮在视口边缘 scrollIntoView({block: 'center'})

进阶用法

批量发布

写一个 shell 脚本批量发布目录下的所有 Markdown 文件:

#!/bin/bash
for file in ~/articles/*.md; do
    title=$(head -1 "$file" | sed 's/^# //')
    python3 csdn_publish.py publish --title "$title" --file "$file" --tags "Python"
    sleep 10  # 避免频率过快
done

定时发布

配合 cron 实现定时发布:

# 每天早上 9 点检查并发布草稿箱里的文章
0 9 * * * python3 ~/csdn_publish.py publish --title "每日分享" --file ~/daily.md --draft

总结

这个方案的核心思路是:不用 API,不用爬虫,直接操控浏览器

Playwright 的浏览器自动化能力 + 合理的页面结构分析,让"写完就发"变成了一条命令的事。整个脚本不到 300 行代码,核心难点不在代码本身,而在调试 CSDN 的页面结构——隐藏的标题 input、左右栏标签、遮罩层拦截,每一个都是需要实际跑一遍才能发现的坑。

如果你也想自动化自己的博客发布流程,这套方案可以直接拿来用。有问题欢迎评论区交流。


技术栈: Python 3.13, Playwright 1.58, Claude Code Skill

完整代码: 已封装为 Claude Code Skill,安装路径 ~/.claude/skills/csdn-publish/

一行命令发布 CSDN 博客:Claude Code + Playwright 自动化实战指南

前言

写技术文章的流程通常是这样的:在 IDE 里写完 Markdown,切到浏览器,打开 CSDN 创作中心,粘贴标题,粘贴正文,选标签,选分类,点发布。

一篇文章还好,十篇呢?二十篇呢?

这篇文章记录了我如何用 Playwright 浏览器自动化 把这个机械流程变成一行命令。核心代码不到 300 行,之后每次发文章就是一条命令的事。

你会学到什么

读完这篇文章,你将掌握:

  1. 如何用 Playwright 自动化 CSDN 的 Markdown 编辑器
  2. 如何处理 CSDN 特有的页面结构(隐藏标题栏、左右栏标签选择、遮罩层拦截)
  3. 如何将自动化脚本封装成 Claude Code Skill,实现 AI 驱动的博客发布

技术方案概览

整体架构非常简单:

CLI 命令 → Python 脚本 → Playwright → Chrome 浏览器 → CSDN 编辑器

没有用 CSDN 的 API(因为没有公开 API),也没有写爬虫,而是直接操控浏览器——和你手动操作一模一样,只是换成了代码。

为什么选 Playwright 而不是 Selenium

对比项 Playwright Selenium
安装 pip install playwright 一行搞定 需要额外下载 ChromeDriver
速度 快,CDP 协议直连 相对慢
API 设计 locator 链式调用,更优雅 find_element 较繁琐
异步支持 原生 async 需要额外封装
等待机制 内置智能等待 需要手动 WebDriverWait

关键设计决策

  1. Cookie 复用:首次手动登录后保存 cookies,后续自动加载,避免每次都要扫码
  2. 多重选择器 fallback:对每个页面元素准备多个 CSS 选择器,CSDN 改版后容错性更强
  3. 鼠标坐标点击:CSDN 的 Vue 框架不响应 JS click(),必须用真实鼠标事件
  4. 遮罩层移除:CSDN 弹窗的 mark-mask-box-div 会拦截点击,直接从 DOM 移除

第一步:环境搭建

安装依赖

pip install playwright
playwright install chromium

首次登录

from playwright.sync_api import sync_playwright
from pathlib import Path

COOKIE_FILE = Path.home() / ".csdn_cookies.json"

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    context = browser.new_context()
    page = context.new_page()
    page.goto("https://passport.csdn.net/login")

    # 手动扫码登录,登录成功后保存 cookies
    page.wait_for_url("**/my.csdn.net/**", timeout=120_000)
    context.storage_state(path=str(COOKIE_FILE))
    browser.close()

之后每次运行都加载这个 cookie 文件,无需再次登录。

第二步:编辑器页面操作

切换到 Markdown 模式

CSDN 编辑器默认打开是"比对"模式,需要先点击 “Markdown” 标签切换:

page.locator("button.nav-tab-btn", has_text="Markdown").first.click()

填写标题(隐藏 input 问题)

这是踩的第一个坑。CSDN 在 Markdown 模式下,标题的 input 元素是 hidden 的:

<input class="article-bar__title article-bar__title--input"
       placeholder="请输入文章标题"
       aria-hidden="true" />

页面上实际显示的是一个 div.article-bar__title-display,点击它之后,隐藏的 input 才会变成可见:

# 点击显示区域,激活隐藏的 input
page.locator(".article-bar__title-display").click()
time.sleep(0.5)

# 等待 input 可见后填入标题
title_input = page.locator("input.article-bar__title--input")
title_input.wait_for(state="visible", timeout=3000)
title_input.fill("")
title_input.type(title, delay=30)

注入 Markdown 正文

这是第二个坑。编辑器是 contenteditablepre 元素:

<pre class="editor__inner markdown-highlighting" contenteditable="true">

直接用 innerText 注入会丢失 Markdown 格式(代码块、标题层级等全没了)。正确做法是用 剪贴板粘贴

# 先点击编辑器聚焦
editor.click()
time.sleep(0.3)

# 全选原有内容并删除
page.keyboard.press("Control+a")
time.sleep(0.2)

# 用 ClipboardEvent 粘贴 Markdown(保留格式)
page.evaluate("""(text) => {
    const dt = new DataTransfer();
    dt.setData('text/plain', text);
    const el = document.querySelector('pre.editor__inner');
    el.dispatchEvent(new ClipboardEvent('paste', {
        clipboardData: dt, bubbles: true, cancelable: true
    }));
}""", markdown_content)

这样粘贴进去的内容,CSDN 编辑器会自动解析 Markdown 语法,代码块、表格、引用等格式全部保留。

第三步:发布弹窗操作

打开发布弹窗

点击编辑器右上角的"发布文章"按钮,弹出发布设置弹窗:

page.locator("button.btn.btn-publish").first.click()
time.sleep(3)

标签选择(左右栏结构)

这是最复杂的部分。CSDN 的标签选择是 左右两栏 结构:

┌──────────────┬────────────────────────┐
│  推荐         │  python  django  pygame │
│  Python  ←   │  tornado  flask        │
│  Java         │  fastapi  scrapy       │
│  编程语言     │  numpy  pandas         │
│  ...         │  ...                   │
└──────────────┴────────────────────────┘
  • 左栏 (ul.mark_add_tag_left > li):标签分类,如 Python、Java、人工智能
  • 右栏 (div.mark_add_tag_right > span.el-tag):该分类下的具体标签

选择流程:

# 1. 点击左侧 "Python" 分类
py_category = page.evaluate("""() => {
    const lis = document.querySelectorAll('.mark_add_tag_left li');
    for (const li of lis) {
        if (li.innerText.trim() === 'Python') {
            const r = li.getBoundingClientRect();
            return {x: r.x + r.width/2, y: r.y + r.height/2};
        }
    }
    return null;
}""")
page.mouse.click(py_category["x"], py_category["y"])
time.sleep(1)

# 2. 点击右侧 "python" 标签
tag = page.evaluate("""() => {
    const tags = document.querySelectorAll('.mark_add_tag_right span.el-tag');
    for (const t of tags) {
        if (t.innerText.trim() === 'python') {
            const r = t.getBoundingClientRect();
            return {x: r.x + r.width/2, y: r.y + r.height/2};
        }
    }
    return null;
}""")
page.mouse.click(tag["x"], tag["y"])

点击发布按钮(遮罩层拦截问题)

这是最大的坑。CSDN 弹窗有一个 mark-mask-box-div 遮罩层,会拦截所有鼠标事件。不管是 Playwright 的 locator.click() 还是 JS 的 click(),都会被挡掉。

最终解决方案:

# 1. 从 DOM 彻底移除遮罩层
page.evaluate("""() => {
    document.querySelectorAll('.mark-mask-box-div').forEach(m => m.remove());
}""")

# 2. 滚动发布按钮到视口中心(避免按钮在视口边缘导致点击无效)
page.evaluate("""() => {
    const btn = document.querySelector('button.btn-b-red.ml16');
    if (btn) btn.scrollIntoView({block: 'center'});
}""")

# 3. 用鼠标坐标点击(不用 locator.click(),Vue 不响应)
pub_rect = page.evaluate("""() => {
    const btn = document.querySelector('button.btn-b-red.ml16');
    const r = btn.getBoundingClientRect();
    return {x: r.x + r.width/2, y: r.y + r.height/2};
}""")
page.mouse.click(pub_rect["x"], pub_rect["y"])

为什么用 page.mouse.click() 而不是 locator.click() 或 JS click()

  • JS click():Vue 框架通过事件代理监听,原生 JS click() 不会触发 Vue 的事件处理
  • locator.click():Playwright 的智能点击会被遮罩层拦截,force=True 也不行
  • mouse.click():模拟真实鼠标事件,触发完整的 mousedown → mouseup → click 链,Vue 能正确响应

第四步:封装成命令行工具

把上面所有步骤整合到一个 Python 脚本里,支持三个子命令:

# 登录
python3 csdn_publish.py login

# 发布
python3 csdn_publish.py publish --title "标题" --file article.md --tags "Python" --category "人工智能"

# 保存草稿
python3 csdn_publish.py publish --title "标题" --file article.md --draft

# 调试页面元素
python3 csdn_publish.py inspect

第五步:封装成 Claude Code Skill

Claude Code 的 Skill 机制可以把这个脚本变成 AI 可以直接调用的工具。

安装方式

~/.claude/skills/csdn-publish/
├── SKILL.md          # Skill 定义文件
└── csdn_publish.py   # 自动化脚本

SKILL.md 定义了 Skill 的名称、描述和用法,Claude Code 启动时会自动加载。

使用方式

在 Claude Code 中,只需说:

“帮我把这篇 Markdown 发到 CSDN”

Claude Code 就会读取文件、调用 Skill、执行发布流程。

踩坑总结

# 问题 原因 解决方案
1 标题填不进去 input 是 hidden 先点击 .article-bar__title-display 激活
2 正文格式丢失 innerText 丢失 MD 语法 改用 ClipboardEvent 粘贴
3 JS click() 无效 Vue 不响应原生 JS click 改用 page.mouse.click() 坐标点击
4 遮罩层拦截 mark-mask-box-div 覆盖弹窗 remove() 从 DOM 移除
5 标签找不到 标签是 span.el-tag 不是 label 检查实际 DOM 结构
6 发布按钮被挡 标签下拉没关 + 视口外 关闭下拉 + scrollIntoView
7 按钮点击无反应 按钮在视口边缘 scrollIntoView({block: 'center'})

进阶用法

批量发布

写一个 shell 脚本批量发布目录下的所有 Markdown 文件:

#!/bin/bash
for file in ~/articles/*.md; do
    title=$(head -1 "$file" | sed 's/^# //')
    python3 csdn_publish.py publish --title "$title" --file "$file" --tags "Python"
    sleep 10  # 避免频率过快
done

定时发布

配合 cron 实现定时发布:

# 每天早上 9 点检查并发布草稿箱里的文章
0 9 * * * python3 ~/csdn_publish.py publish --title "每日分享" --file ~/daily.md --draft

总结

这个方案的核心思路是:不用 API,不用爬虫,直接操控浏览器

Playwright 的浏览器自动化能力 + 合理的页面结构分析,让"写完就发"变成了一条命令的事。整个脚本不到 300 行代码,核心难点不在代码本身,而在调试 CSDN 的页面结构——隐藏的标题 input、左右栏标签、遮罩层拦截,每一个都是需要实际跑一遍才能发现的坑。

如果你也想自动化自己的博客发布流程,这套方案可以直接拿来用。有问题欢迎评论区交流。


技术栈: Python 3.13, Playwright 1.58, Claude Code Skill

完整代码: 已封装为 Claude Code Skill,安装路径 ~/.claude/skills/csdn-publish/

Logo

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务

更多推荐