最近在追一本玄幻小说,网站上广告弹得烦,而且章节标题起得太敷衍,比如“第123章 出手”,看了完全提不起劲。作为一个爱折腾的程序员,我干脆自己写了个爬虫,把小说内容扒下来,顺便用AI重新生成标题——没想到效果出奇的好,现在读起来都带感多了。

今天就把整个实现过程分享给大家,从爬虫搭建到标题生成,全是实战干货,踩过的坑也会一一说明。

一、需求拆解:我们要实现什么?

先理清楚思路,整个项目分成两个核心部分:

  1. 小说内容爬取:从目标网站获取章节标题、正文内容,保存到本地文件。
  2. 智能标题生成:基于章节正文内容,用AI生成更有吸引力、更符合章节情节的标题。

选目标网站的时候,我特意挑了个结构简单、反爬不太严的小说站(这里就不点名了,大家懂的都懂)。重点是学习技术,千万别用于商业用途,尊重版权哈。

二、爬虫部分:稳扎稳打,避开反爬坑

爬虫这东西,说简单也简单,说难也难——难就难在和反爬机制斗智斗勇。我这次踩了几个小坑,给大家提个醒。

2.1 技术选型

  • 请求库requests,简单够用,没必要上Scrapy那么重的框架。
  • 解析库BeautifulSoup4,解析HTML足够灵活。
  • 文件操作:直接用Python内置的open(),保存成TXT文件方便阅读。

2.2 实现步骤

先看目录页的结构,通常目录页里每个章节都有一个<a>标签,包含章节标题和链接。我们的思路是:

  1. 请求目录页,解析出所有章节的链接和原始标题。
  2. 遍历每个章节链接,请求正文页,解析出正文内容。
  3. 将原始标题、正文内容保存到本地,同时把正文传给标题生成模块。

上代码(关键部分):

import requests
from bs4 import BeautifulSoup
import time
import random

# 伪装请求头,这是最基本的反反爬
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}

def get_chapter_links(catalog_url):
    """获取目录页所有章节链接"""
    try:
        response = requests.get(catalog_url, headers=headers, timeout=10)
        response.encoding = 'utf-8'  # 这里要注意网站编码,可能是gbk
        soup = BeautifulSoup(response.text, 'html.parser')
        # 假设章节链接在class为'chapter-list'的div里的a标签中
        chapter_list = soup.select('.chapter-list a')
        links = []
        for item in chapter_list:
            title = item.get_text().strip()
            url = item['href']
            # 有些网站链接是相对路径,需要拼接
            if not url.startswith('http'):
                url = 'https://www.example.com' + url
            links.append({'title': title, 'url': url})
        return links
    except Exception as e:
        print(f"获取目录失败: {e}")
        return []

def get_chapter_content(chapter_url):
    """获取章节正文"""
    try:
        # 加个随机延时,避免请求太快被封
        time.sleep(random.uniform(1, 3))
        response = requests.get(chapter_url, headers=headers, timeout=10)
        response.encoding = 'utf-8'
        soup = BeautifulSoup(response.text, 'html.parser')
        # 假设正文在class为'content'的div里
        content_div = soup.select_one('.content')
        if content_div:
            # 把br标签换成换行符,保持排版
            for br in content_div.find_all('br'):
                br.replace_with('\n')
            return content_div.get_text().strip()
        else:
            return ""
    except Exception as e:
        print(f"获取正文失败: {e}")
        return ""

2.3 踩坑记录

  • 编码问题:有些老小说站用的是gbk编码,response.encoding要改成'gbk',不然保存下来全是乱码。
  • 相对路径链接:解析出来的链接可能是/chapter/123.html,一定要记得拼接上域名。
  • 请求频率:一开始我没加延时,结果爬了20章就被封IP了,后来加了1-3秒的随机延时,就稳了。

三、标题生成:让AI帮我们“标题党”一回

爬下来内容只是第一步,重点是怎么让标题变得吸引人。我试了两种方案:一种是用本地的小模型(比如Qwen-7B),另一种是调用OpenAI的API。考虑到大部分人可能没有本地部署的条件,这里就讲调用API的方式(其实原理都一样)。

