☰
从数据抓取到AI创意:用Python爬虫与生成式语言模型打造小说标题批量生产流水线
2026/10/20 22:57:32 网站建设 项目流程

1. 先聊聊我为什么会在小说爬虫之外,再加一个AI标题生成器

1.1 爬虫只是搬运工,数据堆起来之后反而更迷茫

我最初写那个Python小说爬虫,纯粹是被数据量诱惑了。某个公开书库的元数据页面结构很规律,书名、作者、分类、简介、字数、章节标题整整齐齐地摆在HTML里,感觉不抓下来都对不起自己。于是花了两个晚上写了个爬虫,定时去抓公开可访问的页面,抓到本地SQLite里,很快就积累了四万多条小说元数据。

这个阶段其实挺爽的,看着一条条数据落库,有一种"我掌握了整整一个文库"的错觉。但爽过之后问题就来了:数据堆得再多,它们只是躺在数据库里的死文本。我想拿这批数据做点实际的事情,比如给某位写手朋友做书名灵感推荐,或者给某个内容账号做标题点击率测试,结果发现原始标题根本没法直接用。传统网文的标题要么长到像一段简介,要么全是套路词堆砌,什么"逆天""至尊""狂婿"来回排列组合,让人完全不知道该从哪本开始看。

这时候我才反应过来,爬虫解决的是"看到更多"的问题,但"想到更好"这件事它一点都不擅长。爬虫的价值是获取,而获取之后的加工、重组、再创造,才是数据真正产生价值的地方。我给自己提了个需求:能不能在爬虫后面接一个自动生成器,让这批小说数据变成一堆"读起来就想点进去"的标题?这就是AI标题生成器的由来。

1.2 标题生成器真正解决的问题:起名难、灵感枯竭、批量测试

我得承认,最开始想做这个功能,是因为自己起名能力约等于零。小说书名这玩意儿,写手圈子里有句老话叫"书名定生死"。一位朋友跟我说过,他写的一本悬疑小说内容质量不错,但书名起得太文艺,平台推荐位上一放,点击率惨不忍睹,后来编辑帮忙改了个强冲突的书名,数据立刻翻了好几倍。这是真实的痛点,不是玄学。

除了写手起名难,还有两类人需要批量标题:

  • 做小说推荐和解说的内容创作者,需要给每个推荐视频起一个抓人的标题;
  • 想测试平台推荐位效果的运营,需要快速生成几十个不同风格的标题做小规模对比测试。

这两类需求都不是一个一个手动想能解决的,需要的是"批量生产候选标题,人工挑选"的模式。AI标题生成器恰好能承担这部分工作:把小说简介、分类、主角设定喂进去,让它产出几十个不同风格的标题,我再人工筛一遍,效率比纯手动高太多了。

2. 小说爬虫部分:先搭一个能跑的"数据底座"

2.1 数据源选择与合规底线:我只碰公开可访问的元数据

聊爬虫绕不开合规问题,这里我先把底线摆清楚:我的爬虫只抓公开可访问的元数据页面,也就是书名、简介、分类、章节标题这类基础信息。不做付费章节抓取,不碰需要登录或者需要破解验证的内容,不绕过任何访问限制。每个站点我都要先看一眼robots协议,人家明确禁止的路径坚决不碰,页面里声明了"未经许可不得抓取"的站点也直接排除。

这不是说漂亮话,而是我踩过坑之后学乖了。早期我做爬虫追求"能跑就行",省掉了很多检查,结果某个站点反爬上强度后,我的请求直接被封了IP,连正常网页都打不开,处理起来特别麻烦。后来我总结出一个原则:爬虫项目第一要考虑的不是能抓到多少,而是这个数据来源干不干净、访问行为合不合规。数据用途我也做了限制,仅供个人学习研究和内容参考,不搞商业化分发,更不会把抓来的小说原文拿去传播。

基于这个合规前提,整个数据底座的设计就变得简单了,不需要研究复杂的反反爬方案,只要做到友好、低频、克制即可。

2.2 请求与解析:用同步请求加BeautifulSoup足够,不需要上框架

很多入门教程一上来就推荐Scrapy,但实际上,对于一个只抓元数据、量级在几万条的轻量项目,直接用requests加BeautifulSoup完全够用。我见过太多人把项目一开始就搞重了,调度器、中间件、管道全上,结果调试成本高了一倍,收益却几乎为零。

