当前位置: 首页 > news >正文

Python小说章节自动采集入库工具:含MySQL连接池、去重建表与配置化部署

本文还有配套的精品资源,点击获取

简介:直接可用的小说内容采集入库方案,用Python实现从主流小说站点批量抓取章节标题、正文、作者、书名、更新时间等结构化数据;内置安全配置文件dbMysqlConfig.cnf管理数据库账号密码,mysql_DBUtils.py提供带重试机制的MySQL连接池封装,book_db.py定义标准数据模型和插入逻辑,支持自动建表、主键去重、字段映射;配套爬取小说存入数据库.md文档说明环境安装、参数配置、字段含义及常见问题,Day04目录下包含book_db_test.py等阶段性验证脚本和真实输出样例,requirements.txt列出依赖包,.gitignore和.inscode适配开发协作场景,整体设计面向实际部署与教学复现。

1. 项目概述:这不是一个“爬虫教程”,而是一套能直接跑通的小说数据基建脚本

我做内容平台技术支撑有八年多了,从最早手动导Excel小说目录,到后来写定时任务拉接口,再到如今这套真正能进生产环境的小说章节采集入库工具——它不是为了炫技,而是为了解决三个每天都在发生的现实问题:第一,编辑同事反复问“这本书最新章到底入库没?”;第二,新上架的网文站点结构一变,旧爬虫就全挂,运维得半夜爬起来改正则;第三,测试环境和线上环境数据库配置硬编码在代码里,一不小心就把测试库密码提交到了Git。这套工具就是我在给三家小说聚合平台做数据中台时,把踩过的坑、压测过的参数、上线后稳定跑了23个月的逻辑,全部沉淀下来的最小可用版本。

它核心就干一件事:把网页上看得见的小说章节,变成MySQL里可查、可关联、可统计的结构化记录。不依赖Scrapy这种重型框架,不用Docker编排,不搞分布式调度,就用最朴素的requests + BeautifulSoup + PyMySQL + DBUtils组合,但每个环节都加了生产级的防护:连接池防爆库、异常重试防瞬断、字段映射防字段错位、建表逻辑防重复执行、配置文件分离防密钥泄露。关键词里提到的“小说爬虫”“MySQL入库”“Python采集”“数据库连接池”“自动建表”,每一个都不是概念词,而是你打开终端敲几行命令就能验证的真实能力。比如book_db_test.py运行一次,你会看到控制台实时打印出“已插入《万古神帝》第1278章:青莲剑气(2024-06-15 22:17:03)”,同时MySQL里novel_chapter表多了一条带主键、带时间戳、带唯一索引的记录——这就是它和网上90%“教学爬虫”的本质区别:它默认就按生产标准设计,你删掉注释就能上线,而不是删掉注释才发现连数据库都连不上。

这套工具适合三类人:一是刚学完Python基础、想拿真实项目练手的新手,因为所有模块职责单一、命名直白、注释密集,mysql_DBUtils.py里连连接池最大连接数为什么设为15都写了计算依据;二是中小团队的技术负责人,需要快速搭建小说内容中台但又不想投入大周期开发,它提供完整的部署文档、字段说明、测试样例,甚至.inscode文件都配好了VS Code远程调试参数;三是内容运营同学,只要会改dbMysqlConfig.cnf里的host和port,就能让编辑部自己维护小说入库任务,不再事事找程序员。它不承诺“全自动识别所有网站”,但承诺“对主流小说站(起点、纵横、七猫、笔趣阁系)的典型结构,开箱即用且稳定率超99.2%”。后面你会看到,这个数字不是拍脑袋定的,而是基于我们实际监控的37个站点、连续180天的入库成功率统计得出的。

2. 整体架构与设计思路:为什么放弃Scrapy,坚持手写连接池与建表逻辑?

2.1 架构选型背后的四个硬约束

很多人看到“小说爬虫”第一反应就是上Scrapy,但我在这套工具里坚决放弃了它,原因很实在,来自过去三年服务客户时被反复打脸的四个硬约束:

第一,部署环境不可控。我们对接的客户里,有出版社的老旧服务器(CentOS 6.5 + Python 3.6),有云厂商的精简镜像(只开放80/443端口,禁用pip install),还有政务云的强审计环境(所有外网请求需走统一代理,且不允许后台常驻进程)。Scrapy依赖Twisted异步引擎,在CentOS 6.5上编译报错是常态;它的scrapy crawl命令本质是启动一个长期进程,而政务云要求所有任务必须通过HTTP触发、5分钟内完成并退出。这套工具用纯同步requests,单次运行即结束,requirements.txt里只列requests==2.31.0这种经过千次安装验证的稳定版本,连urllib3的版本都锁死,就是为了在任何Linux发行版上pip install -r requirements.txt后,python book_db_test.py一定能跑通。

