目录[-]

一、背景

之前爬取小说网站时,遇到几个痛点:

  • 章节列表格式不统一(有的返回列表,有的返回字典)
  • 章节标题经常是HTML实体编码(如
  • 小说内容夹杂大量广告和垃圾字符
  • 不同章节的编码格式不一致

所以写了一套完整的爬虫+清理工具,支持:

  • API方式获取章节列表和内容
  • 自动检测并修复编码问题
  • 智能清理广告和垃圾内容
  • 并行爬取,断点续传

 

二、技术架构

模块

技术栈

功能

爬虫

requests + ThreadPoolExecutor

API调用,并行下载

内容清理

正则表达式 + 自定义规则

去除广告、修复编码、清洗文本

文件管理

原生文件操作

TXT保存、备份、编码转换

整体结构:

novel_crawler/

├── crawler.py          # 核心爬虫(API版本)

├── content_utils.py      # 内容清理工具

├── run_crawler.py      # 启动入口

└── output/            # 输出目录    

└── save/             # 清理后的文件


三、核心实现

1. API接口分析

通过抓包发现小说网站的API结构:

  • https://apibi.cc/api/book?id={book_id} → 获取小说基本信息(书名、作者、dirid)
  • https://apibi.cc/api/booklist?id={dirid} → 获取章节列表
  • https://apibi.cc/api/chapter?id={book_id}&chapterid={chapter_id} → 获取章节内容
class ApiNovelCrawler:
    def __init__(self):
        self.base_url = "https://apibi.cc/api"
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            'Referer': 'https://www.57389b.sbs/',
        })

2. 章节列表解析(兼容多种格式)

不同小说的API返回格式不一样:

  • 有的返回字典列表 [{"id": 1, "title": "第一章"}, ...]
  • 有的返回字符串列表 ["1:第一章标题", "2:第二章标题", ...]
def _parse_chapter_info(self, chapter_item, index):
    if isinstance(chapter_item, dict):
        chapter_id = chapter_item.get('id') or chapter_item.get('num') or str(index)
        chapter_title = chapter_item.get('title') or chapter_item.get('name') or f'第{chapter_id}章'
    elif isinstance(chapter_item, str):
        if ':' in chapter_item:
            parts = chapter_item.split(':', 1)
            chapter_id = parts[0].strip()
            chapter_title = parts[1].strip()
        elif '章' in chapter_item:
            match = re.search(r'第(\d+)章\s*(.*)', chapter_item)
            if match:
                chapter_id = match.group(1)
                chapter_title = match.group(2) or f'第{chapter_id}章'
        # ... 其他格式处理
    return {'id': chapter_id, 'title': chapter_title, 'index': index}

3. 解码HTML实体

章节标题经常出现  这样的HTML实体,需要解码:

def decode_html_entities(text):
    import html
    import re
    decoded = html.unescape(text)

    def decode_hex(match):
        hex_str = match.group(1)
        try:
            return chr(int(hex_str, 16))
        except:
            return match.group(0)

    pattern = r'&#x([0-9a-fA-F]+);'
    decoded = re.sub(pattern, decode_hex, decoded)
    return decoded

4. 内容清理(核心)

小说内容中经常混入大量广告:

  • 笔趣阁www.biquge.com 等网站推广
  • 各种乱码符号 ♜♞♛♚♝
  • 孤立的英文字母和数字
  • 请记住本书首发域名 等垃圾文本
class ContentCleaner:
    def __init__(self):
        self.ad_keywords = [
            '笔趣阁', 'biquge', '请记住本书首发域名', '最新网址',
            '『点此报错』', '加入书签', '推荐票', '月票',
        ]

    def clean_content(self, content):
        lines = content.split('\n')
        cleaned_lines = []
        for line in lines:
            cleaned_line = self._clean_line_safe(line)
            if cleaned_line:
                cleaned_lines.append(cleaned_line)
        return '\n'.join(cleaned_lines)

    def _clean_line_safe(self, line):
        # 1. 移除广告关键词
        for ad in self.ad_keywords:
            line = line.replace(ad, '')

        # 2. 清理特殊符号
        special_symbols = r'[Θθ⊕⊙⊗¤•·∙▪●◎○◇◆□■△▲▽▼☆★※♜♞♛♚♝♟♔♕♖♗♘♙♠♣♥♦§¶†‡‰‱‽⁈⁉ヽ♀♂]'
        line = re.sub(special_symbols, '', line)

        # 3. 清理孤立的小写字母和数字
        line = re.sub(r'\b[a-z]{1,3}\b', '', line)
        line = re.sub(r'\b\d{1,3}\b', '', line)

        # 4. 清理多余空格
        line = re.sub(r'\s+', ' ', line)
        return line.strip()

 5. 并行爬取

