用 Playwright 自动发帖到知乎和小红书:一个程序员的偷懒指南
Posted on 六 21 3月 2026 in Tech
用 Playwright 自动发帖到知乎和小红书:一个程序员的偷懒指南
一、程序员的内容分发之痛
写技术博客的人大概都有这个烦恼:文章写完了,发布才是噩梦的开始。
先在自己的博客站发一遍,然后复制到知乎专栏,格式全乱了,得重新调。再发小红书,标题要改短,正文要加 emoji,图片要重新上传。如果还想发公众号,那又是另一套排版规则。
一篇文章,发三个平台,光复制粘贴加排版就得半小时。写文章本身可能也就一小时。
咱们是程序员啊,重复劳动这种事,不应该让机器来干吗?
有人会说,用各平台的 API 不就行了?问题是——知乎没有公开的发文 API,小红书的开放平台主要面向商家,个人创作者基本拿不到权限。就算有 API,每个平台的接口规范、鉴权方式都不一样,维护成本也不低。
所以咱们换个思路:既然手动操作浏览器能发,那就让程序来操作浏览器。
这就是 Playwright 的用武之地。
二、为什么选 Playwright
浏览器自动化工具不少,Selenium 是老前辈,Puppeteer 是 Node.js 生态的宠儿。我选 Playwright 有几个理由:
| 对比项 | Selenium | Puppeteer | Playwright |
|---|---|---|---|
| 语言支持 | 多语言 | JS/TS | Python/JS/Java/C# |
| 浏览器 | 需要 WebDriver | 仅 Chromium | Chromium + Firefox + WebKit |
| 自动等待 | 手动写 wait | 部分自动 | 内置智能等待 |
| 录制工具 | 无 | 无 | codegen 一键录制 |
| Cookie 管理 | 手动 | 手动 | storage_state 一行搞定 |
最打动我的是两个功能:codegen 录制和 storage_state 登录状态管理。前者让你不用猜 DOM 选择器,后者让你不用每次都扫码登录。
三、基本套路:录制 → 改写 → 封装
整个流程就三步:
1. codegen 录制操作,顺便保存登录状态
2. 把录制的代码改写成可复用的函数
3. 加上错误处理和参数化,封装成 CLI
就像练功,先照着师傅的动作比划(录制),再理解每一招的用意(改写),最后融会贯通变成自己的(封装)。
3.1 安装
pip install playwright
playwright install chromium
两行命令,连浏览器都帮你下好了。
3.2 录制登录操作
这是最关键的一步。codegen 会打开一个浏览器窗口,你在里面正常操作,它在旁边实时生成代码。
# 录制知乎登录,保存 cookie
playwright codegen https://www.zhihu.com/signin --save-storage zhihu-auth.json
# 录制小红书登录,保存 cookie
playwright codegen https://www.xiaohongshu.com --save-storage xhs-auth.json
操作流程: 1. 浏览器打开后,正常登录(扫码或密码都行) 2. 登录成功后,关闭浏览器 3. Cookie 自动保存到 json 文件
以后的脚本直接加载这个 json,就跳过登录了。省心。
四、知乎自动发文章
知乎专栏的发文页面是 https://zhuanlan.zhihu.com/write,用的是 Draft.js 富文本编辑器。
#!/usr/bin/env python3
"""zhihu_poster.py — 自动发布知乎文章"""
import asyncio
from playwright.async_api import async_playwright
async def post_to_zhihu(
title: str,
content: str,
topics: list[str] | None = None,
headless: bool = False,
dry_run: bool = False,
) -> str | None:
"""发布文章到知乎专栏,返回文章 URL。"""
async with async_playwright() as p:
browser = await p.chromium.launch(headless=headless)
context = await browser.new_context(storage_state="zhihu-auth.json")
page = await context.new_page()
try:
# 1. 打开写文章页面
await page.goto("https://zhuanlan.zhihu.com/write")
await page.wait_for_load_state("networkidle")
# 2. 填写标题
title_input = page.locator('textarea[placeholder*="请输入标题"]')
await title_input.click()
await title_input.fill(title)
# 3. 填写正文
# 知乎用 Draft.js,不能直接 fill,得模拟键盘输入
editor = page.locator(".public-DraftEditor-content")
await editor.click()
paragraphs = content.split("\n\n")
for i, para in enumerate(paragraphs):
await page.keyboard.type(para, delay=20)
if i < len(paragraphs) - 1:
await page.keyboard.press("Enter")
await page.keyboard.press("Enter")
# 4. 添加话题
if topics:
topic_btn = page.locator('button:has-text("添加话题")')
if await topic_btn.is_visible():
await topic_btn.click()
for topic in topics:
topic_input = page.locator(
'input[placeholder*="搜索话题"]'
)
await topic_input.fill(topic)
await page.wait_for_timeout(1000)
first_result = page.locator(
".TopicSelector-item"
).first
if await first_result.is_visible():
await first_result.click()
# 5. 发布(或 dry run 截图)
if dry_run:
await page.screenshot(path="zhihu-preview.png")
print("🔍 Dry run 完成,截图已保存: zhihu-preview.png")
return None
publish_btn = page.locator('button:has-text("发布")')
await publish_btn.click()
await page.wait_for_url("**/p/**", timeout=15000)
article_url = page.url
print(f"✅ 知乎文章已发布: {article_url}")
# 保存更新后的 cookie
await context.storage_state(path="zhihu-auth.json")
return article_url
except Exception as e:
# 出错时截图,方便排查
await page.screenshot(path="zhihu-error.png")
print(f"❌ 发布失败: {e},截图已保存: zhihu-error.png")
raise
finally:
await browser.close()
几个要注意的点:
- Draft.js 编辑器不能用
fill(),得用keyboard.type()模拟逐字输入,否则内容不会被编辑器识别 delay=20是每个字符之间的间隔(毫秒),太快可能丢字,太慢浪费时间,20ms 是个不错的平衡- dry run 模式很重要——先截图看看效果,确认没问题再真发
五、小红书自动发笔记
小红书的创作者后台在 https://creator.xiaohongshu.com/publish/publish,和知乎最大的区别是:必须有图片。
#!/usr/bin/env python3
"""xhs_poster.py — 自动发布小红书笔记"""
import asyncio
from playwright.async_api import async_playwright
async def post_to_xiaohongshu(
title: str,
content: str,
images: list[str] | None = None,
topics: list[str] | None = None,
headless: bool = False,
dry_run: bool = False,
):
"""发布笔记到小红书。images 为图片路径列表,至少一张。"""
if not images:
print("⚠️ 小红书必须上传至少一张图片")
return
async with async_playwright() as p:
browser = await p.chromium.launch(headless=headless)
context = await browser.new_context(storage_state="xhs-auth.json")
page = await context.new_page()
try:
# 1. 打开发布页
await page.goto(
"https://creator.xiaohongshu.com/publish/publish"
)
await page.wait_for_load_state("networkidle")
# 2. 上传图片
upload_input = page.locator('input[type="file"]')
for img_path in images:
await upload_input.set_input_files(img_path)
await page.wait_for_timeout(2000) # 等上传完成
# 3. 填写标题(小红书标题限 20 字)
if len(title) > 20:
print(f"⚠️ 标题超过 20 字,已截断: {title[:20]}...")
title = title[:20]
title_input = page.locator(
'#title-textarea, [placeholder*="标题"]'
)
await title_input.click()
await title_input.fill(title)
# 4. 填写正文,话题标签直接写在正文末尾
full_content = content
if topics:
tags = " ".join(f"#{t}" for t in topics)
full_content = f"{content}\n\n{tags}"
content_editor = page.locator(
'#post-textarea, [placeholder*="正文"]'
)
await content_editor.click()
await page.keyboard.type(full_content, delay=15)
# 5. 发布
if dry_run:
await page.screenshot(path="xhs-preview.png")
print("🔍 Dry run 完成,截图已保存: xhs-preview.png")
return
publish_btn = page.locator('button:has-text("发布")')
await publish_btn.click()
await page.wait_for_timeout(5000)
print("✅ 小红书笔记已发布")
await context.storage_state(path="xhs-auth.json")
except Exception as e:
await page.screenshot(path="xhs-error.png")
print(f"❌ 发布失败: {e},截图已保存: xhs-error.png")
raise
finally:
await browser.close()
小红书的坑比知乎多一些:
- 标题限 20 字,超了会被截断,脚本里做了自动处理
- 图片是必须的,纯文字发不了
- 话题标签不像知乎有专门的选择器,小红书的做法是直接在正文里写
#话题,平台会自动识别
六、封装成统一 CLI
两个平台的脚本有了,再包一层 CLI,一行命令搞定多平台分发:
#!/usr/bin/env python3
"""auto_poster.py — 多平台自动发帖 CLI"""
import argparse
import asyncio
from pathlib import Path
async def main():
parser = argparse.ArgumentParser(
description="自动发帖到知乎/小红书",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
# 发知乎文章
python auto_poster.py zhihu --title "AI编程" --content ./blog.md
# 发小红书笔记
python auto_poster.py xhs --title "周末coding" \\
--content "今天写了个工具..." --images cover.jpg
# 同时发两个平台(先预览)
python auto_poster.py all --title "新文章" \\
--content ./article.md --images cover.jpg --dry-run
""",
)
parser.add_argument(
"platform", choices=["zhihu", "xhs", "all"],
help="目标平台"
)
parser.add_argument("--title", required=True, help="文章标题")
parser.add_argument(
"--content", required=True,
help="正文内容,或 .md/.txt 文件路径"
)
parser.add_argument("--images", nargs="*", help="图片路径列表")
parser.add_argument("--topics", nargs="*", help="话题标签")
parser.add_argument("--headless", action="store_true", help="无头模式")
parser.add_argument(
"--dry-run", action="store_true", help="只预览截图,不真正发布"
)
args = parser.parse_args()
# 支持从文件读取正文
content_path = Path(args.content)
if content_path.exists() and content_path.suffix in (".md", ".txt"):
content = content_path.read_text(encoding="utf-8")
# 简单处理 Markdown front matter
if content.startswith("---"):
# 跳过 YAML front matter
end = content.find("---", 3)
if end != -1:
content = content[end + 3 :].strip()
else:
content = args.content
results = {}
if args.platform in ("zhihu", "all"):
url = await post_to_zhihu(
title=args.title,
content=content,
topics=args.topics,
headless=args.headless,
dry_run=args.dry_run,
)
results["zhihu"] = url
if args.platform in ("xhs", "all"):
await post_to_xiaohongshu(
title=args.title,
content=content,
images=args.images,
topics=args.topics,
headless=args.headless,
dry_run=args.dry_run,
)
results["xhs"] = "posted"
# 打印汇总
print("\n📋 发布汇总:")
for platform, result in results.items():
status = result if result else "dry-run"
print(f" {platform}: {status}")
if __name__ == "__main__":
asyncio.run(main())
用法很直观:
# 发知乎
python auto_poster.py zhihu --title "AI编程时代的思考" --content ./blog.md --topics "AI" "编程"
# 发小红书
python auto_poster.py xhs --title "周末coding💻" --content "今天试了个新工具..." --images cover.jpg
# 两个平台都发,先 dry run 看看
python auto_poster.py all --title "新文章" --content ./article.md --images cover.jpg --dry-run
七、踩坑备忘录
用了一段时间,总结几个实战经验:
选择器会变,codegen 是你的好朋友
知乎和小红书都在频繁迭代前端,class 名隔几周就可能变。我的经验是:
- 优先用
text=、placeholder=、role=这类语义选择器,比.css-1a2b3c稳定得多 - 选择器失效了?别硬猜,重新跑一遍
playwright codegen,两分钟搞定
Cookie 会过期
知乎的登录态大约能撑一周,小红书更短,可能两三天就要重新登录。两个应对办法:
- 脚本开头检测登录状态,失效了自动提醒
- 写个定时任务,每隔几天手动刷新一次
storage_state
# 检测登录状态的简单方法
async def check_login(page, platform):
if platform == "zhihu":
# 检查是否有登录按钮(有 = 未登录)
login_btn = page.locator('button:has-text("登录")')
if await login_btn.is_visible():
print("⚠️ 知乎 cookie 已过期,请重新录制登录")
return False
return True
别太快,别太频繁
这些平台都有反爬机制。几个建议:
- 字符输入加随机延迟:
delay=random.randint(15, 50)比固定 20ms 更像真人 - 操作之间加等待:
wait_for_timeout(random.randint(500, 1500)) - 发布频率控制:一天 2-3 篇为上限,别搞得像机器人批量灌水
- 首次调试用 headed 模式:
headless=False,亲眼看着跑一遍
错误处理要到位
网络抖动、页面加载慢、弹窗遮挡……线上环境什么情况都可能遇到。关键是:出错时截图。
except Exception as e:
await page.screenshot(path=f"{platform}-error-{int(time.time())}.png")
print(f"❌ 失败: {e}")
一张截图胜过十行日志。
八、进阶方向
基础版跑通之后,还可以往几个方向扩展:
- Markdown → 富文本转换:目前是纯文本输入,如果文章有代码块、图片、链接,需要做格式转换。知乎支持 Markdown 导入,可以利用这个功能
- 图片自动处理:小红书对图片尺寸有要求(推荐 3:4),可以用 Pillow 自动裁剪
- 定时发布:结合 cron 或 APScheduler,设定发布时间
- 发布结果通知:发完之后推送到飞书/微信,附上文章链接
- 更多平台:公众号、掘金、CSDN……套路都一样,录制 → 改写 → 封装
九、一句话总结
整个方案的核心就一句话:
playwright codegen录制操作 → 提取关键步骤 → 加上错误处理和延迟 → 封装成脚本。
选择器变了就重新录,比硬猜 DOM 结构靠谱得多。
说到底,程序员写博客已经够费脑子了,发布这种机械劳动,就该交给机器。省下来的时间,多写一段代码,多看一篇论文,或者——多睡一会儿,都比复制粘贴有意义。