一行命令发布CSDN博客:Claude Code + Playwright自动化实战指南
问题原因解决方案1标题填不进去input 是 hidden先点击激活2正文格式丢失innerText丢失 MD 语法改用粘贴3JSclick()无效Vue 不响应原生 JS click改用坐标点击4遮罩层拦截覆盖弹窗remove()从 DOM 移除5标签找不到标签是不是label检查实际 DOM 结构6发布按钮被挡标签下拉没关 + 视口外关闭下拉 +7按钮点击无反应按钮在视口边缘不用 API,不用
一行命令发布 CSDN 博客:Claude Code + Playwright 自动化实战指南
前言
写技术文章的流程通常是这样的:在 IDE 里写完 Markdown,切到浏览器,打开 CSDN 创作中心,粘贴标题,粘贴正文,选标签,选分类,点发布。
一篇文章还好,十篇呢?二十篇呢?
这篇文章记录了我如何用 Playwright 浏览器自动化 把这个机械流程变成一行命令。核心代码不到 300 行,之后每次发文章就是一条命令的事。
你会学到什么
读完这篇文章,你将掌握:
- 如何用 Playwright 自动化 CSDN 的 Markdown 编辑器
- 如何处理 CSDN 特有的页面结构(隐藏标题栏、左右栏标签选择、遮罩层拦截)
- 如何将自动化脚本封装成 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 |
关键设计决策
- Cookie 复用:首次手动登录后保存 cookies,后续自动加载,避免每次都要扫码
- 多重选择器 fallback:对每个页面元素准备多个 CSS 选择器,CSDN 改版后容错性更强
- 鼠标坐标点击:CSDN 的 Vue 框架不响应 JS
click(),必须用真实鼠标事件 - 遮罩层移除: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 正文
这是第二个坑。编辑器是 contenteditable 的 pre 元素:
<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 框架通过事件代理监听,原生 JSclick()不会触发 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 行,之后每次发文章就是一条命令的事。
你会学到什么
读完这篇文章,你将掌握:
- 如何用 Playwright 自动化 CSDN 的 Markdown 编辑器
- 如何处理 CSDN 特有的页面结构(隐藏标题栏、左右栏标签选择、遮罩层拦截)
- 如何将自动化脚本封装成 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 |
关键设计决策
- Cookie 复用:首次手动登录后保存 cookies,后续自动加载,避免每次都要扫码
- 多重选择器 fallback:对每个页面元素准备多个 CSS 选择器,CSDN 改版后容错性更强
- 鼠标坐标点击:CSDN 的 Vue 框架不响应 JS
click(),必须用真实鼠标事件 - 遮罩层移除: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 正文
这是第二个坑。编辑器是 contenteditable 的 pre 元素:
<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 框架通过事件代理监听,原生 JSclick()不会触发 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/
更多推荐



所有评论(0)