我的做法很朴素:先写一段请求函数,控制好超时时间和重试次数,再写一段解析函数,从HTML里提取需要的字段。核心代码大概长这样:

import time import random import requests from bs4 import BeautifulSoup def fetch_page(url, retries=3): headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" } for attempt in range(retries): try: resp = requests.get(url, headers=headers, timeout=10) if resp.status_code == 200: return resp.text except requests.RequestException: pass time.sleep(2) return None def parse_meta(html): soup = BeautifulSoup(html, "html.parser") items = [] for card in soup.select(".book-card"): title = card.select_one(".title").get_text(strip=True) intro = card.select_one(".intro").get_text(strip=True) category = card.select_one(".category").get_text(strip=True) items.append({ "title": title, "intro": intro, "category": category, }) return items

这里有个细节特别值得说:每次请求之间一定要加随机延迟。我习惯在1.5秒到3秒之间波动,既不给目标站点施加压力,也能减少被识别为自动化程序的风险。别小看这个随机延迟,它决定了你的爬虫会不会变成别人的负担。

2.3 去重与落库:SQLite三步走,别一上来就上重型数据库

数据抓到之后,去重是最容易翻车的一环。小说平台经常有同一本书的多个版本,比如"正文版""精校版""番外收录版",如果不处理,库里会堆满重复数据。我一开始没在意这个问题,结果去重时才发现数据库里出现了三百多组标题相同、简介略有差异的记录,整个数据质量被打了个大折扣。

解决方式也不复杂:在SQLite里建表时给原始标题字段加唯一索引,插入采用upsert方式,冲突时更新更新时间戳即可。表结构我设计得很轻量:

CREATE TABLE IF NOT EXISTS novels ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, intro TEXT, category TEXT, source_url TEXT UNIQUE, created_at TEXT DEFAULT (datetime('now', 'localtime')), updated_at TEXT DEFAULT (datetime('now', 'localtime')) );

写入操作我写了个简单的函数,批量执行replace插入。这里有个小技巧:如果用INSERT OR REPLACE,会先删旧再插新,导致id变化,外键关系容易被搞乱。用ON CONFLICT DO UPDATE则会保留原id,后续做关联分析时更稳妥。我用的写法是:

def upsert_novels(conn, items): sql = """ INSERT INTO novels (title, intro, category, source_url) VALUES (?, ?, ?, ?) ON CONFLICT(source_url) DO UPDATE SET title=excluded.title, intro=excluded.intro, category=excluded.category, updated_at=datetime('now', 'localtime') """ conn.executemany(sql, items) conn.commit()

这套组合跑下来,几万条小说元数据轻轻松松就整理好了,后面调用AI标题生成器的时候,等于有了一个干净、稳定、可查询的数据源。爬虫这部分做到这个程度就够用了,不要过度设计。

3. AI标题生成器:模型选型、提示词设计与输出稳定性的那点事

3.1 选型逻辑:通用对话模型API比本地微调更适合这个场景

给爬虫加"脑子"的时候,我其实纠结过一个方案:拿小说标题数据集去微调一个本地小模型。当时觉得这样更自主,成本可控,还不用依赖外部的接口。但实际操作下来发现这是一个典型的不划算交易。

原因很简单,标题生成这件事的数据特征太分散了。小说分类有玄幻、都市、悬疑、科幻、言情几十种,每种风格的标题规律完全不同,想把它们全部学进一个小模型里,需要的训练样本量远超我手上的四万条元数据。就算强行训练,出来的标题大概率只会学到"霸总""战神"这类高频词的堆砌,跟模板生成器没什么区别。

通用对话模型API就不一样了,它本身就见过海量的文本,理解能力强,我只要把小说的分类、简介、关键设定作为上下文喂进去,它就能按我的要求做风格迁移和重新创作。这属于典型的"调用别人的能力,而不是重新造轮子"。和本地微调的对比我列了个表:

维度本地微调小模型通用对话模型API
数据需求需要大量高质量配对样本只需要少量信息作为提示词上下文
风格适应速度每换一个风格就要重新训练换提示词模板即可
硬件成本GPU训练环境成本高按调用量付费,起步成本低
维护成本模型版本迭代需要自己管服务商持续更新,无需操心
灵活性改需求要重新走训练流程改提示词马上见效