第二,数据一致性优先于吞吐量。小说章节不是日志流水,漏一章可能影响整本书的推荐权重。Scrapy默认的并发模型在遇到网络抖动时容易丢请求,而我们要求“宁可慢一点,也要确保每章必达”。所以整个采集流程是串行+重试:先取目录页→解析章节URL列表→逐个GET正文页→解析标题/正文/时间→构造字典→调用book_db.insert_chapter()入库。insert_chapter()内部再做一次去重校验(主键冲突捕获),失败则记录日志并重试三次,三次都失败才跳过。这不是性能最优解,但它是业务零容忍下的唯一解。

第三,数据库操作必须原子化封装。网上很多爬虫示例把SQL拼接直接写在爬虫循环里,这在测试环境没问题,一上生产就是灾难:连接未关闭导致句柄耗尽、事务未提交导致数据丢失、异常未捕获导致程序静默退出。所以我们把所有DB操作抽成独立模块mysql_DBUtils.py,它只做三件事:初始化连接池、提供get_conn()获取连接、提供close_conn()归还连接。所有业务逻辑(包括建表、插入、查询)都在book_db.py里,通过DBUtils.get_conn()拿到连接后,严格遵循“获取→操作→提交→归还”四步闭环。这样哪怕book_db.py里某行代码抛出未捕获异常,连接池也能保证连接被正确回收——这是靠Scrapy中间件很难干净实现的。

第四,配置必须与代码物理隔离。曾经有个客户把测试库密码写死在settings.py里,结果误提交到公开仓库,当天就被扫库机器人拖走了27万条用户数据。这套工具强制使用dbMysqlConfig.cnf文件存储凭证,格式是标准INI:

[mysql] host = 192.168.1.100 port = 3306 user = novel_reader password = Aa123456! database = novel_db charset = utf8mb4

注意,password字段值里包含特殊字符!,很多教程用JSON或YAML存配置,遇到特殊字符就得转义,而INI天然支持。mysql_DBUtils.py加载时用configparser读取,全程不经过任何字符串拼接,杜绝SQL注入风险。.gitignore里明确排除dbMysqlConfig.cnf.inscode里也配置了VS Code不上传该文件——安全不是靠文档提醒,而是靠工程化约束。

2.2 自动建表逻辑:为什么不用ORM,而选择手写CREATE TABLE语句?

book_db.py里有一段被很多人忽略但极其关键的代码:

def init_table(): conn = DBUtils.get_conn() try: with conn.cursor() as cursor: cursor.execute(""" CREATE TABLE IF NOT EXISTS novel_chapter ( id BIGINT PRIMARY KEY AUTO_INCREMENT, book_id VARCHAR(64) NOT NULL COMMENT '小说唯一标识', chapter_id VARCHAR(64) NOT NULL COMMENT '章节唯一标识', title VARCHAR(255) NOT NULL COMMENT '章节标题', content LONGTEXT NOT NULL COMMENT '章节正文', author VARCHAR(100) NOT NULL COMMENT '作者名', book_name VARCHAR(255) NOT NULL COMMENT '书名', update_time DATETIME NOT NULL COMMENT '更新时间', create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '入库时间', UNIQUE KEY uk_book_chapter (book_id, chapter_id), INDEX idx_book_id (book_id), INDEX idx_update_time (update_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """) conn.commit() logger.info("novel_chapter表初始化完成") except Exception as e: logger.error(f"初始化表失败: {e}") conn.rollback() finally: DBUtils.close_conn(conn)

有人会问:Django ORM或SQLModel不是更优雅吗?答案是:优雅的前提是可控。ORM生成的建表语句在不同MySQL版本下行为不一致——比如VARCHAR(255)在MySQL 5.7和8.0对emoji的支持度不同,TIMESTAMP默认值在严格模式下会报错。而手写SQL能精确控制每个字段的COLLATEENGINECOMMENT,甚至预留了idx_update_time索引,这是为后续按时间范围批量导出章节做的准备。

更重要的是,“自动建表”不等于“每次运行都重建”。CREATE TABLE IF NOT EXISTS是核心,它保证脚本可重复执行:第一次运行创建表,第二次运行直接跳过。我们还在novel_chapter表里加了复合唯一索引uk_book_chapter (book_id, chapter_id),这是去重的物理基础。当insert_chapter()尝试插入重复book_id+chapter_id时,MySQL直接抛出IntegrityError,我们在book_db.py里捕获这个特定异常,记录日志并返回False,而不是让程序崩溃。这种“用数据库约束代替代码逻辑”的设计,比在Python里维护一个内存Set去重,更可靠、更省内存、更易排查。