3.1 Prompt设计

这是最关键的一步。直接让AI“生成个标题”肯定不行,得给它明确的指令。我是这么设计Prompt的:

你是一个资深的玄幻小说编辑。请根据以下章节内容,生成一个不超过20字的标题。
要求:1. 突出本章的核心冲突或关键情节;2. 带有一点悬念感,吸引读者继续阅读;3. 不要使用“第X章”这样的格式。

章节内容:
{content}

3.2 代码实现

这里用openai库(如果用国内的API,比如智谱、通义千问,改一下base_url就行):

from openai import OpenAI

# 初始化客户端,这里以国内某API为例
client = OpenAI(
    api_key="your-api-key",
    base_url="https://api.example.com/v1"
)

def generate_new_title(content):
    """基于正文生成新标题"""
    # 正文太长的话,只取前1000字,避免token超限
    truncated_content = content[:1000]
    prompt = f"""你是一个资深的玄幻小说编辑。请根据以下章节内容,生成一个不超过20字的标题。
要求:1. 突出本章的核心冲突或关键情节;2. 带有一点悬念感,吸引读者继续阅读;3. 不要使用“第X章”这样的格式。

章节内容:
{truncated_content}"""
    
    try:
        response = client.chat.completions.create(
            model="gpt-4o-mini",  # 选个便宜又好用的模型
            messages=[{"role": "user", "content": prompt}],
            temperature=0.7  # 温度稍高一点,让标题更灵活
        )
        new_title = response.choices[0].message.content.strip()
        # 去掉可能出现的引号
        new_title = new_title.replace('"', '').replace("'", "")
        return new_title
    except Exception as e:
        print(f"生成标题失败: {e}")
        return None

3.3 效果对比

给大家看几个我实际生成的例子:

  • 原标题:第123章 出手

  • 新标题:少年一掌震退强敌,全场震惊

  • 原标题:第124章 秘境

  • 新标题:神秘洞府现世,暗藏惊天机缘

是不是感觉一下子就有内味儿了?

四、整合与保存:把成果落地

最后把所有模块串起来,把原始标题、新标题、正文都保存到文件里:

def main():
    catalog_url = "https://www.example.com/novel/123.html"
    chapters = get_chapter_links(catalog_url)
    
    if not chapters:
        print("没有获取到章节")
        return
    
    with open('novel.txt', 'w', encoding='utf-8-sig') as f:
        for i, chapter in enumerate(chapters):
            print(f"正在处理第 {i+1}/{len(chapters)} 章: {chapter['title']}")
            content = get_chapter_content(chapter['url'])
            if not content:
                continue
            
            new_title = generate_new_title(content)
            if not new_title:
                new_title = chapter['title']  # 生成失败就用原标题
            
            # 写入文件
            f.write(f"【{new_title}】\n")
            f.write(f"(原标题:{chapter['title']})\n\n")
            f.write(content)
            f.write("\n\n" + "="*50 + "\n\n")
    
    print("全部完成!")

if __name__ == "__main__":
    main()

五、总结与优化方向

这次折腾下来,虽然功能实现了,但还有很多可以优化的地方:

  1. 异步爬虫:现在是单线程同步请求,太慢了,可以用aiohttp改成异步的,效率能提升好几倍。
  2. 本地模型部署:调用API要花钱,而且有网络延迟,可以试试用Ollama部署本地模型,比如Qwen2-7B,效果也不差。
  3. 排版优化:现在保存的TXT文件排版比较简单,可以进一步处理正文的换行和缩进,或者直接生成EPUB电子书。
  4. 多网站适配:现在的代码只针对一个网站,可以写个配置文件,适配不同的小说网站结构。

最后再啰嗦一句:爬虫技术是把双刃剑,大家一定要在合法合规的前提下使用,不要用于商业用途,也不要给目标网站造成太大压力。

好了,今天的分享就到这里。你们平时写爬虫都遇到过什么有趣的坑?或者有什么更好的标题生成技巧?欢迎一起交流。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