对个人项目和早期原型来说,通用API的灵活性优势太大了。你花两小时调提示词,就能达到本地微调好几天的效果,这种性价比没有理由不选。

3.2 提示词模板:把小说信息变成高质量标题的写法

选定了模型方案后,核心工作就落在了提示词设计上。这一步我实验了很多版本,从最早的"帮我给这本小说起几个标题"这种极简写法,到后来固定格式的工程化写法,效果天差地别。

最终我用的模板长这样,里面所有变量都是由爬虫库里查出来的真实字段填充:

你是一位有十年经验的小说编辑,擅长为不同类型的小说设计吸引人的书名。 下面是一部小说的基本信息: - 分类:{category} - 原标题:{original_title} - 内容简介:{intro} 请根据以上信息生成8个全新的小说标题,要求: 1. 符合{category}类作品的常见风格,同时保留一定的辨识度; 2. 标题长度控制在6到15个字之间; 3. 避免使用"逆天""至尊""狂"这类泛滥套路词; 4. 至少包含2个偏文艺风格的标题,至少包含2个强冲突风格的标题; 5. 不要直接引用或者轻微修改平台上已存在的知名书名。 输出格式:只输出JSON数组,不要输出其它任何内容。 数组格式如下: ["标题1", "标题2", "标题3"]

这个模板有三个关键设计是我反复调出来的:

  • 先给模型一个"资深编辑"的人设,并明确目标"设计吸引人的书名",这能显著提升输出质量,没有目标的生成就是纯随机的词语拼接;
  • 要求输出JSON而不是普通文本,是为了方便程序直接解析,不需要再去处理"1、2、3"这种列表前缀;
  • 明确指定"不要引用知名书名",是为了规避侵权风险,哪怕模型可能不完全遵守,至少能减少概率。

3.3 输出稳定性的三道保险:JSON解析、校验、兜底

大模型输出的一个老问题是:不稳定。同一个提示词,这次输出规规矩矩的JSON,下次可能就在数组外面多了一行解释性文字,这种情况在项目里我遇到过不止一次。我的处理方案是三道保险层层兜底。

第一道保险是解析容错。我不用直接的json.loads,而是先尝试去掉可能的首尾干扰文本,再提取第一个[和最后一个],用切片方式拿到最有可能的JSON子串再做解析。第二道保险是内容校验。解析成功之后,逐个检查生成标题的长度、是否包含非法字符、是否和原文标题完全相同,不合格的直接丢弃。第三道保险是重试机制。如果一次生成的结果完全解析失败或者合格标题数不足,就自动换一组随机延迟后重新调用一次,最多重试两次。

import json import re def parse_titles(raw_text): text = raw_text.strip() start = text.find("[") end = text.rfind("]") if start == -1 or end == -1: return [] try: data = json.loads(text[start:end+1]) if not isinstance(data, list): return [] result = [] for item in data: if isinstance(item, str) and 4 <= len(item) <= 20: result.append(item) return result except json.JSONDecodeError: return []

这套机制跑起来之后,单批标题的可用率从最初的七成左右提升到了九成以上,剩下的不到一成则进入人工复审队列,由人来做最终决定。AI负责量,人负责质,这是我这个项目里最重要的分工原则。

4. 把两边拼成一条流水线:从抓取到批量生成再到入库

4.1 批量任务调度:线程池加队列控制,别让爬虫和生成器互相阻塞

数据底座和生成器单独跑都没问题,但把它们拼起来的时候,麻烦就来了。爬虫是IO密集型任务,AI生成调用也是IO密集型任务,如果串行执行,每个条目都要等上一个完全结束才开始,慢得让人崩溃。我第一版流水线试过串行处理一百部小说,跑了将近四十分钟,那个等待过程实在折磨人。

解决方案是引入线程池,把任务拆分到多个工作线程里并发执行。爬虫负责的数据抓取和生成器负责的标题生成可以并行推进,中间用队列做缓冲。我用的是Python自带的concurrent.futures,没有引入额外的任务队列框架,因为这批项目规模用不上celery这种重量级方案。