2.3 连接池参数的实测依据:为什么maxconnections=15?

mysql_DBUtils.py里连接池初始化代码是这样的:

pool = PersistentDB( creator=pymysql, maxusage=0, setsession=['SET AUTOCOMMIT = 1'], ping=0, closeable=False, threadlocal=True, host=config['host'], port=int(config['port']), user=config['user'], passwd=config['password'], db=config['database'], charset=config['charset'], cursorclass=pymysql.cursors.DictCursor, maxconnections=15 # 关键参数 )

这个maxconnections=15不是随便写的。我们做过三轮压测:第一轮用ab -n 1000 -c 50模拟高并发入库,发现连接池在maxconnections=10时,平均响应时间从120ms飙升到850ms,错误率12%;第二轮调到20,内存占用暴涨40%,但响应时间只降了5ms,性价比极低;第三轮锁定15,在单机8核16G环境下,持续1小时压测,平均响应时间稳定在135±8ms,错误率为0,连接复用率达92.7%。计算依据也很简单:假设单次入库耗时150ms,那么单连接每秒可处理6.67次请求;15个连接理论峰值是100QPS,而实际小说站点API限流通常在30-50QPS,留出50%余量应对突发流量,这个值刚好卡在性能与资源的黄金分割点。

提示:如果你的MySQL服务器配置更高(比如32核64G),可以把maxconnections调到25,但务必同步调整MySQL的max_connections参数,否则会出现“Too many connections”错误。我们在线上环境的MySQL配置是max_connections=500,为连接池留足空间。

3. 核心模块详解与实操要点

3.1dbMysqlConfig.cnf:安全配置的三重防护

这个看似简单的INI文件,是我们整个安全体系的第一道闸门。它不只是存密码,而是承载了三重防护设计:

第一重:物理隔离。.gitignore里明确写着:

# 数据库配置文件,禁止提交 dbMysqlConfig.cnf

同时.inscode文件里配置了VS Code的Remote-SSH插件,当连接到生产服务器时,自动忽略该文件。这意味着即使开发者手滑执行了git add .,Git也不会把它纳入暂存区;即使他用SCP手动上传,VS Code也会弹窗警告“检测到敏感配置文件,是否确认上传?”

第二重:权限控制。在Linux服务器上,我们强制要求:

chmod 600 dbMysqlConfig.cnf # 仅所有者可读写 chown www-data:www-data dbMysqlConfig.cnf # 归属Web服务用户

这样Nginx或uWSGI进程能读取,但其他普通用户无法访问。曾经有客户没设权限,被同服务器的其他租户用find / -name "dbMysqlConfig.cnf"搜出来,导致数据库沦陷。

第三重:字段语义化。注意dbMysqlConfig.cnf里没有usernamepwd这种模糊字段,而是用userpassword——这和PyMySQL官方文档保持一致,避免因字段名不匹配导致静默失败。charset=utf8mb4更是关键,它确保emoji和生僻字(如“䶮”“龘”)能完整入库,而不是变成???。我们曾遇到某小说网站作者名含“𠮷”字(Unicode扩展B区),用utf8会截断,必须utf8mb4才能存全。

实操时最容易犯的错是:把password字段值用双引号包起来。INI规范里,值两侧的空格会被自动trim,但引号会被当作字符串一部分。比如:

password = "Aa123456!" # 错!密码实际成了"Aa123456!" password = Aa123456! # 对!这才是真实密码

mysql_DBUtils.py加载时不会报错,但连接会失败,日志里只显示“Access denied”,新手往往卡在这里半小时。所以我们在爬取小说存入数据库.md文档里专门用加粗强调:“密码值请勿加引号”。

3.2mysql_DBUtils.py:连接池封装的五个关键细节

这个文件只有87行,但每一行都经过生产环境千锤百炼。我们拆解五个关键细节:

细节一:PersistentDB而非PooledDBDBUtils提供两种连接池:PooledDB每次get_conn()都新建连接,PersistentDB则复用已有连接。小说采集是短连接高频场景(每次入库<200ms),用PersistentDB能减少TCP握手开销。我们实测过,同样1000次入库,PersistentDBPooledDB快1.8倍。

细节二:setsession=['SET AUTOCOMMIT = 1']这行代码把MySQL连接默认设为自动提交模式。为什么?因为小说入库是单条INSERT,不需要事务回滚。如果用默认的AUTOCOMMIT=0,每次INSERT后必须显式conn.commit(),一旦忘记,数据就卡在事务里不落盘。设为1后,INSERT即生效,代码更简洁,出错概率更低。

细节三:ping=0与心跳检测。ping=0表示不启用DBUtils内置的心跳检测,而是由我们自己控制。因为在高负载MySQL上,频繁SELECT 1会增加无谓压力。我们改为在get_conn()里加一层检测:

def get_conn(): conn = pool.connection() try: with conn.cursor() as cursor: cursor.execute("SELECT 1") # 主动执行轻量查询检测连接 except: conn.close() # 连接失效则关闭 raise return conn

这样既保证连接有效性,又避免无效心跳。

细节四:cursorclass=pymysql.cursors.DictCursor这让cursor.fetchall()返回字典列表而非元组列表,比如{'title': '第一章', 'content': '正文...'},而不是('第一章', '正文...')。虽然内存占用略高,但业务代码可读性提升巨大,book_db.py里直接row['title']就能取值,不用记索引位置。

细节五:threadlocal=True这是线程安全的关键。当多个线程(比如用concurrent.futures.ThreadPoolExecutor并发采集)同时调用get_conn()时,每个线程拿到的是自己的连接副本,不会互相干扰。我们在线上用8线程并发采集,从未出现连接错乱。

注意:mysql_DBUtils.py里所有日志都用logger而非print,且logger配置了文件输出。这样当脚本在后台运行(nohup python book_db_test.py > /dev/null 2>&1 &)时,错误依然能追查。日志格式是[2024-06-15 14:22:33] ERROR mysql_DBUtils.py:127 - 连接池获取失败: ...,带毫秒和文件行号,运维排查时效率极高。

3.3book_db.py:数据模型与插入逻辑的健壮性设计

这个文件定义了小说章节的数据契约。它的健壮性体现在三个层面:

第一层:字段映射防御。爬虫解析的HTML结构千差万别,有的网站用<h1 class="title">,有的用<div id="bookname">,还有的把标题藏在<meta property="og:title">里。book_db.py不负责解析,只负责接收标准化字典:

def insert_chapter(self, data: dict) -> bool: """ 插入章节数据,data必须包含以下key: - book_id: 小说唯一标识(如'qidian_123456') - chapter_id: 章节唯一标识(如'qidian_123456_ch1278') - title: 章节标题(非空) - content: 章节正文(非空) - author: 作者名(非空) - book_name: 书名(非空) - update_time: 更新时间(格式'YYYY-MM-DD HH:MM:SS') """

这个docstring就是契约。爬虫模块(不在本项目里,但会引用此模块)必须按此格式传参,否则insert_chapter()会主动抛出ValueError。我们拒绝“尽力而为”的模糊逻辑,坚持“契约先行”。

第二层:主键去重的双重保险。去重不是靠Python判断,而是靠数据库+代码双保险:

try: cursor.execute(sql, params) conn.commit() return True except pymysql.IntegrityError as e: if "Duplicate entry" in str(e): logger.warning(f"章节已存在,跳过: {data['book_id']}_{data['chapter_id']}") return False else: raise

这里捕获的是pymysql.IntegrityError,且只处理Duplicate entry错误。如果是其他错误(比如Data too long),直接抛出,让上游知道数据有问题。这种精准捕获,避免了“所有异常都吃掉”的反模式。

第三层:时间字段的强制校验。update_time必须是合法datetime字符串,否则入库失败:

from datetime import datetime try: datetime.strptime(data['update_time'], '%Y-%m-%d %H:%M:%S') except ValueError: raise ValueError(f"update_time格式错误,应为'YYYY-MM-DD HH:MM:SS',当前值: {data['update_time']}")

我们曾遇到某网站把时间写成“2024年6月15日 22:17”,这种中文格式必须由爬虫模块清洗成标准格式,book_db.py绝不妥协。这是保证数据质量的底线。

3.4爬取小说存入数据库.md:部署文档的实战颗粒度

这份Markdown文档不是说明书,而是我们的部署checklist。它把每个步骤拆解到终端命令级别,比如“安装依赖”不是写“运行pip install”,而是:

# 1. 创建虚拟环境(强烈推荐,避免污染系统Python) python3 -m venv novel_env source novel_env/bin/activate # Linux/Mac # novel_env\Scripts\activate # Windows # 2. 升级pip到最新版(解决某些旧pip安装wheel失败的问题) pip install --upgrade pip # 3. 安装依赖(-i参数指定清华源,国内访问更快) pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/

为什么强调虚拟环境?因为requirements.txt里有lxml==4.9.3,这个版本在系统Python 3.8上编译需要libxml2-dev,而新手往往不知道装。虚拟环境能隔离依赖,降低踩坑概率。

文档里还包含“字段含义速查表”,这是编辑部同事最爱的部分:

字段名类型是否为空含义示例
book_idVARCHAR(64)NOT NULL小说唯一标识,由爬虫生成,规则为站点缩写_小说IDqidian_123456,biquge_789012
chapter_idVARCHAR(64)NOT NULL章节唯一标识,规则为book_id_ch序号qidian_123456_ch1278
titleVARCHAR(255)NOT NULL章节标题,已去除前后空格和换行第一章 青莲剑气
contentLONGTEXTNOT NULL章节正文,已过滤广告、JS代码、多余空白符<p>林枫睁开眼...</p>
authorVARCHAR(100)NOT NULL作者名,已去除“著”“作者”等冗余词风凌天下
book_nameVARCHAR(255)NOT NULL书名,已去除副标题和括号内容万古神帝
update_timeDATETIMENOT NULL网站显示的更新时间,精确到秒2024-06-15 22:17:03
create_timeTIMESTAMPDEFAULT入库时间,由MySQL自动生成2024-06-15 22:17:05

这张表解决了90%的“这个字段怎么填”的疑问。比如book_id的生成规则,直接告诉开发人员“不要用UUID,要用站点+ID组合”,避免后期关联困难。

4. 实操过程与核心环节实现

4.1 从零开始部署:五分钟跑通第一个章节入库

我们以“采集笔趣阁《斗破苍穹》第一章”为例,演示完整流程。这不是理想化的演示,而是真实环境下的操作录像:

第一步:准备数据库

-- 登录MySQL,创建数据库(注意字符集!) CREATE DATABASE novel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 创建专用用户(最小权限原则) CREATE USER 'novel_reader'@'%' IDENTIFIED BY 'Aa123456!'; GRANT SELECT, INSERT ON novel_db.* TO 'novel_reader'@'%'; FLUSH PRIVILEGES;

这里强调utf8mb4和最小权限。曾经有客户用utf8建库,结果《斗破苍穹》里“药老”的“藥”字(繁体)存成??,读者投诉“主角名字都错了”。

第二步:配置dbMysqlConfig.cnf

[mysql] host = 127.0.0.1 port = 3306 user = novel_reader password = Aa123456! database = novel_db charset = utf8mb4

注意host127.0.0.1而非localhost,因为MySQL里localhost走socket连接,127.0.0.1走TCP,后者更稳定,尤其在容器环境。

第三步:运行测试脚本

# 激活虚拟环境 source novel_env/bin/activate # 运行测试(Day04目录下) cd Day04 python book_db_test.py

book_db_test.py内容很简单:

from book_db import NovelDB db = NovelDB() test_data = { "book_id": "biquge_123", "chapter_id": "biquge_123_ch1", "title": "第一章 药老", "content": "<p>斗气大陆,强者为尊...</p>", "author": "天蚕土豆", "book_name": "斗破苍穹", "update_time": "2024-06-15 22:17:03" } result = db.insert_chapter(test_data) print(f"入库结果: {result}") # 输出 True

运行后,控制台显示入库结果: True,同时MySQL里查:

SELECT * FROM novel_chapter WHERE book_id='biquge_123' \G # 输出: # id: 1 # book_id: biquge_123 # chapter_id: biquge_123_ch1 # title: 第一章 药老 # content: <p>斗气大陆,强者为尊...</p> # author: 天蚕土豆 # book_name: 斗破苍穹 # update_time: 2024-06-15 22:17:03 # create_time: 2024-06-15 22:17:05

看到create_timeupdate_time晚2秒,证明入库成功。整个过程,从建库到看到数据,不超过五分钟。

4.2 批量采集实战:如何安全接入新小说站点?

接入新站点不是改一行代码,而是一个标准化流程。我们以“纵横中文网”为例:

流程一:分析页面结构
用浏览器打开纵横《仙逆》目录页,F12看源码,找到章节列表的HTML结构:

<div class="chapter-list"> <a href="/book/123456/chapter/789012.html" title="第一章 逆凡">第一章 逆凡</a> <a href="/book/123456/chapter/789013.html" title="第二章 凡逆">第二章 凡逆</a> </div>

提取规律:章节URL路径是/book/{book_id}/chapter/{chapter_id}.html,标题在<a>标签的title属性里。

流程二:编写适配器(外部模块)
在项目外新建spider_zongheng.py

import requests from bs4 import BeautifulSoup def get_chapter_list(book_url: str) -> list: """获取纵横中文网章节列表""" resp = requests.get(book_url, timeout=10) soup = BeautifulSoup(resp.text, 'html.parser') links = soup.select('div.chapter-list a') chapters = [] for link in links: url = link['href'] title = link.get('title', '').strip() if not title or not url.startswith('/book/'): continue # 解析book_id和chapter_id parts = url.strip('/').split('/') if len(parts) >= 4: book_id = f"zongheng_{parts[1]}" chapter_id = f"zongheng_{parts[1]}_ch{parts[3].split('.')[0]}" chapters.append({ 'url': f"https://www.zongheng.com{url}", 'title': title, 'book_id': book_id, 'chapter_id': chapter_id }) return chapters def get_chapter_content(chapter_url: str) -> str: """获取纵横中文网章节正文""" resp = requests.get(chapter_url, timeout=15) soup = BeautifulSoup(resp.text, 'html.parser') content_div = soup.select_one('div.chapter-content') return str(content_div) if content_div else ""

注意:book_idchapter_id的生成规则,必须和book_db.py的契约一致,即站点缩写_唯一ID

流程三:集成入库

from book_db import NovelDB db = NovelDB() chapters = get_chapter_list("https://www.zongheng.com/book/123456") for chap in chapters[:5]: # 先试5章 content = get_chapter_content(chap['url']) data = { "book_id": chap['book_id'], "chapter_id": chap['chapter_id'], "title": chap['title'], "content": content, "author": "耳根", # 这里需要从目录页额外抓取 "book_name": "仙逆", "update_time": "2024-06-15 22:17:03" # 实际需从页面解析 } db.insert_chapter(data)

关键点:authorbook_name不能硬编码,必须从目录页解析。我们通常在get_chapter_list()里顺带抓取:

# 在get_chapter_list开头加 book_info = soup.select_one('div.book-info h1') book_name = book_info.get_text().strip() if book_info else "" author_tag = soup.select_one('div.book-info span.author') author = author_tag.get_text().replace('作者:', '').strip() if author_tag else ""

流程四:上线前检查清单
- [ ]book_idchapter_id是否全局唯一?用SELECT COUNT(*) FROM novel_chapter WHERE book_id='zongheng_123456'验证
- [ ]content字段是否过滤了纵横的广告JS?检查get_chapter_content()返回的HTML是否含<script>标签
- [ ]update_time是否解析正确?纵横的时间在<span class="time">里,需额外解析
- [ ] 并发数是否合理?纵横反爬较严,建议ThreadPoolExecutor(max_workers=3)

这个流程确保每次接入新站点,都有迹可循,不靠记忆,不靠运气。

4.3Day04目录深度解析:测试脚本的设计哲学

Day04不是随便起的名字,它代表“第四天交付的最小可行版本”。这个目录下有三个核心文件:

book_db_test.py:契约验证脚本
它不测试爬虫逻辑,只测试book_db.py的接口契约。输入一个符合docstring要求的字典,验证输出是否为True,且数据库有对应记录。这是TDD(测试驱动开发)的体现,保证book_db.py的API永远稳定。

test_output_sample.txt:真实输出样例
里面是某次真实运行的完整日志:

[2024-06-15 14:22:33] INFO book_db.py:89 - 开始插入章节: qidian_123456_ch1278 [2024-06-15 14:22:33] DEBUG mysql_DBUtils.py:45 - 获取连接池连接 [2024-06-15 14:22:33] INFO mysql_DBUtils.py:52 - 连接池当前活跃连接数: 1 [2024-06-15 14:22:34] INFO book_db.py:102 - 章节插入成功: qidian_123456_ch1278

运维同学遇到问题时,第一件事就是对比自己的日志和这个样例,看哪一步缺失,而不是盲目百度。

test_mysql_connection.py:独立连接诊断脚本

from mysql_DBUtils import DBUtils try: conn = DBUtils.get_conn() print("✅ 数据库连接成功") conn.close() except Exception as e: print(f"❌ 连接失败: {e}")

这个脚本只有5行,但它能快速区分问题是出在数据库(网络/权限/配置),还是出在业务逻辑。我们要求所有故障排查,必须先运行它。

实操心得:Day04目录的存在,把“部署”变成了可验证的动作。很多项目失败不是因为代码不行,而是因为没人定义“什么叫部署成功”。这里的三个脚本,就是成功的明确定义。

5. 常见问题与排查技巧实录

5.1 连接池相关问题速查

问题现象可能原因排查命令解决方案
pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on '127.0.0.1'")MySQL服务未启动,或dbMysqlConfig.cnfhost/port错误systemctl status mysqldtelnet 127.0.0.1 3306启动MySQL服务,或修正配置文件
pymysql.err.OperationalError: (1045, "Access denied for user 'novel_reader'@'localhost'")用户密码错误,或用户权限不足mysql -u root -p -e "SELECT User,Host FROM mysql.user;"CREATE USERGRANT重新授权
DBUtils: Connection pool exhausted并发数超过maxconnections,或连接未正确归还SHOW STATUS LIKE 'Threads_connected';降低并发数,或检查代码中是否漏掉DBUtils.close_conn(conn)
pymysql.err.InternalError: (1366, "Incorrect string value: '\\xF0\\x9F\\x92\\x96...'")MySQL字符集不是utf8mb4SHOW VARIABLES LIKE 'character_set%';执行ALTER DATABASE novel_db CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;

独家避坑技巧:当遇到Connection pool exhausted时,不要急着调高maxconnections。先运行SHOW PROCESSLIST;,看是否有大量Sleep状态的连接。如果有,说明代码里有连接未关闭。我们曾在一个客户的代码里发现,cursor.execute()后忘了conn.commit(),导致连接一直被占用。修复方法是在mysql_DBUtils.pyclose_conn()里加日志:

def close_conn(conn): if conn and conn.open: conn.close() logger.debug("连接已关闭")

这样就能定位到哪段代码没关连接。

5.2 数据入库问题排查

问题现象可能原因快速验证SQL解决方案
控制台显示入库结果: True,但MySQL里查不到数据INSERT语句没commit,或AUTOCOMMIT=0未生效SELECT * FROM novel_chapter ORDER BY id DESC LIMIT 1;检查mysql_DBUtils.pysetsession=['SET AUTOCOMMIT = 1']是否生效
titlecontent字段存为NULL或空字符串爬虫解析失败,传入了空值SELECT title,content FROM novel_chapter WHERE id=1;在爬虫代码里加assert data['title'].strip(), "title为空"
update_time存为0000-00-00 00:00:00时间字符串格式错误,strptime失败SELECT update_time FROM novel_chapter WHERE id=1;修改爬虫,用正则提取时间:re.search(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', text)
同一章节重复入库(主键冲突)book_idchapter_id生成规则有误,导致不同书ID相同SELECT book_id,chapter_id,COUNT(*) FROM novel_chapter GROUP BY book_id,chapter_id HAVING COUNT(*)>1;检查爬虫里book_id生成逻辑,确保站点缩写唯一

实操心得:我们在线上环境加了一个“入库健康检查”脚本,每天凌晨2点自动运行:

# health_check.py from book_db import NovelDB import datetime db = NovelDB() today = datetime.date.today() yesterday = today - datetime.timedelta(days=1) # 检查昨日是否有入库 count = db.get_chapter_count_by_date(str(yesterday)) if count == 0: send_alert("⚠️ 昨日无小说入库,请检查爬虫任务") # 检查重复率 dup_rate = db.get_duplicate_rate() if dup_rate > 0.5: send_alert(f"❌ 重复率过高: {dup_rate:.2%},请检查book_id生成逻辑")

这个脚本让我们在问题影响用户前就发现苗头。

5.3 配置与环境问题

问题:ModuleNotFoundError: No module named 'book_db'
原因:Python找不到模块路径。解决方案不是pip install,而是设置PYTHONPATH

export PYTHONPATH="${PYTHONPATH}:/path/to/your/project" python Day04/book_db_test.py

或者在代码开头加:

import sys sys.path.append('/path/to/your/project')

问题:UnicodeEncodeError: 'gbk' codec can't encode character '\U0001f4a5'
原因:Windows终端默认GBK编码,无法显示emoji。解决方案:在脚本开头加:

import io import sys sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

问题:pip install时报Failed building wheel for lxml
原因:lxml需要C编译器和libxml2。解决方案(Ubuntu):

sudo apt-get install libxml2-dev libxslt1-dev python3-dev pip install lxml==4.9.3

最后分享一个小技巧:所有配置文件(dbMysqlConfig.cnf)、所有测试脚本(book_db_test.py)、所有文档(爬取小说存入数据库.md),我们都用pre-commit做了钩子检查。比如pre-commit会自动扫描dbMysqlConfig.cnf里是否含password =,如果含则阻止git commit。这比靠人工检查靠谱一万倍。

这套工具没有魔法,它只是把我们踩过的每一个坑,都变成了可执行的代码、可验证的步骤、可复用的经验。当你跑通第一个章节入库时,你得到的不仅是一条数据库记录,而是一整套面对真实世界复杂性的思考框架——这,才是它真正的价值。

本文还有配套的精品资源,点击获取

简介:直接可用的小说内容采集入库方案,用Python实现从主流小说站点批量抓取章节标题、正文、作者、书名、更新时间等结构化数据;内置安全配置文件dbMysqlConfig.cnf管理数据库账号密码,mysql_DBUtils.py提供带重试机制的MySQL连接池封装,book_db.py定义标准数据模型和插入逻辑,支持自动建表、主键去重、字段映射;配套爬取小说存入数据库.md文档说明环境安装、参数配置、字段含义及常见问题,Day04目录下包含book_db_test.py等阶段性验证脚本和真实输出样例,requirements.txt列出依赖包,.gitignore和.inscode适配开发协作场景,整体设计面向实际部署与教学复现。


本文还有配套的精品资源,点击获取

http://www.jsqmd.com/news/980243/

相关文章:

  • vue3实现的纯前端护肤品商城网站
  • 无人机管理系统|完整源码交付,支持私有化部署与定制开发
  • 手把手教你用Simulink搭建永磁直驱风机并网模型(附单位功率因数控制与弱磁控制仿真)
  • 2026年6月岳阳楼区流量卡“闭眼入”指南:39元电信神卡杀疯了!
  • 鼻毛剪刀哪个牌子好?鼻毛器哪个牌子最好用?2026鼻毛修剪器第一名
  • 普元EOS平台深度体验:除了快速开发,它的监控治理工具EOS Governor到底有多强?
  • LLM多智能体语义传播监控与漂移治理方法
  • UniVidX——基于扩散先验的统一多模态视频生成框架
  • 小程序毕设选题推荐:基于python的档案室档案宝微信小程序基于python的档案室档案宝微信小程序【附源码、mysql、文档、调试+代码讲解+全bao等】
  • 手机拍证件照哪个好2026年专业证件照工具推荐
  • 51单片机控制16×16点阵LED,支持自定义文字滚动显示(含仿真+代码+文档)
  • 别再只当LCD驱动器了!解锁STM32 FMC的‘隐藏技能’:连接AD7606、OLED等并行总线设备
  • 逆向工程师的利器:手把手教你将OLLVM-14.x集成到Android NDK(Windows 10环境)
  • 告别迷茫!工业组态软件选型指南:从Qt、C#到Web,5分钟帮你找到最适合的技术栈
  • 类风湿关节炎 干细胞试验进展怎么样了?
  • 医院HIS药房模块实战避坑系列》之三:公立/私立医院药品调价模式对比:账务处理与行业演进
  • 基于STC89C52的智能洗衣机控制原型:三档面料适配+LCD实时显示+Proteus可运行仿真工程
  • 别再为VC++和LabVIEW报错头疼了!手把手教你搞定USB-CAN分析仪软件安装(附避坑指南)
  • 告别Softmax:YOLOv3的多标签分类与Binary Cross-Entropy Loss实战调优指南
  • XUnity Auto Translator:高效配置智能翻译插件的深度解析与实战指南
  • NCMconverter终极指南:3步解锁网易云音乐加密格式,免费实现ncm到mp3/flac批量转换
  • 从GISInternals官网到命令行:一份给Windows用户的GDAL 3.x 最新版避坑配置指南
  • Vue3后台模板:TypeScript + Element Plus 实现多标签页管理界面,零配置开箱即用
  • STM32F4 CANopen SDO通信避坑指南:心跳关了没?COB-ID算对了吗?
  • 存量老旧视觉项目智能化升级改造(五):人工全检工位改造 TVA 落地指南|三级报价模板 + 标准工期 + 全维度避坑清单
  • 别再买错卡了!Arduino+RC522复制门禁卡全指南:从M1 S50卡到UID卡避坑详解
  • 零基础可跑的MATLAB平面应力FEA代码包,含网格设置、求解与应力可视化
  • 从零到一:拆解一个开源QScada项目(HmiFuncDesigner),搞懂工业组态软件的核心模块设计
  • 小程序毕业设计-基于协同过滤算法的运动场馆服务平台微信小程序基于Springboot+微信小程序的协同过滤算法的运动场馆服务平台设计与实现(源码+LW+部署文档+全bao+远程调试+代码讲解等)
  • 从一根电缆的延时算起:深入理解1553B总线100米长度限制背后的工程考量