1. 项目概述:为什么需要一个小说自动化发布工具?
写小说,尤其是连载小说,最怕什么?不是没灵感,而是日复一日、枯燥繁琐的发布流程。每天打开浏览器,登录后台,复制粘贴章节内容,设置标题、分卷,检查格式,点击发布……这套动作重复几十上百次,不仅消磨创作热情,更挤占了宝贵的构思和写作时间。对于在番茄小说这类平台上进行连载的作者而言,保持稳定更新是吸引和留住读者的关键,但手动操作的低效和易错性,成了许多创作者的隐形负担。
这正是我动手设计并实现这个“番茄小说自动化发布工具”的初衷。它不是一个简单的“按键精灵”脚本,而是一个基于现代浏览器自动化框架 Playwright 构建的、能够模拟真人操作、稳定处理复杂网页交互的智能助手。其核心目标就一个: 将作者从重复的机械劳动中解放出来,让创作回归创作本身 。你只需要专注于写好每一章的内容,将整理好的文本文件交给这个工具,它就能帮你完成从登录到发布的全流程,甚至能处理一些常见的异常情况,比如网络波动导致的发布失败、章节内容格式校验等。
这个工具适合谁?首先是像我一样,在番茄小说进行多部作品连载的“肝帝”型作者。其次,是那些希望将作品同步发布到多个平台,寻求效率最大化的创作者。最后,对于任何想要学习如何将 Playwright 这类强大的自动化技术应用于实际生活与工作场景,解决具体痛点的开发者来说,这个项目也是一个绝佳的实战案例。它涉及了网页自动化、状态保持、文件处理、错误重试等一整套工程化思维,价值远超一个简单的脚本。
2. 核心设计思路与技术选型
2.1 为什么是 Playwright 而不是 Selenium?
在决定做这个工具时,第一个面临的选择就是自动化框架。Selenium 是老牌王者,生态成熟,但 Playwright 作为后起之秀,在多个维度上展现出了更贴合我们需求的特质。
第一,无头浏览器(Headless Browser)的稳定与高效。 Playwright 为 Chromium、Firefox 和 WebKit 都提供了高质量的原生支持,其无头模式运行极其稳定,渲染速度快,且对现代 Web 技术(如单页应用 SPA)的支持更好。番茄小说的作者后台正是一个典型的复杂单页应用,大量操作依赖前端 JavaScript 动态加载和交互。Playwright 能够更可靠地等待页面元素加载完成,减少了因页面状态未就绪而导致的脚本失败。
第二,强大的自动等待(Auto-waiting)机制。
这是 Playwright 对比 Selenium 的一个巨大优势。在 Selenium 中,我们经常需要手动编写
WebDriverWait
来等待某个元素出现、可点击或可见,代码冗长且易出错。Playwright 的大多数操作(如
click
,
fill
,
type
)内置了智能等待:它会等待元素可交互(例如,按钮不再被禁用,输入框可见且可编辑)后再执行操作。这大大简化了代码,提升了脚本的健壮性。在发布章节时,我们需要等待富文本编辑器加载完成、等待“发布”按钮变为可点击状态,Playwright 的自动等待让这些逻辑变得异常简洁。
第三,网络拦截与模拟(Network Interception)。 Playwright 可以轻松监听和修改网络请求。虽然在这个发布工具的第一版中我们可能用不到太复杂的网络操控,但这个特性为未来可能的扩展留下了空间,比如自动处理图片上传、监控发布是否成功的 API 请求等。
第四,跨平台与易于部署。
Playwright 通过
playwright install
命令可以一键安装所需的浏览器二进制文件,部署非常方便。无论是 Windows、macOS 还是 Linux 服务器,都能快速搭建环境。这对于希望将工具部署到云服务器实现定时自动发布的作者来说,是个福音。
基于以上几点,Playwright 在开发效率、运行稳定性和面向未来的扩展性上,都成为了不二之选。
2.2 工具整体架构设计
这个自动化发布工具不是一个庞然大物,它的核心架构清晰而紧凑,主要分为四个模块:
- 配置与初始化模块 :负责读取用户配置(如账号密码、作品ID、本地章节文件路径等),初始化 Playwright 浏览器实例,并设置必要的上下文(如视窗大小、是否无头运行、忽略 HTTPS 错误等)。
- 身份认证与会话管理模块 :这是工具的“钥匙”。它需要安全地登录番茄小说作者后台,并妥善管理登录状态(Cookies)。为了避免每次运行都重新登录,我们会实现会话持久化功能,将登录后的 Cookies 保存到本地文件,下次启动时直接加载,既安全又高效。
- 内容解析与发布模块 :这是工具的“核心引擎”。它需要读取本地的章节文件(通常是 Markdown 或纯文本),解析出章节标题和正文内容。然后,导航到目标作品的发布页面,将内容填充到网页编辑器中,并模拟点击发布按钮。这里需要精细处理富文本编辑器的交互,可能涉及模拟键盘输入、处理文件上传(如章节封面图)等。
- 流程控制与异常处理模块 :这是工具的“安全网”。它需要控制发布的节奏(例如,章节间发布间隔),监控每个步骤的执行状态。一旦遇到网络超时、元素找不到、发布失败等异常,工具应能进行重试、记录错误日志,并在可能的情况下跳过当前章节继续后续任务,而不是整体崩溃。
整个工具的数据流是线性的:读取配置 -> 登录/恢复会话 -> 遍历章节文件 -> 逐个发布 -> 生成报告。但通过异常处理模块,这条流水线具备了容错能力。
注意 :在设计之初就必须考虑道德和法律边界。这个工具的目的是提升创作者个人的工作效率, 严禁 用于恶意刷量、攻击平台、干扰正常服务或任何违反番茄小说用户协议的行为。使用时请务必遵守平台规则,合理控制请求频率,避免对服务器造成不必要的压力。
3. 实战开发:从零构建发布工具
3.1 环境准备与基础依赖安装
工欲善其事,必先利其器。我们选择 Python 作为开发语言,因为它语法简洁,生态丰富,Playwright 对 Python 的支持也非常完善。
首先,创建一个新的项目目录,并初始化虚拟环境,这是保持项目依赖隔离的好习惯。
mkdir tomato-novel-publisher
cd tomato-novel-publisher
python -m venv venv # Windows 使用 `venv\Scripts\activate` 激活
source venv/bin/activate # macOS/Linux 激活虚拟环境
接着,安装核心依赖 Playwright for Python。
pip install playwright
安装完成后,需要安装 Playwright 所需的浏览器驱动。这里我们选择 Chromium,因为它与 Chrome 兼容性好,且是 Playwright 支持最全面的。
playwright install chromium
为了更方便地管理配置和记录日志,我们还可以安装两个辅助库:
pip install python-dotenv # 用于从 .env 文件加载敏感配置(如密码)
pip install loguru # 更美观、功能更强大的日志库
现在,基本的开发环境就搭建好了。你的项目根目录下应该有一个
venv
文件夹和一个
requirements.txt
(可以通过
pip freeze > requirements.txt
生成)。
3.2 实现安全的登录与会话持久化
登录是自动化的第一步,也是最容易出问题的一步。番茄小说后台的登录页面可能有验证码、动态加载等问题。我们的策略是:首次手动登录,保存会话;后续使用保存的会话恢复登录状态。
首先,创建一个
config.py
文件来管理配置,并使用
.env
文件存储敏感信息。
.env
文件 (切记加入
.gitignore
):
TOMATO_USERNAME=your_phone_number
TOMATO_PASSWORD=your_password
config.py
文件:
import os
from pathlib import Path
from dotenv import load_dotenv
load_dotenv() # 加载 .env 文件中的环境变量
class Config:
# 基础路径
BASE_DIR = Path(__file__).parent
COOKIES_FILE = BASE_DIR / "cookies.json"
CHAPTERS_DIR = BASE_DIR / "chapters"
# 账号信息(从环境变量读取)
USERNAME = os.getenv("TOMATO_USERNAME")
PASSWORD = os.getenv("TOMATO_PASSWORD")
# 作品ID(需要从番茄后台URL中获取)
BOOK_ID = "123456789" # 替换为你的作品ID
# 浏览器配置
HEADLESS = False # 开发时设为 False 方便调试,生产环境可设为 True
VIEWPORT = {"width": 1920, "height": 1080}
SLOW_MO = 100 # 操作延迟毫秒数,调试时有用,生产时可设为 0 或较小值
# 发布间隔(秒),避免请求过快
PUBLISH_INTERVAL = 5
config = Config()
接下来,创建核心的
publisher.py
,实现登录类。
import json
import asyncio
from loguru import logger
from playwright.async_api import async_playwright, BrowserContext
from config import config
class TomatoPublisher:
def __init__(self):
self.context: BrowserContext = None
self.browser = None
self.page = None
async def __aenter__(self):
"""异步上下文管理器入口,初始化浏览器"""
self.playwright = await async_playwright().start()
# 启动浏览器,使用 persistent context 有助于会话管理
self.browser = await self.playwright.chromium.launch(
headless=config.HEADLESS,
slow_mo=config.SLOW_MO
)
# 尝试从文件加载 cookies
if config.COOKIES_FILE.exists():
logger.info(f"尝试从 {config.COOKIES_FILE} 加载 cookies...")
self.context = await self.browser.new_context(
viewport=config.VIEWPORT,
storage_state=config.COOKIES_FILE # 关键:加载持久化状态
)
else:
self.context = await self.browser.new_context(viewport=config.VIEWPORT)
self.page = await self.context.new_page()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""异步上下文管理器出口,关闭资源"""
if self.browser:
await self.browser.close()
await self.playwright.stop()
async def login_if_needed(self):
"""检查当前会话是否有效,如果无效则执行登录"""
# 导航到作者后台首页,这是一个需要登录才能访问的页面
await self.page.goto("https://author.tomatolearning.com/")
# 通过判断页面是否跳转到登录页,或者是否存在用户信息元素,来判断登录状态
# 这里以检查页面标题或特定元素为例
try:
# 假设登录后页面会有“创作中心”之类的字样
await self.page.wait_for_selector("text=创作中心", timeout=5000)
logger.success("会话有效,已处于登录状态。")
return True
except Exception as e:
logger.warning("会话已失效或未登录,开始执行登录流程...")
return await self._perform_login()
async def _perform_login(self):
"""执行实际的登录操作"""
login_url = "https://passport.tomatolearning.com/login" # 示例,实际URL需确认
await self.page.goto(login_url)
# 等待登录表单加载
await self.page.wait_for_selector("input[type='text'], input[type='tel']", timeout=10000)
# 填充账号密码
# 注意:选择器需要根据番茄小说实际登录页面的HTML结构进行调整
await self.page.fill('input[placeholder="手机号"]', config.USERNAME)
await self.page.fill('input[type="password"]', config.PASSWORD)
# 点击登录按钮
login_btn = self.page.locator('button:has-text("登录")').first
await login_btn.click()
# 等待登录成功,跳转到作者后台
try:
await self.page.wait_for_url(https://blog.csdn.net/"**/author/**",%20timeout=15000)
logger.success("登录成功!")
# 登录成功后,立即保存 cookies 到文件
await self.context.storage_state(path=config.COOKIES_FILE)
logger.info(f"会话已保存至 {config.COOKIES_FILE}")
return True
except Exception as e:
logger.error(f"登录失败或超时: {e}")
# 可以在这里截图以便调试
await self.page.screenshot(path="login_error.png")
return False
关键点解析与避坑指南:
-
使用异步 (
async/await) : Playwright 推荐使用异步 API 以获得最佳性能。我们整个工具都采用异步编程模型。 -
BrowserContext与storage_state: 这是实现会话持久化的核心。BrowserContext代表一个独立的浏览器会话环境,其storage_state()方法可以导出当前上下文的所有 Cookies、LocalStorage 等数据。反之,在创建new_context时传入storage_state参数,即可还原整个会话状态。这比手动处理 Cookie 文件更可靠。 -
选择器策略
: 登录页面的元素选择器必须准确。建议使用 Playwright 的代码生成器(
playwright codegen)来录制登录操作,快速获取可靠的选择器。优先使用text=或role定位,它们比复杂的 CSS 选择器更稳定。 -
等待策略
:
wait_for_selector,wait_for_url是确保页面状态就绪的关键。结合 Playwright 的自动等待,可以构建出健壮的操作链。 - 错误处理与调试 : 登录环节最容易遇到验证码或动态安全策略。我们的代码中加入了超时控制和错误截图。如果遇到无法绕过的验证码,可能需要考虑半自动化方案(如工具暂停,提示用户手动完成验证后再继续)。
3.3 章节内容解析与发布流程实现
登录问题解决后,就进入了核心的发布环节。我们假设章节文件以特定的格式存储在本地
chapters
目录下,例如
001_第一章标题.txt
,文件内容第一行为标题,空一行后是正文。
首先,实现一个简单的章节文件解析器。
# file_parser.py
import re
from pathlib import Path
from typing import List, Tuple
from loguru import logger
class ChapterParser:
@staticmethod
def parse_chapter_file(file_path: Path) -> Tuple[str, str]:
"""解析章节文件,返回(标题, 正文)元组"""
try:
content = file_path.read_text(encoding='utf-8')
lines = content.strip().split('\n')
if len(lines) < 2:
raise ValueError(f"文件 {file_path.name} 内容格式错误,至少需要标题和正文。")
title = lines[0].strip()
# 假设标题行之后有一个空行,然后全是正文
body_lines = []
found_empty_line = False
for line in lines[1:]:
if not found_empty_line and line.strip() == "":
found_empty_line = True
continue
if found_empty_line:
body_lines.append(line.rstrip()) # 保留行尾空格可能影响格式
body = '\n'.join(body_lines).strip()
if not body:
raise ValueError(f"文件 {file_path.name} 正文内容为空。")
logger.debug(f"解析文件成功: {file_path.name} -> 标题: {title[:20]}...")
return title, body
except Exception as e:
logger.error(f"解析章节文件 {file_path} 时出错: {e}")
raise
@staticmethod
def get_chapter_files(chapters_dir: Path) -> List[Path]:
"""获取指定目录下所有的章节文件,并按文件名排序"""
if not chapters_dir.exists():
raise FileNotFoundError(f"章节目录不存在: {chapters_dir}")
# 假设文件名为 数字序号_标题.txt 的格式
files = list(chapters_dir.glob("*.txt"))
# 按文件名中的数字部分排序
files.sort(key=lambda x: int(re.search(r'^(\d+)', x.stem).group(1)) if re.search(r'^(\d+)', x.stem) else 0)
logger.info(f"在目录 {chapters_dir} 中找到 {len(files)} 个章节文件。")
return files
接下来,在
TomatoPublisher
类中添加发布章节的核心方法。
# 在 publisher.py 的 TomatoPublisher 类中继续添加
async def publish_chapter(self, book_id: str, title: str, content: str):
"""发布单个章节到指定作品"""
logger.info(f"开始发布章节: 《{title}》")
# 1. 导航到作品发布页面
publish_url = f"https://author.tomatolearning.com/books/{book_id}/chapters/new" # 示例URL
await self.page.goto(publish_url)
# 2. 等待页面关键元素加载完成
# 通常包括章节标题输入框和正文编辑器
try:
await self.page.wait_for_selector('input[placeholder="请输入章节标题"]', timeout=10000)
# 富文本编辑器可能是一个 iframe 或 contenteditable div,需要特殊处理
# 这里假设编辑器是一个 contenteditable 的 div
await self.page.wait_for_selector('div[contenteditable="true"]', timeout=10000)
except Exception as e:
logger.error(f"发布页面元素加载超时: {e}")
await self.page.screenshot(path=f"publish_page_error_{title[:10]}.png")
raise
# 3. 填写章节标题
await self.page.fill('input[placeholder="请输入章节标题"]', title)
# 4. 填写章节正文 - 这是最复杂的一步
editor_selector = 'div[contenteditable="true"]'
editor = self.page.locator(editor_selector).first
# 方法A:直接设置 innerHTML (如果编辑器支持且简单)
# await editor.evaluate('(element, content) => element.innerHTML = `<p>${content.replace(/\\n/g, "</p><p>")}</p>`', content)
# 方法B:模拟键盘输入(更贴近真人操作,但较慢)
await editor.click() # 聚焦编辑器
await self.page.keyboard.press("Control+A") # 全选(清除可能存在的默认提示文字)
await self.page.keyboard.press("Delete")
# 将内容按段落分割后输入
paragraphs = content.split('\n')
for i, para in enumerate(paragraphs):
if para.strip(): # 非空段落
await self.page.keyboard.type(para)
if i < len(paragraphs) - 1: # 不是最后一段,则按回车换行
await self.page.keyboard.press("Enter")
logger.debug("章节正文填充完成。")
# 5. (可选)设置分卷、封面图等
# 这里需要根据番茄后台的实际界面进行适配,可能涉及点击下拉框、上传文件等操作。
# 示例:选择分卷
# await self.page.click('text=选择分卷')
# await self.page.click('li:has-text("第一卷")')
# 6. 点击发布按钮
publish_button = self.page.locator('button:has-text("发布"), button:has-text("确认发布")').first
# 等待按钮变为可点击状态(Playwright 的 click 已内置此等待)
await publish_button.click()
# 7. 等待发布成功反馈
try:
# 等待成功提示出现,或者页面跳转
await self.page.wait_for_selector('text=发布成功', timeout=10000)
# 或者等待页面URL变化
# await self.page.wait_for_url(https://blog.csdn.net/"**/chapters/**",%20timeout=10000)
logger.success(f"章节《{title}》发布成功!")
return True
except Exception as e:
logger.error(f"章节《{title}》发布后未检测到成功状态: {e}")
# 截图以便排查
await self.page.screenshot(path=f"publish_fail_{title[:10]}.png")
# 检查是否有错误提示
error_msg = await self.page.locator('.error-message, .ant-message-error').text_content(timeout=2000)
if error_msg:
logger.error(f"页面错误提示: {error_msg}")
return False
关键点解析与避坑指南:
-
富文本编辑器交互
: 这是最大的挑战。不同平台的富文本编辑器实现千差万别。上述代码提供了两种思路:直接注入 HTML(快,但可能被过滤或触发异常)和模拟键盘输入(慢,但最接近真人操作)。
必须通过实际测试确定哪种方法有效
。使用
playwright codegen录制一次手动发布操作,是分析编辑器交互逻辑的最佳方式。 - 等待与稳定性 : 在点击“发布”按钮后,一定要等待明确的成功信号(如成功提示弹窗、页面跳转)。不要假设点击完就万事大吉,网络延迟或后端处理可能导致发布实际未成功。
- 错误恢复 : 发布失败后,工具记录了错误截图和可能的错误信息。在实际的批量发布中,我们可能希望将失败的章节记录到一个列表中,稍后重试,而不是让整个流程中断。
-
节奏控制
: 在
config中我们设置了PUBLISH_INTERVAL。在批量发布循环中,每发布一章后使用await asyncio.sleep(config.PUBLISH_INTERVAL)进行等待,是对平台友好的做法,避免被误判为恶意请求。
3.4 主流程整合与批量发布
最后,我们将所有模块整合起来,实现一个完整的、可处理批量章节发布的
main
函数。
# main.py
import asyncio
from pathlib import Path
from loguru import logger
from publisher import TomatoPublisher
from file_parser import ChapterParser
from config import config
async def main():
"""主发布流程"""
# 初始化日志
logger.add("publish.log", rotation="10 MB", level="INFO")
# 1. 检查配置
if not config.USERNAME or not config.PASSWORD:
logger.error("请在 .env 文件中配置 TOMATO_USERNAME 和 TOMATO_PASSWORD。")
return
if not config.BOOK_ID or config.BOOK_ID == "123456789":
logger.error("请在 config.py 中配置正确的 BOOK_ID。")
return
# 2. 获取章节文件列表
try:
chapter_files = ChapterParser.get_chapter_files(config.CHAPTERS_DIR)
except FileNotFoundError as e:
logger.error(e)
return
if not chapter_files:
logger.warning("未找到任何章节文件,程序退出。")
return
# 3. 使用异步上下文管理器启动浏览器和发布器
async with TomatoPublisher() as publisher:
# 4. 登录或恢复会话
login_success = await publisher.login_if_needed()
if not login_success:
logger.critical("登录失败,无法继续发布流程。")
return
# 5. 遍历并发布章节
success_count = 0
fail_list = []
for idx, chap_file in enumerate(chapter_files, start=1):
logger.info(f"处理进度: [{idx}/{len(chapter_files)}] - {chap_file.name}")
try:
# 解析章节
title, content = ChapterParser.parse_chapter_file(chap_file)
# 发布章节
result = await publisher.publish_chapter(config.BOOK_ID, title, content)
if result:
success_count += 1
logger.info(f"成功发布: {title}")
else:
fail_list.append((chap_file.name, title))
logger.error(f"发布失败: {title}")
except Exception as e:
fail_list.append((chap_file.name, str(e)))
logger.exception(f"处理文件 {chap_file.name} 时发生未预期错误: {e}")
# 发布间隔,避免请求过快
if idx < len(chapter_files):
await asyncio.sleep(config.PUBLISH_INTERVAL)
# 6. 发布结果汇总
logger.info("="*50)
logger.info(f"批量发布完成!")
logger.info(f"总计章节: {len(chapter_files)}")
logger.info(f"成功发布: {success_count}")
logger.info(f"失败章节: {len(fail_list)}")
if fail_list:
logger.info("失败详情:")
for file_name, reason in fail_list:
logger.info(f" - {file_name}: {reason}")
logger.info("="*50)
if __name__ == "__main__":
asyncio.run(main())
至此,一个具备基础功能的番茄小说自动化发布工具就完成了。你可以通过运行
python main.py
来启动它。首次运行会弹出浏览器窗口让你手动登录,之后便会自动保存会话,后续运行可实现全自动发布。
4. 进阶优化与异常处理实战
一个能用的工具和一个好用的工具之间,差的就是细节处理和健壮性。在实际使用中,你肯定会遇到各种意想不到的问题。下面分享几个我踩过坑后总结的进阶优化点。
4.1 增强的富文本编辑器处理策略
上述的编辑器填充方法(模拟键盘输入)虽然稳定,但速度慢,且对于超长章节(几万字)可能不稳定。我们可以采用混合策略,并增加容错。
async def _fill_editor_robust(self, content: str, max_retries: int = 2):
"""更健壮的编辑器填充方法"""
editor = self.page.locator('div[contenteditable="true"]').first
for attempt in range(max_retries):
try:
await editor.click()
await self.page.keyboard.press("Control+A")
await self.page.keyboard.press("Delete")
await asyncio.sleep(0.5) # 给编辑器一点反应时间
# 尝试快速注入(如果编辑器允许)
if attempt == 0:
try:
# 尝试用 evaluate 直接设置文本内容(非HTML)
await editor.evaluate('(el, txt) => el.textContent = txt', content)
# 触发一次 input 事件,让编辑器知道内容变了
await editor.evaluate('el => el.dispatchEvent(new Event("input", { bubbles: true }))')
await asyncio.sleep(1)
# 检查内容是否成功注入
actual_content = await editor.text_content()
if actual_content and len(actual_content) > len(content) * 0.8: # 粗略检查
logger.debug("使用快速文本注入成功。")
return True
except Exception as e:
logger.debug(f"快速注入失败,回退到模拟输入: {e}")
# 回退方案:分段模拟输入
logger.debug("使用分段模拟键盘输入。")
# 将内容分成小块,避免单次操作过长
chunks = [content[i:i+500] for i in range(0, len(content), 500)]
for chunk in chunks:
await self.page.keyboard.type(chunk)
await asyncio.sleep(0.05) # 小块之间的微小延迟
return True
except Exception as e:
logger.warning(f"编辑器填充第{attempt+1}次尝试失败: {e}")
if attempt < max_retries - 1:
await asyncio.sleep(2)
# 可以尝试重新聚焦编辑器
await editor.click({'force': True})
else:
raise Exception(f"编辑器填充失败,已重试{max_retries}次。")
4.2 网络异常与元素失联的重试机制
网络不稳定或页面动态加载可能导致元素定位失败。我们需要一个带有重试的通用操作包装器。
async def retry_operation(self, operation, *args, max_attempts=3, delay=2, **kwargs):
"""重试装饰器,用于包装可能失败的操作"""
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return await operation(*args, **kwargs)
except Exception as e:
last_exception = e
logger.warning(f"操作第 {attempt} 次尝试失败: {e}")
if attempt < max_attempts:
logger.info(f"{delay}秒后重试...")
await asyncio.sleep(delay)
else:
logger.error(f"操作在 {max_attempts} 次尝试后均失败。")
raise last_exception
在发布章节时,可以这样使用:
# 在 publish_chapter 方法中,替换原有的 goto 和 wait_for_selector
await self.retry_operation(self.page.goto, publish_url, timeout=15000)
await self.retry_operation(self.page.wait_for_selector, 'input[placeholder="请输入章节标题"]', timeout=10000)
4.3 发布状态的双重校验
仅仅等待“发布成功”的提示可能不够,因为提示可能一闪而过,或者页面有延迟。更可靠的做法是结合多种信号进行校验。
async def _verify_publish_success(self, chapter_title: str) -> bool:
"""验证章节是否发布成功"""
verification_passed = False
# 信号1:等待成功提示文本
try:
await self.page.wait_for_selector('text=发布成功', timeout=8000)
logger.debug("检测到‘发布成功’提示。")
verification_passed = True
except:
logger.debug("未检测到明确的‘发布成功’文本提示。")
# 信号2:检查URL是否跳转到章节列表页或编辑页
current_url = self.page.url
if "/chapters/" in current_url and "/new" not in current_url:
logger.debug(f"URL跳转成功: {current_url}")
verification_passed = True
# 信号3:(可选)检查页面中是否出现刚发布的章节标题
# 这可能需要导航到章节列表页,实现较复杂,可作为备选
if not verification_passed:
# 最终手段:短暂等待后,尝试获取任何错误信息
await asyncio.sleep(3)
error_elements = await self.page.locator('.ant-alert-error, .error-text, [class*="error"], [class*="fail"]').all()
for elem in error_elements:
error_text = await elem.text_content()
if error_text and len(error_text) < 100: # 避免过长文本
logger.error(f"页面显示错误: {error_text}")
return False
# 如果既无成功信号,也无错误信号,我们倾向于认为可能成功了(但记录警告)
logger.warning(f"章节《{chapter_title}》发布状态不明确,请手动确认。")
return True # 或 False,取决于你的策略
return verification_passed
然后在
publish_chapter
方法的最后,用
_verify_publish_success(title)
替换简单的
wait_for_selector
。
4.4 配置文件与命令行参数增强
让工具更易用,可以支持命令行参数来覆盖配置文件。
# config.py 补充
import argparse
def update_config_from_cli():
parser = argparse.ArgumentParser(description='番茄小说自动化发布工具')
parser.add_argument('--book-id', help='指定作品ID,覆盖配置文件')
parser.add_argument('--headless', action='store_true', help='以无头模式运行浏览器')
parser.add_argument('--dir', help='指定章节文件目录,覆盖配置文件')
parser.add_argument('--start-from', type=int, default=1, help='从第几个章节文件开始发布(基于排序后)')
parser.add_argument('--dry-run', action='store_true', help='试运行,填充内容但不点击发布按钮')
args = parser.parse_args()
if args.book_id:
config.BOOK_ID = args.book_id
if args.headless:
config.HEADLESS = True
if args.dir:
config.CHAPTERS_DIR = Path(args.dir)
config.START_FROM = args.start_from
config.DRY_RUN = args.dry_run
在
main.py
中调用
update_config_from_cli()
,并在发布循环中加入判断:
if config.DRY_RUN:
logger.warning("*** 试运行模式已开启,不会实际点击发布按钮!***")
# 在发布函数内部
if not config.DRY_RUN:
await publish_button.click()
else:
logger.info(f"[试运行] 模拟点击发布按钮,章节《{title}》")
await asyncio.sleep(1) # 模拟等待时间
5. 部署与持续运行建议
开发完成后,你可能希望工具能定时自动运行,比如每天凌晨自动发布存稿。
方案一:本地定时任务(Windows 任务计划 / macOS/Linux crontab)
这是最简单的方式。创建一个批处理脚本(
.bat
或
.sh
),激活虚拟环境并运行
python main.py
,然后让系统定时执行这个脚本。
- 缺点 :电脑需要一直开机。
方案二:云服务器部署
购买一台低配的云服务器(如腾讯云、阿里云的轻量应用服务器),将代码部署上去。在服务器上同样使用
crontab
设置定时任务。
-
关键点
:
- 在服务器上安装必要的依赖(包括 Chromium)。
-
首次运行需要在有图形界面的环境下完成登录(或者使用
xvfb等虚拟显示框架在无头环境下处理可能的登录验证码)。 -
将保存了有效 Cookies 的
cookies.json文件备份好,服务器重启或失效后可以手动恢复。
方案三:容器化部署(Docker) 这是更优雅和可移植的方案。创建一个 Dockerfile,包含 Python 环境、Playwright 及其浏览器依赖。将代码、配置和持久化的 Cookies 文件通过卷(Volume)挂载到容器中。然后使用 Docker 的定时任务工具或配合 Kubernetes CronJob 来运行。
- 优势 :环境隔离,一次构建,随处运行。
- 挑战 :需要一定的 Docker 知识,且处理无头浏览器在容器中的运行可能需要额外的系统依赖。
重要安全提醒 :无论采用哪种部署方式,务必妥善保管你的
.env文件和cookies.json文件。它们包含了你的账号敏感信息。切勿将其提交到公开的代码仓库(如 GitHub)。务必在.gitignore中添加.env,cookies.json,*.png(截图),publish.log等条目。
这个基于 Playwright 的自动化发布工具,从构思到实现,再到不断优化以应对真实场景中的各种“坑”,整个过程本身就是一次宝贵的全栈工程实践。它不仅仅帮你节省了时间,更让你深入理解了浏览器自动化的精髓——如何让代码像人一样与复杂的 Web 应用交互,同时保持比人更高的准确性和不知疲倦的稳定性。希望这个详细的实战记录,能为你自己的自动化之路提供一个坚实的起点。
171

被折叠的 条评论
为什么被折叠?



