每年春节前后,手头总有几个自动化的活儿要赶。前年接了一个内容平台的需求,要在节前半个月内产出一批定制春联,每家客户给的祝福方向都不一样,有求财运的、求健康的、求事业的,还有指定要嵌字藏头联的。人工写肯定来不及,网上现成春联库又没法灵活定制。后来走了AI接口的方案,用Python写了一个调用AI春联生成接口的脚本,每天定时跑一批,把对联文案自动抓回来,再套进模板发布。整个过程下来,其实就是一次典型的“接口式爬虫”实践。
这个项目表面上是写春联,实际上把Python爬虫的请求构造、响应解析、异常重试、参数封装、批处理调度这几个环节全练了一遍。今天把这个案例完整拆出来,既是记录自己的实现思路,也给准备上手爬虫、做自动化或者玩AI接口的朋友一份可以直接抄作业的参考。
1. 为什么是“接口爬虫”而不是直接抓网页:春联生成场景的技术选型
1.1 先把场景说清楚:要的是一批不是一副
很多人一听“爬虫”就想到去网页上抓HTML、解析DOM。但在这个案例里,目标不是去某个春联网站把所有现成的对联抓下来,而是要生成一批带定制属性的内容。比如:
- 上联可以嵌入客户公司名称或人名
- 横批要有特定寓意,如“招财进宝”“健康平安”
- 联句要押韵、对仗工整,符合传统春联的平仄逻辑
- 同一条主题下需要多组候选,方便运营挑稿
这种动态生成的需求,静态抓网页解决不了。你没法跑到别人的网页上去替人家改输入框再拿回结果,所以更合理的路线是找到背后真正提供内容生成的程序接口,用Python模拟请求直接调用。
1.2 为什么称它为“接口爬虫”
接口爬虫的核心逻辑是:整个数据交换过程不走HTML页面,而是直接发送HTTP请求到特定的API地址,服务器返回的是结构化数据,通常是JSON格式。爬虫要做的事情变成三件:
- 分析接口的请求方式和参数结构
- 构造合法的HTTP请求
- 解析结构化响应,提取需要的内容
用这种思路来调AI春联接口,比写正则表达式从一堆HTML标签里抠文本要干净得多。尤其AI接口通常返回的是完整字段,如“上联”“下联”“横批”“解析说明”等,直接按key取值就行。这也是为什么我建议做内容生成类自动化项目的朋友,优先去研究接口而不是硬啃网页。
1.3 技术选型的判断标准
这个项目我之所以选择“Python requests + JSON解析”的组合,其实就三个原因:
- requests库的代码量最小,调通一个接口只需要十几行代码
- JSON是结构化数据,字段清晰,解析结果稳定可控,不需要面对网页改版带来的选择器失效问题
- Python的批处理和定时调度生态成熟,每天跑一批任务,cron或计划任务就能搞定
如果你对爬虫的印象还停留在BeautifulSoup加Selenium的阶段,看完这个案例你会发现,很多内容型需求真正卡点不在“解析网页”,而在“构造请求”。把接口调用练熟,实际上你就能解锁一大类数据获取和内容生成的自动化脚本。
2. 接口调研:理清AI春联生成请求的参数结构与返回格式
2.1 从哪里找到接口信息
调接口之前,要先搞清楚请求长什么样。我做这个项目时,接口信息主要来自两部分:一部分是平台开放文档,另一部分是通过浏览器开发者工具观察实际请求。
这两条路可以交叉验证。有文档的以文档为准,文档里一般会写清楚请求地址、请求方式、请求参数、返回示例,甚至标注了每个字段的含义和取值范围。如果没有文档,就用开发者工具的Network面板,打开一个调用春联生成的页面,观察发出去的请求报文。
2.2 请求参数的关键维度
以我使用的那个AI春联接口为例,请求方式是POST,请求体是JSON格式。核心字段基本上围绕“内容主题”展开:
| 参数名 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
| subject | string | 是 | 春联主题,比如“财运”“健康”“事业”“学业” |
| keyword | string | 否 | 藏头字或嵌字词,比如“宏图” |
| count | int | 是 | 返回对联的组数,一般取1到5 |
| style | string | 否 | 风格偏好,比如“传统”“现代”“诙谐” |
| model | string | 是 | AI模型标识,可能影响生成质量和速度 |
注意subject这个字段很关键,它决定了AI生成的方向。有的人一上来就把它当成提示词,塞了一大段描述进去,结果接口直接报参数错误。要理解这类接口的设计思路:它在服务端已经把提示词工程做掉了,上层只需要传一个意图分类,模型会在对应模板上做二次生成。
2.3 返回格式解析
响应同样是一个JSON对象,典型结构如下:
{ "code": 0, "message": "success", "data": { "pairs": [ { "up": "财源广进如意岁", "down": "福气盈门吉祥年", "horizontal": "招财纳福", "source": "ai" } ], "request_id": "c7a2f9e1d4b64c3b8f0a5e6d7c8b9a01" } }各个字段的用途一目了然。“code”用于判断请求是否成功,通常0代表成功,非0代表失败。“message”里是对失败原因的简短说明。“data.pairs”里存放生成好的春联列表,每组包含上联、下联、横批。
这里有一个容易踩的细节:不要把“code”和HTTP状态码混为一谈。HTTP 200只代表TCP层面连接正常、服务器收到了请求并给了响应,但业务上可能还是失败。比如参数校验不通过,服务器照样返回HTTP 200,但body里的“code”可能是40001。所以解析响应时,第一步永远是检查业务code,然后再去处理data。
2.4 接口测试的准备工作
正式写爬虫脚本之前,我建议先用命令行或者Postman把接口调通一次,确认三件事:请求头需要哪些字段、是否需要鉴权、返回结构是否文档描述一致。这个阶段会暴露很多隐藏问题,比如Header里面的Content-Type不对导致的数据格式错误,或者缺少某个Header信息导致请求被拒。
我的做法是先用curl命令快速验证:
curl -X POST https://api.example.com/ai/springcouplet \ -H "Content-Type: application/json" \ -H "User-Agent: Mozilla/5.0" \ -d '{"subject":"财运","count":1,"model":"standard"}'看到返回结果里有正常的春联内容,再把它翻译成Python代码。一上来就直接写Python,万一遇到问题,你还要区分是代码问题、网络问题还是接口问题,排查成本会高不少。
3. Python爬虫落地:从requests封装到首个正常响应
3.1 最基础的调用代码
确认接口通了之后,我用requests写了一个极简版本,先把链路跑通:
import requests import json url = "https://api.example.com/ai/springcouplet" headers = { "Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" } payload = { "subject": "财运", "count": 2, "style": "传统", "model": "standard" } resp = requests.post(url, data=json.dumps(payload), headers=headers, timeout=10) result = resp.json() if result.get("code") == 0: for pair in result["data"]["pairs"]: print("上联:", pair["up"]) print("下联:", pair["down"]) print("横批:", pair["horizontal"]) print("---") else: print("请求失败:", result.get("message"))3.2 为什么要用data=json.dumps而不是json=
很多人在这里有个固化习惯:直接用requests.post里的json参数,让它自动序列化。大多数情况下没问题,但一旦遇到需要在请求体里做字符串拼接或者二次处理的场景,你根本不知道压进去的到底是什么格式。我的习惯是用data=json.dumps(payload),这样看到请求体就是纯字符串,心里有底。
还有一个细节是timeout参数。别觉得加个10秒只是拖延时间,它其实是告诉requests:如果10秒内没有响应,直接抛出超时异常,不要无限等待。批量任务里最怕的就是某个请求卡死,拖垮整个调度周期。
3.3 会话复用与连接池优化
每天跑一批任务,如果每生成一条春联就重新建立TCP连接,效率很低。尤其当你需要生成上百条时,连接重复创建的耗时和资源损耗会非常明显。改进方式是使用requests.Session,让同一个session实例复用底层的连接池。
session = requests.Session() session.headers.update(headers) def fetch_couplet(subject, keyword, count=1): payload = { "subject": subject, "keyword": keyword, "count": count, "model": "standard" } try: resp = session.post(url, data=json.dumps(payload), timeout=10) return resp.json() except requests.Timeout: return {"code": -1, "message": "timeout"}这样定义的好处是,后续所有对这个AI接口的调用都走同一个连接池,会话的Header也统一管理。批量生成时只要有需要就调用这个函数,页面级别的重复工作被收敛到了一处。
3.4 响应解析的健壮性设计
接口文档再清晰,也不代表返回内容永远符合预期。真实场景中经常遇到联字段缺失、返回内容包含多出来的换行符、甚至结构性null的情况。所以要设计一个健壮的提取函数:
def extract_pairs(data): pairs = [] if not data or not isinstance(data, dict): return pairs pairs_data = data.get("data", {}).get("pairs", []) for item in pairs_data: up = (item.get("up") or "").strip() down = (item.get("down") or "").strip() horizontal = (item.get("horizontal") or "").strip() if up and down and horizontal: pairs.append({ "up": up, "down": down, "horizontal": horizontal }) return pairs这个函数的重点是“先校验再提取”。字段缺失时用空字符串兜底,最后检查三联内容是否齐全,过滤掉不合规的数据。这样做的原因是下游马上要套发布模板,如果缺失字段导致模板错乱,前面的工作就白费了。
4. 实战踩坑:接口调用中三个高频问题的完整排查链路
4.1 总是报签名错误,排查半天才发现时间戳问题
第一次跑批量任务时,连续几条请求都被接口报错,code返回了401001,意思是签名验证失败。我第一时间怀疑是密钥配置错了,于是打开配置核对了一遍,发现没问题。又怀疑是参数拼接顺序不对,反复对照文档也没找到问题。
直到用抓包工具对比了成功的请求和失败的请求,才发现失败请求的时间戳参数是“昨天”的缓存值。原来脚本里把timestamp写成了全局变量,等到第二天凌晨任务继续跑的时候,这个全局timestamp没刷新,签名自然就校验不过了。
这个问题暴露了一个经验:涉及签名校验的接口,每次请求必须重新生成时间戳和随机串,千万不要复用上一次的值。把这两项参数从模块级的全局变量改成函数内的局部变量,问题立刻消失。
4.2 高频请求触发限流,不能只有一个重试策略
项目在测试阶段需要生成大量测试数据,我写了一个没有间隔的for循环,一次性调用200次接口。跑到第80多次的时候,返回的code开始变成429001,message是“frequency limit exceeded”。
这个问题的本质是接口服务端对单IP或单凭证的请求频率有限制。如果只是简单重试,短时间内依然会被拒。我的解决思路是三层递进:
- 正常请求间隔:每次请求之间强制sleep 0.5秒
- 遇到限流错误时,第一次等待5秒后重试一次
- 如果还是限流,这次就跳过,把失败任务写入待重试队列,放到下一轮补跑
用代码实现就是这样:
import time def robust_fetch(subject, keyword, max_retry=2): for attempt in range(max_retry + 1): result = fetch_couplet(subject, keyword) if result.get("code") == 0: return result if result.get("code") in (429001, 500001): time.sleep(5) continue break return {"code": -1, "message": "failed after retry"}这样做的好处是,既给了服务端恢复的时间,又不会因为反复重试把请求频率堆得更高。最终成功率和任务稳定性都有明显提升。
4.3 返回内容出现重复和错别字,需要一层额外校验
AI生成的文本毕竟不是模板数据库中挑选出来的,偶尔会有一副对联的上联和下联内容高度雷同,或者横批直接复用了上联的一两个字。最初我没做这一步校验,把内容直接往模板里套,结果被审核那边打回了好几条。
后来我加了一个简单的去重和合规检查:先去掉纯空格和换行符,再判断上联、下联、横批两两之间是否互相包含超过一半的文字,如果包含程度过高就判定为低质量数据,丢弃后重试。
还有一个容易被忽略的点:接口偶尔会在文本中夹带Markdown标记或编程风格的引号字符,比如英文双引号。如果你的下游是发布到排版工具里,这些小符号很容易影响展示效果。我在提取函数里同时做了一个轻度清洗:
import re def clean_text(text): text = re.sub(r'["\']', '', text) text = re.sub(r'\s+', ' ', text) return text.strip()5. 模块化进阶:把单一接口封装成可批量复用的内容生产工具
5.1 从脚本到函数库
跑通单次调用后,下一步就是把它从“能跑的脚本”升级成“能复用的工具”。我的做法是抽象出一个独立的模块文件,比如叫couplet_client.py,将所有接口相关的逻辑封装成一个类。
class CoupletClient: def __init__(self, base_url, headers): self.session = requests.Session() self.session.headers.update(headers) self.base_url = base_url def generate(self, subject, keyword="", count=1, style="传统"): payload = {...} resp = self.session.post(...) return self._parse(resp) def _parse(self, resp): ...封装类的核心收益在于调用方不需要关心请求细节。后续不管是做每日自动任务,还是做一个便携的命令行工具,都只需要面对一个简洁的generate方法。这个思路也能迁移到其他AI生成类接口上,改改参数名和新构建请求的地方就能复用。
5.2 两个Python脚本之间怎么传参数
实际使用中你会发现,经常需要从外部把“任务清单”传进生成脚本。比如有一个计划文件里定义了今天要生成哪些主题的春联,然后调度脚本读取计划,逐条调用生成脚本。
最简单的传参方式是命令行参数。假设couplet_worker.py接收三个参数:
python couplet_worker.py --subject 财运 --keyword 宏图 --count 3在worker内部用sys.argv或argparse解析。如果参数之间还有复杂的嵌套结构,比如一个主题下面需要多组风格,建议用JSON文件作为中间载体。调度脚本把配置写进配置文件,worker读取后逐条执行。我不太推荐用环境变量传递大量业务参数,调试麻烦,容易遗漏。
如果你只是想在脚本A里直接调用脚本B,也可以使用subprocess模块:
import subprocess result = subprocess.run( ["python", "couplet_worker.py", "--subject", "事业", "--count", "2"], capture_output=True, text=True ) print(result.stdout)这里有个实践教训:使用subprocess时,尽量把执行路径写成绝对路径或基于脚本所在目录的路径,否则在定时任务里很容易出现“找不到文件”的情况。因为计划任务的工作目录和你手动执行时的当前目录不一定一致。
5.3 非技术用户场景:把脚本打包成可执行文件
项目验收时,运营同学说他们不想自己装Python环境,希望拿到一个双击就能跑的工具。于是我把整个项目用PyInstaller做了打包。这里有一个关键的坑:脚本里如果引用了配置文件,比如config.ini,打包时不指定路径,运行时会因为找不到配置而报错。
我的解决方案是统一从脚本所在目录动态计算文件路径,而不是依赖当前工作目录。打包时再用--add-data把配置文件一起带进去。这样运营同学不管把可执行文件放到什么目录,都能正确读取到配置。
命令行打包的大致方式是:
pyinstaller -F couplet_worker.py --add-data "config.ini;."-F参数表示打成一个单独的可执行文件。打包前建议先在干净的虚拟环境里重新安装一遍依赖,避免把一堆无关模块打进去,导致exe体积虚胖。
5.4 批处理调度:每天固定时间跑一批
这个案例的核心场景之一就是“每日spider”,所以定时调度必不可少。Linux下用crontab设定执行时间非常简单:
0 6 * * * cd /opt/couplet_project && /usr/bin/python3 couplet_scheduler.py >> logs/cron.log 2>&1Windows下用“任务计划程序”也能达到同样的效果。重点是日志管理:总是把输出重定向到指定日志文件,因为任务跑在后台,没有任何窗口能让你看到print输出。就算程序跑挂了,日志就是唯一的排查线索。
另一个重要经验:定时任务里的Python建议用虚拟环境,并且路径要写全。很多人在这上面栽过跟头,写完crontab后任务一直不执行,查了半天发现是python3路径不对。在crontab里要使用绝对路径,不要依赖系统PATH。
6. 让调用更稳:AI接口内容生成任务需要单独关注的几个细节
6.1 超时设置要区分连接和读取
默认的requests超时只给一个数字时,表示连接阶段和读取阶段用同一个超时时间。但对于生成类AI接口,有响应可能比较久,连接却很快。我建议拆开设置:
resp = self.session.post(url, data=json.dumps(payload), timeout=(3.05, 30))前面的3.05表示连接阶段阈值,后面30表示读取阶段的最大等待时间。这样做可以避免因为某个接口生成速度慢导致超时误判。
6.2 生成内容保存时统一编码
这个案例中生成的春联全是中文,保存到文件时如果编码不对,会出现乱码。我统一使用UTF-8编码,并在打开文件时显式指定:
with open("output.json", "w", encoding="utf-8") as f: json.dump(all_pairs, f, ensure_ascii=False, indent=2)一定记得在json.dump中设置ensure_ascii=False。默认情况下,json模块为了兼容性,会把中文字符转成ASCII的\uXXXX形式,虽然不损坏数据,但可读性很差,也不利于后续人工审核。
6.3 日志分级记录每个请求ID
接口响应里往往带有一个request_id字段。建议在日志中把这个request_id记下来。将来一旦遇到某个内容生成异常,可以拿request_id向接口提供方反馈排查,快速定位服务端处理链路。
logging.info("request_id=%s subject=%s keyword=%s result=%s", result.get("data", {}).get("request_id"), subject, keyword, result.get("code"))看起来只是一行简单的日志,但如果没有这个字段,后面的排查就像大海捞针。尤其是当你每天生成上千条春联后,出现一条诡异内容时,你根本不可能逐条回忆是哪一次请求产生的。
6.4 失败任务自动补跑
日常自动任务稳定运行一段时间后,偶尔还是会出现某一次请求超时或接口报错的情况。针对这种情况,我设计了一个失败任务队列机制:生成失败的主题进入待重跑列表,在主流程结束之后,再对这些任务做一轮降速重试。
failed_tasks = [] for task in today_tasks: result = robust_fetch(task["subject"], task.get("keyword", "")) if result.get("code") != 0: failed_tasks.append(task) # 第二轮:对失败任务降速重试 for task in failed_tasks: time.sleep(2) result = robust_fetch(task["subject"], task.get("keyword", ""), max_retry=3) ...这套机制上线后,每日任务的完成率从最初的96%左右提升到99.5%以上,基本实现了无人值守。
7. 实际产出与应用:除了春联,这套代码还能做什么
7.1 每日产出的管理方式
这个项目每天生成的春联内容,我按日期分目录保存。每个文件里包含原始主题、生成时间、request_id和内容明细。文件名格式为YYYY-MM-DD.json。运营同学直接打开当天文件就能拿到全部文字。
保存成JSON的另一个好处是后续可以做自动化接入,比如直接联动发布系统,或者导入到内容管理后台,不用人工复制粘贴。数据格式标准化的价值,在自动化流程里会逐步显现出来。
7.2 从春联接口延伸到通用文案生成接口
春联生成只是AI文本生成接口应用的一个小分支。同样的请求结构,只要换一下接口参数和提示词配置,就可以做完自我介绍改写、商品卖点提炼、节日祝福语生成、朋友圈文案扩写等场景。
我后来基于同一个脚本框架,只改动了参数构造与返回字段的映射关系,就接入了另外三个不同的AI文案生成接口。这套模式的核心不是“爬春联”这个具体动作,而是把“构造请求-解析响应-异常兜底-批量调度”这一整条链路沉淀成了一个可复用的工程模板。
7.3 与AI Agent或更上层业务结合
如果你在搭个人Agent或自动化工作流,这个接口客户端甚至可以暴露成一个本地函数,直接嵌入到Agent的工具集里。比如用户输入“帮我写三副带‘瑞雪’二字的春联”,Agent分析出主题是“瑞雪”,然后调用这个封装好的工具函数,把结果格式化返回。整体上就是一层很薄但很实用的集成。
结合最近大家经常提到的AI Agent、接口封装、自动化流程这些热点,你会发现这类“单点接口工具化”的思路,其实是搭建上层应用的一块稳固地基。
8. 个人经验小结:这个案例里最有价值的三件事
如果非要总结这个“py每日spider案例之ai春联接口”项目里最有价值的部分,我自己感受最深的是三点。
第一,接口调用的严谨程度决定了自动化任务的上限。把参数设计、返回值校验、异常重试、日志关联这些细节做好,项目才能真正做到无人值守地跑上一个月而不掉链子。
第二,不要迷信“文档写得很清楚”这件事。真相是需要在真实请求中验证每一个字段。至少在这个项目里,我是通过在包头、请求体、响应解析三层各截获一次实际报文,才最终确认了所有细节。
第三,这类接口爬虫脚本,真正拉开差距的地方不在“能不能调到接口”,而在于“接口报错后怎么办”。你的重试策略是否合理、日志是否完整、失败任务能否自动补跑,这些才是项目能否长期稳定运行的关键。调通一个接口只是半小时的活,把整个流程打磨稳健才是项目真正花时间的部分。
另一个小技巧是:在开发初期,别急着把入口函数写得特别复杂。先用最简单的脚本把链路跑通,然后每解决一个问题、再加一道保险。这样做的好处是每次变更的边界清晰,就算改出问题也能快速回退到上一版本。我这次就是从十几行的临时脚本,一步步演变成支持批量调度、日志监控、失败重跑的稳定工具,整个过程非常顺滑。
如果你是刚开始接触Python爬虫或AI接口调用,完全可以照着这个案例的逻辑,随便选一个你熟悉的接口,一个个环节走下来。等到你完成“分析接口、封装请求、解析响应、异常兜底”这四步,基本就掌握了这一类项目的全部核心技术点。以后再遇到任何AI生成类、数据查询类接口,你都会有很明确的思路和充足的经验可以复用。