1.搭建环境
# 安装依赖包
pip install playwright schedule pyyaml markdown
playwright install chromium
2.创建项目结构
csdn_auto_publisher/
│
├── config.yaml # 配置文件(存放账号文章路径)
├── csdn_publisher.py # 主发布脚本
├── articles/ # 存放待发布文章的文件夹
│ ├── 2026-06-15_文章标题1.md
│ └── 2026-06-16_文章标题2.md
├── cookies.json # 登录凭证(自动生成)
└── logs/ # 日志文件夹
3. 写一个YAML配置文件:
# CSDN 自动发布配置
csdn:
cookie_file: "cookies.json" # 字符串
editor_url: "https://editor.csdn.net/md/"
article:
source_dir: "./articles" # 路径
auto_set_tags: true # 布尔值
tags: # 列表
- "Python"
- "后端开发"
schedule:
publish_time: "08:00" # 字符串(虽然像数字)
timezone: "Asia/Shanghai"
browser:
headless: false # 是否无头模式
4.文件说明代码块
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
CSDN 博客自动发布脚本 v2.0
功能:每天08:00自动扫描articles文件夹,读取Markdown内容并发布到CSDN
"""
5.接口调用代码块
import os
import re
import sys
import time
import json
import logging
import yaml
import schedule
import markdown
from pathlib import Path
from datetime import datetime
from playwright.sync_api import sync_playwright
6.配置日志库
# ========== 日志配置 ==========
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('logs/publisher.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
7.加载配置文件
# ========== 工具函数 ==========
def load_config():
"""读取配置文件"""
with open('config.yaml', 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
8.加载cookie
def load_cookies(context, cookie_file):
"""加载已保存的登录凭证(避免重复登录)"""
if os.path.exists(cookie_file):
with open(cookie_file, 'r', encoding='utf-8') as f:
cookies = json.load(f)
context.add_cookies(cookies)
logger.info("✅ 加载已保存的登录凭证")
return True
logger.info("⚠️ 未找到凭证文件,需要手动登录")
return False
9.保存cookie
def save_cookies(context, cookie_file):
"""保存登录凭证"""
cookies = context.cookies()
with open(cookie_file, 'w', encoding='utf-8') as f:
json.dump(cookies, f)
logger.info("💾 登录凭证已保存")
def parse_markdown_file(file_path):
"""解析Markdown文件,提取标题和内容"""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 提取标题(首行 # 开头的行)
title_match = re.search(r'^#\s+(.+?)$', content, re.MULTILINE)
if title_match:
title = title_match.group(1).strip()
# 移除正文中的标题行
content = re.sub(r'^#\s+.+?$\n?', '', content, count=1, flags=re.MULTILINE)
else:
# 如果没有#标题,用文件名作标题
title = Path(file_path).stem
# 移除日期前缀(可选)
title = re.sub(r'^\d{4}-\d{2}-\d{2}_', '', title)
return title, content.strip()
def extract_tags_from_title(title):
"""从标题提取关键词作为标签(用于CSDN标签系统)"""
# 提取技术关键词(常见技术栈词汇库)
tech_keywords = [
"Python", "Java", "Go", "Rust", "JavaScript", "TypeScript", "Vue", "React",
"Spring", "Docker", "Kubernetes", "MySQL", "Redis", "MongoDB", "PostgreSQL",
"AI", "机器学习", "深度学习", "数据分析", "算法", "架构", "微服务", "后端", "前端"
]
found = []
for kw in tech_keywords:
if kw.lower() in title.lower():
found.append(kw)
return found[:3] # 最多返回3个标签
def get_pending_articles(source_dir, published_log):
"""获取待发布的文章列表(检查是否已发布过)"""
published = set()
if os.path.exists(published_log):
with open(published_log, 'r', encoding='utf-8') as f:
published = set(line.strip() for line in f)
md_files = list(Path(source_dir).glob("*.md"))
pending = []
for md_file in md_files:
if md_file.name not in published:
title, content = parse_markdown_file(md_file)
pending.append({
'file': md_file,
'filename': md_file.name,
'title': title,
'content': content
})
return pending
def mark_published(filename, published_log):
"""标记文章为已发布"""
with open(published_log, 'a', encoding='utf-8') as f:
f.write(f"{filename}\n")
logger.info(f"📝 已记录: {filename}")
# ========== 核心发布函数(Playwright方案)==========
def publish_article(title, content, config):
"""使用Playwright发布单篇文章到CSDN"""
with sync_playwright() as p:
# 启动浏览器(持久上下文 + 反检测配置)
browser = p.chromium.launch(
headless=config['browser']['headless'],
args=[
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
'--disable-dev-shm-usage'
]
)
# 创建上下文(模拟真实用户)
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
)
# 尝试加载Cookie
cookie_file = config['csdn']['cookie_file']
cookies_loaded = load_cookies(context, cookie_file)
page = context.new_page()
try:
# 步骤1:打开CSDN编辑器页面
editor_url = config['csdn']['editor_url']
page.goto(editor_url, timeout=60000)
logger.info("🌐 已打开CSDN编辑器页面")
time.sleep(3)
# 步骤2:如果需要登录,手动完成
if not cookies_loaded or "login" in page.url:
logger.warning("🔐 需要登录,请在浏览器中扫码登录...")
logger.warning("页面打开后将显示二维码,登录后脚本会自动继续")
# 等待跳转到编辑页面(最长90秒)
page.wait_for_url(lambda url: "editor.csdn.net/md" in url, timeout=90000)
# 保存登录凭证
save_cookies(context, cookie_file)
logger.info("✅ 登录完成,凭证已保存")
time.sleep(3)
# 步骤3:填写文章标题
title_selector = 'input[placeholder*="标题"]'
page.wait_for_selector(title_selector, timeout=10000)
page.fill(title_selector, "")
page.fill(title_selector, title)
logger.info(f"📌 标题已填写: {title[:50]}...")
time.sleep(1)
# 步骤4:填写文章正文(Markdown编辑器)
# CSDN编辑器是基于textarea的
content_selector = 'textarea'
page.wait_for_selector(content_selector, timeout=10000)
# 如果有图片引用,需要将本地图片转为base64(进阶功能,此处简化)
# 直接将Markdown内容填入
page.fill(content_selector, content)
logger.info(f"📄 正文已填写 (长度: {len(content)} 字符)")
time.sleep(2)
# 步骤5:填写文章标签
if config['article'].get('auto_set_tags', True):
tags = extract_tags_from_title(title)
if not tags:
tags = config['article'].get('tags', ['技术', '开发'])
# 点击添加标签按钮
tag_input = page.locator('input[placeholder*="标签"]').first
if tag_input.count() > 0:
for tag in tags:
tag_input.fill(tag)
time.sleep(0.5)
page.keyboard.press("Enter")
time.sleep(0.3)
logger.info(f"🏷️ 标签已添加: {', '.join(tags)}")
# 步骤6:点击发布按钮
publish_btn = page.locator('button:has-text("发布文章")')
if publish_btn.count() == 0:
publish_btn = page.locator('button:has-text("发布")')
if publish_btn.count() > 0:
publish_btn.first.click()
logger.info("🚀 正在提交发布...")
time.sleep(5)
else:
logger.error("❌ 未找到发布按钮")
return False
# 步骤7:等待发布成功(检查成功提示)
page.wait_for_timeout(5000)
if "发布成功" in page.content() or page.url.startswith("https://blog.csdn.net/"):
logger.info(f"✅ 发布成功!文章: {title}")
return True
else:
logger.warning("⚠️ 未检测到成功提示,请人工检查")
return False
except Exception as e:
logger.error(f"❌ 发布失败: {str(e)}")
return False
finally:
browser.close()
# ========== 批量发布调度器 ==========
def run_publisher():
"""执行发布任务(扫描所有待发文章并逐一发布)"""
logger.info("=" * 50)
logger.info(f"📆 开始执行定时任务: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
config = load_config()
source_dir = config['article']['source_dir']
published_log = config['article']['published_log']
# 检查文章目录
if not os.path.exists(source_dir):
os.makedirs(source_dir)
logger.info(f"📁 创建文章目录: {source_dir}")
# 获取待发布文章
pending = get_pending_articles(source_dir, published_log)
if not pending:
logger.info("📭 没有待发布的文章")
return
logger.info(f"📚 发现 {len(pending)} 篇待发布文章")
# 逐一发布
for idx, article in enumerate(pending, 1):
logger.info(f"\n--- [{idx}/{len(pending)}] 正在发布: {article['title']} ---")
success = publish_article(
title=article['title'],
content=article['content'],
config=config
)
if success:
mark_published(article['filename'], published_log)
logger.info(f"✨ 发布完成: {article['filename']}")
else:
logger.error(f"💥 发布失败: {article['filename']}")
# 发布间隔(避免被识别为异常操作)
if idx < len(pending):
wait_time = 60
logger.info(f"⏳ 等待 {wait_time} 秒后发布下一篇...")
time.sleep(wait_time)
logger.info("🎉 全部发布任务执行完毕")
def schedule_daily():
"""配置定时任务(每天08:00执行)"""
config = load_config()
publish_time = config['schedule']['publish_time']
schedule.every().day.at(publish_time).do(run_publisher)
logger.info(f"⏰ 定时任务已设定: 每日 {publish_time} 自动发布")
while True:
schedule.run_pending()
time.sleep(60)
# ========== 入口函数 ==========
if __name__ == "__main__":
if not os.path.exists('logs'):
os.makedirs('logs')
if not os.path.exists('articles'):
os.makedirs('articles')
if len(sys.argv) > 1 and sys.argv[1] == "--now":
# 立即执行发布(测试用)
run_publisher()
else:
# 启动定时任务模式
schedule_daily()
```
📅 配置 Windows 任务计划(每日08:00准时触发)
1. 打开 任务计划程序 → 创建任务
2. 常规:
· 名称:CSDN_Auto_Publish_Daily
· 勾选「不管用户是否登录都要运行」
· 勾选「使用最高权限运行」
3. 触发器:
· 新建 → 每天 → 开始时间 08:00:00 → 勾选「已启用」
4. 操作:
· 新建 → 启动程序
· 程序:python
· 添加参数:D:\csdn_auto_publisher\csdn_publisher_once.py(你的脚本绝对路径)
· 起始于:D:\csdn_auto_publisher
5. 条件:取消所有「只有在以下条件才启动」的勾选(保证即使未登录也会执行)
6. 设置:勾选「如果任务失败,按以下频率重新启动」→ 间隔 5 分钟,最多 3 次