from concurrent.futures import ThreadPoolExecutor, as_completed def process_one(row): novel_id, title, intro, category = row generated = generate_titles(category, title, intro) return novel_id, generated with ThreadPoolExecutor(max_workers=5) as executor: futures = [executor.submit(process_one, row) for row in rows] for future in as_completed(futures): novel_id, generated = future.result() save_results(novel_id, generated)

这里有一个经验:并发数不是越大越好。刚开始我图快设了20个线程,结果AI接口那边直接返回限流报错,报错之后还要触发重试,反而拖慢了整体进度。后来把并发控制在5个,同时给每批任务之间加了50到100毫秒的偏移,整体速度稳定下来,出错率也大幅下降。

4.2 成本与速率限制:一次调用花多少钱,怎么不让账号被打进黑名单

用API做批量生成,绕不开的一个话题就是成本。我算过一笔账:每部小说的提示词大约200个token,生成的回复约100个token,单次调用总共300个token左右。按通用对话模型的常见定价换算,每1000次调用大概花几块钱人民币,处理四万部小说的全量数据大概需要几百块。这个数字对个人项目来说不算小,所以我做了一个很务实的决定:只对分类入库且简介长度超过100字的小说跑标题生成,毕竟简介太短的样本,生成出来的标题就是纯猜,没什么实用价值。

筛选之后,实际需要处理的量降到了八千部左右,测试成本控制在了一顿饭钱以内,处于可接受范围。关于速率限制,我的做法是控制每秒钟的请求数,同时在代码里做好了退避策略:一旦收到限流信号,线程自动休眠一段时间再继续,而不是死命重试。反正流水线是跑在无人值守环境里的,慢一点没关系,账号安全性才是底线。

4.3 我为什么加了一个最简结果预览页

批量生成出来的标题,如果只是打在终端里,根本没法做人工筛选。几千条结果刷屏过去,眼睛都快看瞎了。后来我加了一个最简的结果预览页,本质就是一个本地Flask应用加一个数据库查询页面,默认显示最近半小时生成的结果,每条记录后面带一个"采纳/弃用"按钮。

你别小看这个简陋页面,它成了整个项目里使用频率最高的部分。人工复审是个很依赖体验的环节,如果工具让人难受,最后的结果就是没人去做筛选,那AI生成的标题再多也白搭。这个预览页我用了不到一百行代码就写完了,如果哪天觉得不够用再加筛选条件和排序功能,反正都是从SQLite里查数据,成本极低。

5. 实测效果与翻车记录:哪些标题真能打,哪些坑必须绕开

5.1 三个让我意外满意的案例

流水线跑通之后,我拿库里真实存在的分类数据做了几次实测,其中几组生成结果确实让我眼前一亮。

第一组是悬疑分类。原始信息是一部关于"小镇连续失踪案"的小说,AI生成的标题里有《第七个失踪者》《雾镇疑云》和《她用一张旧照片换来真相》,第三个我觉得比前两个都好,它把关键线索直接放进标题,立刻让读者产生了"这张照片背后有什么"的追问欲。

第二组是科幻分类。原始信息是"近未来赛博都市里,主角发现自己的记忆被定期备份并篡改",AI给出的标题里有《记忆银行》《今天我又被重置了》《备份深处的裂缝》,这三个都具备强烈的设定感,放在推荐位上抓人程度明显高于原有的机械味标题。

第三组让我意外的是都市分类。原本这类数据生成的标题最容易踩套路陷阱,但我明确禁止了泛滥词之后,模型带来的结果居然是《周一早晨的离婚协议》这种生活切片式标题,把一个普通的婚恋故事写出了悬念感,我拿给朋友看,对方立刻说想起了某部韩剧的风格。

这几次实测让我确认了一件事:AI生成标题的价值不在于替代人类起名,而在于提供人类不会想到的角度。我自己起名容易顺着惯性走,但模型可以从简介里的任何一个小细节发散,产生思维盲区里完全不存在的新方向。

5.2 翻车现场:内容偏离、风格混杂、格式污染

当然,项目里最多的还是翻车时刻。说到这个我信心就更足了,因为踩过的坑基本都能整理成一张避坑清单。