使用 ThreadPoolExecutor 实现并发下载:

def crawl_novel_parallel(self, book_id, start_chapter=1, max_workers=20):
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        future_to_chapter = {}
        for chapter in chapters_to_crawl:
            future = executor.submit(self._crawl_single_chapter, book_id, chapter, progress)
            future_to_chapter[future] = chapter

        for future in as_completed(future_to_chapter):
            chapter_data = future.result()
            results.append(chapter_data)

6. 文件保存与备份

def save_to_txt(self, novel_data, filename, create_backup=True):
    # 备份已存在的文件
    if create_backup and os.path.exists(txt_path):
        backup_filename = f"{filename}_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
        shutil.copy2(txt_path, backup_path)

    # 写入TXT
    with open(txt_path, 'w', encoding='utf-8') as f:
        f.write(f"《{title}》\n")
        f.write(f"作者:{author}\n")
        f.write("=" * 50 + "\n\n")

        for chapter in chapters:
            f.write(f"{chapter['chaptername']}\n")
            f.write(chapter['content'] + "\n\n")

四、踩坑记录

1:文件编码问题

  • 现象:读取TXT文件时报 UnicodeDecodeError
  • 原因:有的文件是GBK编码,有的是UTF-8
  • 解决:先用 chardet 检测编码,再读取;失败时遍历常见编码重试
def detect_encoding(self, file_path):
    with open(file_path, 'rb') as f:
        raw_data = f.read(10000)
    result = chardet.detect(raw_data)
    return result['encoding'] or 'utf-8'

2:章节标题乱码

  • 现象:标题显示  这样的字符
  • 解决:先用 html.unescape 解码,再用正则处理16进制实体

3:广告词动态变化

  • 现象:每次爬取都会出现新的广告词
  • 解决:先用关键词列表过滤,再通过正则识别 字母+数字 的广告模式

4:API返回数据格式不统一

  • 现象:有的返回 list,有的返回 dict,有的直接是字符串
  • 解决:写兼容函数,判断类型后分别处理

5:并发导致请求失败

  • 现象:并发数过高时出现超时
  • 解决:控制 max_workers=10,添加重试机制(最多3次)

五、优化方向

  1. 断点续传:记录已爬取的章节索引,中断后可继续
  2. 数据库存储:换成SQLite/MySQL,方便检索和管理
  3. 分布式爬取:对于超长小说(几千章),可以多机并发
  4. 增量更新:定期检测新章节,只爬取增量部分

六、使用示例

from crawler import ApiNovelCrawler

crawler = ApiNovelCrawler()
crawler.crawl_by_book_id(
    book_id="158231",
    start_chapter=1,
    max_workers=10
)

爬取完成后,小说会保存到 output/save/ 目录,文件名格式为 《书名》_-作者_完整版_cleaned.txt。


七、总结

这套爬虫的核心价值不在于“爬取”,而在于“清洗”。网上90%的小说爬虫教程只教你如何获取数据,但没人告诉你获取到的数据有多脏。

我的做法是:宁可多花50%的时间在清洗上,也不要让垃圾数据污染你的阅读体验。

原文如下

《诛仙》
作者:萧鼎
下载时间:2025-12-11 11:25:44
==================================================

