十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Python的Substack非官方API自动化发布实战指南

基于Python的Substack非官方API自动化发布实战指南 最近在尝试自动化内容发布流程时发现 Substack 作为流行的邮件订阅和内容发布平台其官方并未提供完整的公开 API。这对于希望通过脚本批量发布文章、同步内容或集成到其他工作流的开发者来说是个不小的障碍。为了解决这个问题社区中出现了不少非官方的 API 封装项目。本文将深入探讨一个基于 Python 的 Substack 非官方 API 库的更新版本并手把手教你如何利用它结合 CLI 工具和 MCP 协议等现代开发理念构建一个属于自己的自动化内容发布管道。无论你是想批量管理多个 Substack 专栏还是希望将写作流程与你的代码仓库或 AI 工具链集成这篇文章都将提供从环境搭建、核心 API 调用到实战项目的一站式解决方案。1. Substack 非官方 API背景、价值与挑战1.1 Substack 平台与开发者的需求鸿沟Substack 以其简洁的邮件订阅模式和创作者友好的分成机制吸引了大量写作者。然而其产品设计更侧重于前端交互和邮件投递后台的创作和管理功能相对基础。对于技术背景的创作者或希望进行规模化运营的团队以下痛点尤为明显批量操作困难无法通过程序批量创建、编辑或发布文章。集成能力弱难以将 Substack 与现有的 CMS、数据分析工具或自动化工作流如 GitHub Actions, Zapier 的高级用法无缝连接。数据导出与备份虽然可以导出订阅者列表但文章内容的批量导出和管理缺乏官方工具支持。协作流程割裂团队协作撰写、审核、发布的流程无法与开发者熟悉的 Git、CI/CD 等工具结合。这些痛点催生了社区对 Substack API 的强烈需求。虽然 Substack 官方没有公开 RESTful API但其网页端的所有操作最终都通过其内部 API 完成。这就为“非官方 API”的实现提供了可能性——通过模拟浏览器行为网络请求来与后端服务器交互。1.2 非官方 API 的工作原理与风险提示非官方 API 库如本文讨论的项目的核心原理是“逆向工程”。开发者通过浏览器开发者工具DevTools的 Network 面板观察在 Substack 网站上执行操作如登录、发布文章时浏览器向服务器发送了哪些 HTTP 请求。这些请求通常包括端点Endpoint如https://substack.com/api/v1/post。请求方法Method如POST创建、PUT更新、GET获取。请求头Headers包含关键的Authorization认证令牌、Cookie或X-CSRF-Token等。请求体Body通常是 JSON 格式包含了文章的标题、内容、发布时间等信息。非官方 API 库将这些观察到的请求模式封装成友好的函数让开发者可以用几行代码完成复杂的操作。重要风险与合规性声明违反服务条款使用非官方 API 可能违反 Substack 的服务条款。Substack 有权检测并封禁他们认为异常的自动化行为。稳定性风险Substack 的内部 API 随时可能变更导致非官方库失效。安全风险需要处理登录凭证邮箱、密码。务必使用环境变量管理敏感信息切勿将密码硬编码在代码中。道德与法律仅将此技术用于管理你自己拥有的 Substack 出版物。切勿用于爬取他人数据、发送垃圾信息或进行任何破坏性操作。在充分了解并接受上述风险后我们可以开始技术探索。2. 环境准备与核心工具栈为了完整演示从 API 调用到构建 CLI 工具的流程我们需要准备以下环境。2.1 Python 环境与依赖安装本文示例基于 Python 3.8。首先确保你的 Python 环境已就绪。# 检查 Python 版本 python --version # 或 python3 --version接下来创建一个新的项目目录并初始化虚拟环境这是管理项目依赖的最佳实践。# 创建项目目录 mkdir substack-automation cd substack-automation # 创建虚拟环境以 venv 为例 python -m venv venv # 激活虚拟环境 # Windows (PowerShell) venv\Scripts\Activate.ps1 # macOS/Linux source venv/bin/activate激活虚拟环境后命令行提示符前通常会出现(venv)标识。在这个隔离的环境中我们安装核心依赖。假设我们使用的非官方 API 库名为substack-api这是一个示例名称实际库名可能不同如substack-py我们还需要requests库来处理 HTTP 请求python-dotenv来管理环境变量。# 安装核心库请替换为实际可用的库名例如通过 pip install gitrepo_url # 此处以 requests 和 python-dotenv 为例假设 API 功能由我们自己封装 pip install requests python-dotenv # 可选用于构建更友好 CLI 的工具 pip install typer richrequests用于发送 HTTP 请求。python-dotenv用于从.env文件加载环境变量。typer一个强大的库用于构建命令行应用能自动生成帮助文档。rich让终端输出更美观支持表格、进度条等。2.2 获取并保护你的 Substack 凭证非官方 API 需要通过你的 Substack 账号进行认证。绝对不要将密码直接写在代码里。在项目根目录创建一个名为.env的文件。将你的 Substack 登录邮箱和密码或应用专用密码如果支持填入该文件。# .env 文件内容 SUBSTACK_EMAILyour_emailexample.com SUBSTACK_PASSWORDyour_secure_password_here SUBSTACK_PUBLICATION_URLhttps://yourpublication.substack.com至关重要将.env添加到你的.gitignore文件中确保它不会被提交到版本控制系统如 GitHub。# .gitignore .env *.pyc __pycache__/ venv/2.3 项目结构预览一个清晰的项目结构有助于维护。我们的示例项目结构如下substack-automation/ ├── .env # 环境变量保密不提交 ├── .gitignore # Git忽略文件 ├── requirements.txt # 项目依赖列表 ├── src/ │ ├── __init__.py │ ├── api_client.py # Substack API 客户端封装 │ ├── cli.py # 命令行接口主程序 │ └── models.py # 数据模型如 Post 类 ├── scripts/ │ └── publish_example.py # 示例发布脚本 └── README.md使用pip freeze requirements.txt生成依赖列表。3. 核心 API 客户端封装实战由于没有统一的“官方”非官方库我们将基于对 Substack 网络请求的分析自己封装一个简易但功能完整的客户端。这能让你更深刻地理解其工作原理。3.1 分析登录与认证流程这是最复杂也最关键的一步。打开浏览器进入 Substack 登录页打开开发者工具F12的 Network 面板勾选 “Preserve log”。完成一次手动登录观察网络请求。你可能会发现一个向https://substack.com/api/v1/login发送的POST请求。其请求体Payload可能包含email,password,captcha_response等。响应中可能包含一个 token 或设置了特定的 Cookie如substack.sid。注意Substack 可能使用基于 Cookie 的会话管理也可能使用 JWT Token。我们的客户端需要能够处理会话的持久化。3.2 构建ApiClient类我们创建一个src/api_client.py文件封装所有与 Substack 后端交互的逻辑。# src/api_client.py import os import json import logging from typing import Optional, Dict, Any from dotenv import load_dotenv import requests # 加载 .env 文件中的环境变量 load_dotenv() # 设置日志便于调试 logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) class SubstackApiClient: Substack 非官方 API 客户端 BASE_URL https://substack.com API_BASE f{BASE_URL}/api/v1 def __init__(self, email: str None, password: str None): 初始化客户端。 优先使用传入的参数其次使用环境变量。 self.email email or os.getenv(SUBSTACK_EMAIL) self.password password or os.getenv(SUBSTACK_PASSWORD) self.publication_url os.getenv(SUBSTACK_PUBLICATION_URL) if not self.email or not self.password: raise ValueError(必须提供 Substack 邮箱和密码通过参数或环境变量) self.session requests.Session() # 设置一个通用的请求头模拟浏览器 self.session.headers.update({ User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36, Accept: application/json, Content-Type: application/json, Origin: self.BASE_URL, Referer: f{self.BASE_URL}/, }) self._authenticated False def login(self) - bool: 执行登录获取并保存认证凭据如 Cookie。 这是最可能因网站改版而失效的部分。 login_url f{self.API_BASE}/login payload { email: self.email, password: self.password, # 注意实际请求可能需要更多字段如 redirect甚至需要先获取一个 CSRF token。 # 以下 payload 仅为示例需要根据实际网络请求调整。 for_publisher: False } try: logger.info(f尝试登录: {self.email}) # 禁用重定向以便观察中间响应 resp self.session.post(login_url, jsonpayload, allow_redirectsFalse) # 登录成功的标志可能是状态码 200 并返回用户信息或者是 302 重定向并设置了 Cookie if resp.status_code in [200, 302]: # 检查响应头中是否有设置 Cookie if set-cookie in resp.headers: logger.info(登录成功会话 Cookie 已设置。) # 尝试解析响应 JSON 获取用户 ID 等 try: user_data resp.json() logger.info(f登录成功用户: {user_data.get(user, {}).get(name)}) except: logger.info(登录成功响应非JSON。) self._authenticated True return True else: logger.error(f登录失败状态码: {resp.status_code}, 响应: {resp.text[:200]}) return False except requests.exceptions.RequestException as e: logger.error(f登录请求异常: {e}) return False def _ensure_auth(self): 确保客户端已认证未认证则尝试登录。 if not self._authenticated: if not self.login(): raise Exception(无法完成认证请检查凭证和网络。) def get_publication_id(self) - Optional[str]: 获取当前出版物的内部 ID。 许多 API 操作需要这个 ID。 self._ensure_auth() if not self.publication_url: raise ValueError(未设置 SUBSTACK_PUBLICATION_URL) # 解析出 publication slug # 例如从 https://techinsights.substack.com 解析出 techinsights slug self.publication_url.rstrip(/).split(/)[-1].replace(.substack.com, ) # 尝试通过 API 获取出版物信息 api_url f{self.API_BASE}/publication/slug/{slug} resp self.session.get(api_url) if resp.status_code 200: data resp.json() return data.get(id) else: logger.error(f获取出版物 ID 失败: {resp.status_code}) return None def create_draft(self, title: str, body: str, subtitle: str ) - Optional[Dict[str, Any]]: 创建一篇草稿。 Args: title: 文章标题 body: 文章正文HTML 或 Markdown取决于 Substack 编辑器 subtitle: 文章副标题可选 Returns: 创建的草稿数据字典或 None self._ensure_auth() pub_id self.get_publication_id() if not pub_id: logger.error(无法获取出版物 ID创建草稿失败。) return None create_url f{self.API_BASE}/post payload { publication_id: int(pub_id), # 注意类型转换 title: title, body: body, subtitle: subtitle, type: post, # 可能是 post, newsletter 等 status: draft, # 创建为草稿 } resp self.session.post(create_url, jsonpayload) if resp.status_code 201: # 201 Created 是常见成功状态码 logger.info(f草稿创建成功: {title}) return resp.json() else: logger.error(f创建草稿失败 {resp.status_code}: {resp.text}) return None def publish_post(self, post_id: str, publish_time: str None) - bool: 发布一篇已存在的草稿。 Args: post_id: 文章 ID从创建草稿的响应中获取 publish_time: 计划发布时间 (ISO 8601 格式)为 None 则立即发布 self._ensure_auth() update_url f{self.API_BASE}/post/{post_id} payload { status: published } if publish_time: payload[publish_at] publish_time resp self.session.put(update_url, jsonpayload) if resp.status_code 200: logger.info(f文章 {post_id} 发布成功) return True else: logger.error(f发布文章失败 {resp.status_code}: {resp.text}) return False def list_posts(self, limit: int 10) - Optional[list]: 获取出版物下的文章列表 self._ensure_auth() pub_id self.get_publication_id() if not pub_id: return None list_url f{self.API_BASE}/post params { publication_id: pub_id, limit: limit, offset: 0 } resp self.session.get(list_url, paramsparams) if resp.status_code 200: return resp.json().get(posts, []) else: logger.error(f获取文章列表失败: {resp.status_code}) return None关键点解析会话管理使用requests.Session()可以自动管理 Cookies模拟浏览器保持登录状态。错误处理对 HTTP 状态码进行判断并记录详细的日志便于调试。灵活性create_draft方法将文章状态设为draft这是一个安全的选择。你可以先创建草稿审核后再调用publish_post发布。可扩展性这个类很容易扩展添加update_draft,delete_post,get_analytics等方法。4. 构建命令行工具 (CLI)有了稳定的 API 客户端我们可以用typer构建一个易用的命令行工具将功能暴露给终端。4.1 创建 CLI 主程序创建src/cli.py文件。# src/cli.py import typer from rich.console import Console from rich.table import Table from datetime import datetime import sys import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from src.api_client import SubstackApiClient app typer.Typer(helpSubstack 自动化发布命令行工具) console Console() app.command() def login( email: str typer.Option(None, --email, -e, helpSubstack 邮箱), password: str typer.Option(None, --password, -p, helpSubstack 密码, hide_inputTrue), ): 测试登录并验证凭证 client SubstackApiClient(emailemail, passwordpassword) if client.login(): console.print([green]✓ 登录成功[/green]) pub_id client.get_publication_id() if pub_id: console.print(f[blue]出版物 ID: {pub_id}[/blue]) else: console.print([red]✗ 登录失败请检查凭证。[/red]) raise typer.Exit(code1) app.command() def create( title: str typer.Argument(..., help文章标题), body_file: str typer.Option(..., --body-file, -f, help包含文章正文的 Markdown/HTML 文件路径), subtitle: str typer.Option(, --subtitle, -s, help文章副标题), draft: bool typer.Option(True, --draft/--publish, help创建为草稿默认还是直接发布), ): 创建一篇新文章草稿或直接发布 try: with open(body_file, r, encodingutf-8) as f: body_content f.read() except FileNotFoundError: console.print(f[red]错误找不到文件 {body_file}[/red]) raise typer.Exit(code1) client SubstackApiClient() console.print(f正在创建文章: [bold]{title}[/bold]) # 创建草稿 result client.create_draft(titletitle, bodybody_content, subtitlesubtitle) if not result: console.print([red]创建草稿失败。[/red]) raise typer.Exit(code1) post_id result.get(id) console.print(f[green]✓ 草稿创建成功文章 ID: {post_id}[/green]) # 如果不是草稿则直接发布 if not draft: if client.publish_post(post_id): console.print([green]✓ 文章已发布[/green]) else: console.print([yellow]⚠ 草稿已创建但发布失败。请稍后手动发布。[/yellow]) app.command() def list_posts( limit: int typer.Option(10, --limit, -l, help显示的文章数量) ): 列出出版物下的最近文章 client SubstackApiClient() posts client.list_posts(limitlimit) if not posts: console.print([yellow]未获取到文章列表。[/yellow]) return table Table(titlef最近 {len(posts)} 篇文章) table.add_column(ID, stylecyan) table.add_column(标题, stylemagenta) table.add_column(状态, stylegreen) table.add_column(发布时间, styleblue) for post in posts: post_id str(post.get(id, )) title post.get(title, 无标题)[:50] ... if len(post.get(title, )) 50 else post.get(title, 无标题) status post.get(status, unknown) # 转换时间戳 pub_at post.get(published_at) if pub_at: pub_time datetime.fromtimestamp(pub_at/1000).strftime(%Y-%m-%d %H:%M) else: pub_time 未发布 table.add_row(post_id, title, status, pub_time) console.print(table) app.command() def publish( post_id: str typer.Argument(..., help要发布的草稿文章 ID), schedule: str typer.Option(None, --schedule, -s, help计划发布时间 (格式: YYYY-MM-DDTHH:MM:SS)), ): 发布一篇草稿 client SubstackApiClient() if client.publish_post(post_id, publish_timeschedule): console.print(f[green]✓ 文章 {post_id} 发布成功[/green]) else: console.print(f[red]✗ 发布文章 {post_id} 失败。[/red]) if __name__ __main__: app()4.2 使用 CLI 工具现在你可以在终端中使用这个工具了。首先确保你在项目根目录并且虚拟环境已激活。# 查看帮助 python -m src.cli --help # 测试登录 python -m src.cli login # 创建一篇草稿从 markdown 文件 python -m src.cli create 我的技术文章 --body-file ./content/my_post.md --subtitle 关于自动化的思考 # 创建并直接发布 python -m src.cli create 立即发布的文章 --body-file ./content/urgent.md --draft False # 列出最近5篇文章 python -m src.cli list-posts --limit 5 # 发布指定ID的草稿 python -m src.cli publish 123456789 # 定时发布 python -m src.cli publish 123456789 --schedule 2023-10-27T09:00:00这个 CLI 工具极大地简化了操作你可以将其集成到脚本或自动化流程中。5. 进阶集成与 MCP 和 AI 工作流结合现代开发中CLI 工具是连接不同服务的粘合剂。我们可以将 Substack CLI 与更广泛的生态系统集成。5.1 理解 MCP (Model Context Protocol)MCP模型上下文协议是一个新兴概念旨在为 AI 模型如 Claude、GPT提供访问工具、数据库和 API 的标准方式。虽然 Substack API 客户端本身不是 MCP 服务器但我们可以将其功能包装成 AI 可用的工具。思路是创建一个脚本接收自然语言指令如“在 Substack 上发布一篇关于 Python 装饰器的草稿”解析后调用我们的SubstackApiClient或 CLI。5.2 示例简易 AI 集成脚本创建一个scripts/ai_publisher.py作为概念验证。# scripts/ai_publisher.py import sys import os sys.path.append(os.path.join(os.path.dirname(__file__), ..)) from src.api_client import SubstackApiClient import argparse def main(): parser argparse.ArgumentParser(description通过自然语言指令发布到 Substack (示例)) parser.add_argument(--title, requiredTrue, help文章标题) parser.add_argument(--body, requiredTrue, help文章正文纯文本或简单Markdown) parser.add_argument(--action, choices[draft, publish], defaultdraft, help操作draft(保存草稿) 或 publish(直接发布)) args parser.parse_args() client SubstackApiClient() # 这里可以添加更复杂的正文解析或格式化逻辑 # 例如将简单的 Markdown 转换为 Substack 编辑器兼容的 HTML formatted_body args.body # 简化处理实际可能需要转换 result client.create_draft(titleargs.title, bodyformatted_body) if result: post_id result.get(id) print(f✅ 草稿创建成功ID: {post_id}) if args.action publish: if client.publish_post(post_id): print(✅ 文章已发布) else: print(⚠️ 草稿已保存但发布失败。) else: print(❌ 创建失败。) if __name__ __main__: main()这个脚本可以通过其他 AI 代理或自动化平台如n8n,Zapier的 Code Step或 GitHub Actions来调用。5.3 集成到 GitHub Actions 实现自动发布你可以将写作流程 Git 化每当向特定分支如main推送 Markdown 文件时自动发布到 Substack。创建.github/workflows/publish-to-substack.ymlname: Publish to Substack on: push: branches: [ main ] paths: - posts/**/*.md # 监控 posts 目录下的 markdown 文件 jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.10 - name: Install dependencies run: | pip install requests python-dotenv - name: Publish new post env: SUBSTACK_EMAIL: ${{ secrets.SUBSTACK_EMAIL }} SUBSTACK_PASSWORD: ${{ secrets.SUBSTACK_PASSWORD }} SUBSTACK_PUBLICATION_URL: ${{ secrets.SUBSTACK_PUBLICATION_URL }} run: | # 这里需要编写一个脚本解析推送的文件提取元数据如标题并调用 API # 以下是一个极其简化的示例逻辑 python scripts/auto_publisher.py在 GitHub 仓库的 Settings - Secrets and variables - Actions 中添加SUBSTACK_EMAIL,SUBSTACK_PASSWORD,SUBSTACK_PUBLICATION_URL三个 secrets。6. 常见问题与排查指南在使用非官方 API 时你会遇到各种问题。以下是一个排查清单。问题现象可能原因排查步骤与解决方案登录失败(401,403)1. 凭证错误。2. Substack 登录流程已更新如添加了 CAPTCHA。3. 请求头或载荷格式不正确。1. 手动在浏览器登录一次确认凭证有效。2. 使用浏览器开发者工具重新抓取最新的登录请求对比api_client.py中的login方法。检查 URL、请求头特别是Cookie,X-CSRF-Token、请求体是否一致。3. 考虑是否需要先访问登录页获取一个初始 Cookie 或 token。创建草稿失败(400,404)1. 出版物 ID 获取失败。2. 请求体 JSON 结构错误或缺少必要字段。3. 文章正文格式不被接受。1. 调用get_publication_id()并打印结果确认 ID 正确。2. 再次抓取浏览器创建草稿的请求仔细比对payload字典的每一个键值对。3. 尝试先发送一个非常简单的正文如ptest/p。Substack 可能要求 HTML 格式。发布失败(400,404)1. 文章 ID 错误。2. 文章状态不允许发布可能已是发布状态。3. 发布时间格式错误。1. 确认post_id来自创建草稿的成功响应。2. 先通过list_posts或网页后台查看文章状态。3.publish_time必须为 ISO 8601 格式的字符串。请求被限制或账号异常Substack 检测到自动化行为触发了风控。立即停止高频请求1. 大幅降低请求频率添加随机延迟如time.sleep(random.uniform(2,5))。2. 模拟更真实的用户行为如先访问几个页面再操作。3. 最安全的方式仅用于低频、个人用途。api error: 400 type must be in [enabled, disabled, auto]请求中包含了无效的type字段值。检查 API 请求负载找到type字段确保其值只能是enabled,disabled,auto中的一个。这可能是其他 API 端点如通知设置的错误。api error: 400 this models maximum context length is ...此错误通常与AI 模型 API如 DeepSeek, GPT相关与 Substack API 无关。确认你调用的 API 端点是否正确。如果你在集成 AI 生成内容需要确保发送给 AI 模型的文本长度在其上下文窗口限制内。7. 最佳实践与工程建议为了安全、稳定地使用非官方 API请遵循以下建议使用环境变量与 Secrets 管理这是铁律。永远不要在代码、日志或版本控制中暴露密码。实现请求重试与退避网络请求可能失败。使用tenacity或backoff库实现带指数退避的智能重试。import backoff import requests backoff.on_exception(backoff.expo, requests.exceptions.RequestException, max_tries3) def safe_api_call(url, session, payload): return session.post(url, jsonpayload)添加详尽的日志记录关键操作、请求 URL、状态码和响应摘要。这比print更专业便于生产环境调试。考虑使用structlog或loguru。进行彻底的异常处理API 客户端中的每个网络调用都应被try-except包裹并处理特定的异常如ConnectionError,Timeout,JSONDecodeError。编写单元测试至少是集成测试为你的ApiClient编写测试使用pytest和responses库来模拟网络请求确保核心逻辑正确。尊重平台与频率限制将你的自动化脚本视为一个“有礼貌的用户”。在操作之间添加延迟例如每次发布间隔至少几分钟避免在短时间内发出大量请求。准备降级方案非官方 API 随时可能失效。确保你的内容在本地如 Markdown 文件和 Git 中有备份。如果 API 失效你仍然可以手动发布。关注社区动态在 GitHub 或相关论坛上关注你使用的非官方库的动态。如果原作者更新了你可能需要同步更新你的代码。通过本文的梳理你不仅学会了一个具体的 Substack API 工具的使用更重要的是掌握了“逆向工程”封装非官方 API、构建 CLI 工具、并集成到现代自动化工作流中的完整方法论。这套方法可以迁移到许多其他缺乏官方 API 但又有自动化需求的平台。技术探索的乐趣在于此但请务必牢记安全与合规的边界将自动化作为提升个人效率的助手而非攻击平台的武器。
返回列表