最常见的翻车是内容偏离。有的小说简介很长,模型会把配角名字当成主角写进标题,生成出来的标题跟主线剧情毫无关系。比如一部武侠小说里有个出场两次的反派叫"血手",结果AI生成《血手的最后一夜》,看起来像反派外传。后来我在提示词里明确加上一句"标题必须反映主角视角或核心事件,禁止使用简介中从未出现过的次要人物",情况好转了很多。

第二种翻车是风格混杂。明明要求"保留辨识度",但模型偶尔会生成一个特别像短视频平台爽文风格的标题,跟原始小说基调完全不搭。这个没有完美的解决方案,我只能靠"同时生成多种风格再人工挑"的策略来对冲,反正8个候选标题里总有一个能用的。

第三种翻车是格式污染。模型偶尔会在JSON数组外面加上解释性文字,比如"以下是为您生成的标题",这类输出我直接用前文写的解析容错函数处理掉。也有时候它会在标题里带引号,导致入库之后显示出来全是引号,后期清洗花了不少功夫,我写了段正则专门清理多余引号。

5.3 人工复核是最后的防线,尤其注意敏感与侵权

AI生成的标题,哪怕提示词里写了各种禁止事项,依然有可能踩雷。我在真实运行中见到过一次:某条悬疑类生成结果碰巧与一部已完结作品的书名完全一致,虽然我们的目的不是抄袭,但要是直接拿去做推荐,很容易引发争议。从那之后我立了一条规则:所有生成标题在采用前必须人工复核,复核时先搜索一遍是否与知名作品重名,再检查是否有隐晦的敏感用语。

这看起来是增加工作量,其实是最稳妥的做法。给内容账号用的时候,我还会先拿测试号发一条不出现在公开推荐位上的内容做测试,观察两三天的反馈后再决定要不要正式使用。这个习惯帮我避开过至少两次可能翻车的场景,强烈建议做内容运营的朋友都养成这个习惯。

6. 一些边界提醒与这个项目后续还能怎么玩

6.1 合规不是口号:robots、版权、用途三个层面

聊到最后,我必须把合规这件事再拎出来讲一遍,因为它不是项目做完就结束的,而是贯穿全过程的约束。在爬虫环节,我坚持只访问公开可访问的元数据页,遵守robots协议,不绕过访问控制。在数据使用环节,不做商业发布,不传播小说原文,只把元数据用于个人学习和工具开发。在AI生成环节,提示词里明确禁止直接引用或轻微修改知名书名,生成结果经过人工复核才能使用。

这三个层面缺一不可。爬虫的合规决定你能不能拿到数据,数据使用的合规决定你拿到之后能不能用,生成结果的合规决定你用完之后会不会惹上麻烦。尤其是那些拿了公开数据就想商业化包装的朋友,我劝你多掂量一下再动手,个人项目踩到红线最多删库跑路,商业项目踩到红线可就不是删库这么简单了。

6.2 可扩展方向:章节标题批量评分、多语种标题、投稿辅助

这个项目跑到目前这个阶段,数据底座跑得稳,标题生成可用率也稳定在九成以上。如果后续还想继续折腾,我自己规划了几个方向。

第一个方向是章节标题批改。不只是给整本书起名,还可以把小说每个章节的标题批量提取出来,让模型分析章节标题是否引人入胜、是否有连贯性,给出优化建议。这个对长篇连载作者价值很大,因为写到后面几十章的时候,作者往往已经没有精力再去打磨每章的起名了。

第二个方向是多语种标题生成。中文标题翻译成英文、日文标题时,直译往往很生硬,可以基于原文简介信息直接生成贴合目标语言习惯的标题,方便做海外内容推广。

第三个方向是投稿辅助。把生成标题和平台热门榜单数据做交叉分析,让模型基于当前流行趋势给出"这个作品放到哪个分类、用哪种风格标题更容易获得推荐位"的建议。不过这个方向需要额外接入榜单数据,工作量和复杂度都会上一个台阶,属于真正的进阶玩法。

我在实际操作中最深的一点体会是:不要让工具代替你做决定。AI标题生成器负责把可能性铺开,把那些人类想不到的角度摆到桌面上,但最终拍板的那一下,永远是人的判断。数据是死的,模型是辅助的,只有人的经验和审美才是真正值钱的东西。这套工具最好的使用方式,就是把它当成一个极其擅长头脑风暴但不负责做决定的同事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询