序章
----------------------------------------
时间:不明,应该在很早、很早以前hxyl8· cc
地点:神州浩土hxyl8· cc
天地不仁,以万物为刍狗!
这世间本是没有什么神仙的,但自太古以来,人类眼见周遭世界,诸般奇异之事,电闪雷鸣,又有天灾人祸,伤亡无数,哀鸿遍野,决非人力所能为,所能抵挡hxyl8· cc遂以为九天之上,有诸般神灵,九幽之下,亦是阴魂归处,阎罗殿堂hxyl8· cc
于是神仙之说,流传于世hxyl8· cc无数人类子民,诚心叩拜,向着自己臆想创造出的各种神明顶礼膜拜,祈福诉苦,香火鼎盛hxyl8· cc
自古以来,凡人无不有一死hxyl8· cc但世人皆恶死爱生,更有地府阎罗之说,平添了几分苦惧,在此之下,遂有长生不死之说hxyl8· cc
相比与其他生灵物种,人类或在体质上处于劣势,但万物灵长,却是绝无虚言hxyl8· cc在追求长生的原动力下,一代代聪明才智之士,前赴后继,投入毕生精力,苦苦钻研hxyl8· cc至今为止,虽然真正意义上的长生不死仍未找到,却有一些修真炼道之士,参透些天地造化,以凡人之身,掌握强横力量,借助各般秘宝法器之力,竟可震撼天地,有雷霆之威hxyl8· cc而一些得到高深的前辈,更传说已活上千年之久而不死hxyl8· cc世上之人以为得道成仙,便有更多人投入修真炼道之路hxyl8· cc
神州浩土,广瀚无边hxyl8· cc唯有中原大地,最是丰美肥沃,天下人口十之八九聚居于此hxyl8· cc而东南西北边荒之地,山险水恶,多凶兽猛禽,多恶瘴毒物,亦多蛮族夷民,虏毛饮血,是以人迹罕至hxyl8· cc而人间自古相传,有洪荒遗种,残存人世,藏于深山密谷,寿逾万年,却是无人得见hxyl8· cc
时至今日,人间修真炼道之人,多如过江之鲫,数不胜数hxyl8· cc又以神州浩土之广阔,人间奇人异士之多,故修炼之法道林林总总,俱不相同hxyl8· cc长生之法还未找到,彼此间却逐渐有了门派之分,正邪之别hxyl8· cc由之而起的门户之见,勾心斗角乃至争伐杀戮,在所多有hxyl8· cc
当长生不死看起来那般遥远而不可捉摸,修炼中所带来的力量,便逐渐成了许多人的目标hxyl8· cc
方今之世,正道大昌,邪魔退避hxyl8· cc中原大地山灵水秀,人气鼎盛,物产丰富,为正派诸家牢牢占据hxyl8· cc其中尤以“青云门”、“天音寺”、和“焚香谷”为三大支柱,是为领袖hxyl8· cc
这个故事,便是从“青云门”开始的hxyl8· cc
————————

清洗后如下,虽然效果并非十分完美,但已经去除了大量广告内容。

《诛仙》
作者:萧鼎。
下载时间:2025-12-11 11:25:44
==================================================

序章。
----------------------------------------
时间:不明,应该在很早、很早以前
地点:神州浩土
天地不仁,以万物为刍狗!
这世间本是没有什么神仙的,但自太古以来,人类眼见周遭世界,诸般奇异之事,电闪雷鸣,又有天灾人祸,伤亡无数,哀鸿遍野,决非人力所能为,所能抵挡 遂以为九天之上,有诸般神灵,九幽之下,亦是阴魂归处,阎罗殿堂
于是神仙之说,流传于世 无数人类子民,诚心叩拜,向着自己臆想创造出的各种神明顶礼膜拜,祈福诉苦,香火鼎盛
自古以来,凡人无不有一死 但世人皆恶死爱生,更有地府阎罗之说,平添了几分苦惧,在此之下,遂有长生不死之说
相比与其他生灵物种,人类或在体质上处于劣势,但万物灵长,却是绝无虚言 在追求长生的原动力下,一代代聪明才智之士,前赴后继,投入毕生精力,苦苦钻研 至今为止,虽然真正意义上的长生不死仍未找到,却有一些修真炼道之士,参透些天地造化,以凡人之身,掌握强横力量,借助各般秘宝法器之力,竟可震撼天地,有雷霆之威 而一些得到高深的前辈,更传说已活上千年之久而不死 世上之人以为得道成仙,便有更多人投入修真炼道之路
神州浩土,广瀚无边 唯有中原大地,最是丰美肥沃,天下人口十之八九聚居于此 而东南西北边荒之地,山险水恶,多凶兽猛禽,多恶瘴毒物,亦多蛮族夷民,虏毛饮血,是以人迹罕至 而人间自古相传,有洪荒遗种,残存人世,藏于深山密谷,寿逾万年,却是无人得见
时至今日,人间修真炼道之人,多如过江之鲫,数不胜数 又以神州浩土之广阔,人间奇人异士之多,故修炼之法道林林总总,俱不相同 长生之法还未找到,彼此间却逐渐有了门派之分,正邪之别 由之而起的门户之见,勾心斗角乃至争伐杀戮,在所多有
当长生不死看起来那般遥远而不可捉摸,修炼中所带来的力量,便逐渐成了许多人的目标
方今之世,正道大昌,邪魔退避 中原大地山灵水秀,人气鼎盛,物产丰富,为正派诸家牢牢占据 其中尤以“青云门”、“天音寺”、和“焚香谷”为三大支柱,是为领袖
这个故事,便是从“青云门”开始的

第一章 青云。