ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用Python自建小说下载器:网页正文提取、清洗与格式转换实战

用Python自建小说下载器:网页正文提取、清洗与格式转换实战 从小说App跳来跳去的那几年我最大的痛点就是书架散在各处这个平台有书城权限那个平台有独家番外等到想离线看的时候又被导出限制和格式兼容折磨到没脾气。后来我一怒之下自己写了一套下载脚本——输入单本书或批量书单抓公开页面的正文内容自动清洗广告和杂质再按需输出TXT、EPUB、HTML表格甚至CSV书单方便在WPS表格里管理和标记进度。这套东西我管它叫“番茄小说下载器”其实是个非常朴素的自建工具。它的核心不是“破解”什么而是把网页正文提取、清洗和格式转换这几个环节串起来省去复制粘贴的重复劳动。适用人群也很明确经常读网页版小说的朋友、需要把书转进阅读器或手机里离线看的人、以及刚接触Python想练手爬虫和文本处理的新手。这篇文章我会把从设计思路到完整脚本、再到典型问题排查全部摊开讲保证你看完能直接上手抄作业。1. 内容整体设计与思路拆解1.1 下载器的核心定位与功能边界先明确一件事它叫“下载器”但它不是那种号称“全网破解无限下载”的灰色工具。我设计的这个脚本目标很单一——把已经在浏览器里能正常阅读的公开网页正文提取下来转成本地文件。也就是说网页上能看到什么脚本抓的就是什么不绕过登录、不破解付费、不碰加密资源。为什么要做这个定位因为一旦涉及绕过平台规则就同时带来版权风险和技术风险。版权风险不用多说市面上很多小说下载器隔三差五被删库、被维权甚至开发者本人背上官司技术风险更实际平台的反爬措施会不断升级你依赖的那些“破解接口”一旦失效整个工具就废了。而我这个思路只处理公开页面稳定性反而更高因为普通正文页面的结构变动远没有接口那么频繁。从功能上看我把工具拆成了三层获取层负责访问网页并拿到章节列表和正文内容清洗层负责把页面里的广告、推荐语、脚本标签等噪音过滤掉输出层负责按照TXT、EPUB、HTML、CSV等格式完成转换。三层的设计逻辑是独立的哪一层出问题就单独修哪一层不会牵一发动全身。1.2 为什么不用现成下载器而要自建很多人会问网上不是有一堆现成的“小说下载器”吗直接下载一个不就行了我的回答是看情况但更建议自己掌握原理。第一是安全问题。现成下载器质量参差不齐不少来路不明的绿色版、破解版里面塞了广告插件或者挖矿脚本。你把一个不知名程序双击运行等于把自己的电脑和账号权限交出去。我见过有朋友为了下载一本小说安装之后浏览器首页被篡改静默装了三个全家桶最后只能重装系统。第二是兼容性。平台网页结构每隔几个月就改版一次现成工具更新跟不上你下载到的往往就是一片“404”或者空白章节。而自建脚本里当你发现抓不到内容改一个CSS选择器就能修好完全不需要等作者更新。第三是格式控制的灵活度。我希望最终的TXT是“书名/作者/正文”的干净结构希望生成的HTML能直接导入WPS表格做阅读记录希望EPUB在手机阅读器里目录不乱——这些定制需求市面上的工具很少能满足。自建工具可以把转换规则完全攥在自己手里。1.3 技术方案选型Python三件套足够整套脚本我只用了Python标准库加三个第三方库Requests做HTTP请求BeautifulSoup4做HTML解析EbookLib负责生成EPUB。如果只先跑通文本和HTML标准库里的urllib和html.parser也够用但没必要折磨自己这三个库都是久经考验的成熟方案。有人觉得爬虫一定要上Scrapy、Playwright这些重型框架其实对于“抓网页正文”这种需求杀鸡不用牛刀。Scrapy适合大规模分布式抓取但我们要的是小工具不是爬虫平台Playwright适合处理动态渲染页面但大部分小说正文页是服务端渲染的直接Requests就能拿到完整HTML。除非遇到极少数用JavaScript异步加载正文的站点再针对性地处理也不迟。选择这套技术栈还有一个原因它好解释、好调试。Requests挂代理、BeautifulSoup选节点每一步都能打印出来看中间结果对新手来说学习曲线非常平滑。我下面给的脚本就是按这个思路来你先在命令行跑通再根据自己的需求改规则。2. 核心细节解析与实操要点2.1 书籍页到章节列表的抓取逻辑下载一本小说通常分两步先拿到整本书的章节列表再逐个章节抓正文。第一步的难点在于不同网站的章节列表页结构差异很大。有的是在书籍页直接输出所有章节链接的静态列表这种最好处理有的则是点击“展开全部章节”之后才加载这中间可能涉及一个异步接口还有的会做分页每页显示20个章节需要翻页后再拼接。实操里的通用策略是先打印页面HTML看看结构。用Requests拿到HTML之后一次性打印前5000个字符用肉眼判断章节链接是直接写在HTML里还是需要额外请求接口。如果是静态列表那么用BeautifulSoup查找所有a标签再按href属性中包含章节关键词比如“chapter”、“c1”、“detail”等来过滤即可。拿到章节链接之后一定要做URL拼接。现在很多页面的href是相对路径比如/book/123/456.html直接拿它去请求会报错。正确的做法是用urllib.parse.urljoin让基础URL和相对路径自动拼接成完整链接。这个细节看着小但实际运行中一半的“抓不到章节”问题都出在这里。2.2 获取正文与内容清洗的细节正文页面比列表页简单但也有很多坑。我建议直接用CSS选择器定位正文容器比如查找div标签里面id或class包含content、article、chapter-content的那些节点。找到之后只用.get_text()提取文本就行HTML标签会被自动剥离。清洗是决定下载体验的关键环节也是最值得花心思的部分。页面正文里往往混着这些噪音章节标题、本站推荐语、作者有话说、广告锚文本、上一章下一章的导航文字。清洗规则我按优先级排序先把script、style、ins这类标签直接移除再用正则去掉形如“手机阅读请访问xxx”和“最新网址xxx”的一整行之后把连续两个及以上的换行符压缩成一个把全角空格、nbsp;统一替换成空字符串最后对每一行做一次长度判断少于10个字且不含句号的孤立行直接丢弃。清洗规则不要一次写得太狠否则容易误删正文段落。我的经验是宁少删勿多删先跑一章看看效果再一条一条加过滤规则。每次规则改动之后最好对着同一章正文跑一次diff确认没有把有效内容删掉。2.3 输出格式转换的基本思路格式转换这块看起来花样多本质都是“把清洗后的结构化文本重新包装”。TXT最简单把章节标题和正文段落按顺序写入一个文本文件中间用空行隔开再加个UTF-8编码声明就好。需要注意Windows记事本对UTF-8的BOM识别问题如果直接在记事本打开看到中文乱码就改成UTF-8 with BOM编码。HTML转换这里要多说一句因为很多人不知道“HTML格式转换WPS表格”是什么需求。其实就是把下载记录或书单生成成一个结构化的HTML表格里面包含书名、作者、最新章节、下载时间、阅读进度这些字段然后用WPS表格直接打开这个HTML文件表格数据就会被自动识别成行列结构方便筛选和排序。我实际使用中这个场景非常高频。EPUB转换稍微复杂一点它本质上是一个ZIP压缩包里面有mimetype、META-INF/container.xml、OEBPS/content.opf、OEBPS/toc.ncx这几个核心文件外加若干HTML章节文件。用EbookLib库可以省掉手写XML的麻烦你只需要添加章节、添加元数据最后调用epub.write()就能生成。如果你还需要MOBI或AZW3建议转换完EPUB之后用Calibre的命令行工具做二次转换因为MOBI的编码和元数据处理比较特殊纯Python做容易生成畸形文件。这个组合方案在电子书爱好者的圈子里非常成熟。3. 实操过程与核心环节实现3.1 环境准备在命令行里依次执行以下操作python -m venv novel_env source novel_env/bin/activate # Windows 下执行 novel_env\Scripts\activate pip install requests beautifulsoup4 ebooklib lxml这里创建虚拟环境是为了防止依赖冲突强烈不建议直接往全局Python环境里装。如果你平时用Anaconda也可以直接用conda create -n novel python3.9创建独立环境。安装完成之后建议先写一个最小请求测试确认网络没问题import requests url https://example.com/book/12345/ headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://example.com/ } resp requests.get(url, headersheaders, timeout10) print(resp.status_code) print(resp.encoding) print(resp.text[:200])这里设置timeout的作用是防止某个章节请求卡死导致整个脚本挂起。请求头里的Referer要填网站的首页或者书籍页很多轻量级反爬会校验这个字段缺了它可能返回403。3.2 下载核心脚本目录、正文、保存下面这个脚本是我实际在用的精简版覆盖了“获取章节列表-抓取正文-保存TXT”的完整流程import re import time import requests from bs4 import BeautifulSoup from urllib.parse import urljoin BASE_URL https://example.com/book/12345/ HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://example.com/, } def get_chapter_list(url): resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, lxml) chapters [] for a in soup.select(a[href*chapter]): title a.get_text(stripTrue) if not title: continue link urljoin(url, a[href]) chapters.append((title, link)) seen set() unique_chapters [] for item in chapters: if item[1] not in seen: seen.add(item[1]) unique_chapters.append(item) return unique_chapters def clean_content(html): soup BeautifulSoup(html, lxml) for tag in soup.find_all([script, style, ins]): tag.decompose() content_div soup.find(div, class_re.compile(rcontent|article, re.I)) if not content_div: text soup.get_text() else: text content_div.get_text(\n) lines [re.sub(r\s, , line).strip() for line in text.split(\n)] lines [line for line in lines if len(line) 2] return \n.join(lines) def download_book(chapter_list, save_path): with open(save_path, w, encodingutf-8) as f: for idx, (title, link) in enumerate(chapter_list, 1): resp requests.get(link, headersHEADERS, timeout10) resp.encoding utf-8 body clean_content(resp.text) f.write(f\n\n{title}\n\n) f.write(body) print(f[{idx}/{len(chapter_list)}] 下载完成: {title}) time.sleep(1) if __name__ __main__: chapters get_chapter_list(BASE_URL) print(f共找到 {len(chapters)} 章) download_book(chapters, output.txt)为什么clean_content里用正则re.compile(rcontent|article, re.I)来匹配class名因为不同网站的正文容器class命名五花八门有chapter-content、article-content、rich_media_content等等用正则包含匹配比写死某一个类名更实用。这里用了re.I忽略大小写算是个很老练的经验点。time.sleep(1)的作用是控制请求频率。你不想因为两秒钟请求十几张页面把网站服务器搞得有压力更不想被限流。如果书很长建议把这个间隔提到2到3秒虽然慢一点但稳定。3.3 转换脚本一键输出HTML表格适配WPS下载完之后要做一个书单管理功能把下载记录输出成HTML表格方便WPS表格直接打开。我这里写了一个独立的转换函数import html import csv from datetime import datetime def generate_html_report(records, output_path): rows [] for rec in records: rows.append( tr ftd{html.escape(rec[title])}/td ftd{html.escape(rec[author])}/td ftd{rec[chapters]}/td ftd{html.escape(rec[status])}/td ftd{rec[time]}/td /tr ) html_content f!DOCTYPE html html headmeta charsetutf-8title我的小说书单/title/head body h2书单记录/h2 table border1 cellspacing0 cellpadding6 trth书名/thth作者/thth章节数/thth状态/thth下载时间/th/tr {.join(rows)} /table /body /html with open(output_path, w, encodingutf-8) as f: f.write(html_content)用WPS打开这个HTML文件时它会自动识别table结构并转换成可编辑的单元格。这里有个细节meta charsetutf-8必须写在head最前面否则WPS可能按GBK去解析中文全部变成乱码。cellspacing和cellpadding参数是给表格加边框和留白让视觉效果更清爽。如果你更希望直接生成CSV文件可以用Python标准库的csv模块。需要注意newline这个参数Windows下如果漏了它每一行后面会多一个空行导入WPS后会看到行间距很夸张def generate_csv(records, output_path): with open(output_path, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[书名, 作者, 章节数, 状态, 下载时间]) writer.writeheader() for rec in records: writer.writerow(rec)这里用utf-8-sig编码就是为了Excel和WPS能直接识别UTF-8中文原理是文件开头加了BOM标识。3.4 批量化用书单CSV批量下载一次只下三五本书还可以手动运行脚本但如果你想整理一个五十本的书架就要用书单来驱动。思路是先维护一个CSV文件里面有四列书名、书籍URL、目标格式、保存路径。脚本循环读取每一行逐本调用下载和转换逻辑。import csv import os BOOK_LIST_CSV books.csv def download_from_csv(csv_path): with open(csv_path, r, encodingutf-8-sig) as f: rows csv.DictReader(f) for row in rows: title row[书名] url row[书籍URL] fmt row[目标格式] save_path fdownloads/{title}.{fmt} print(f开始下载: {title}) chapters get_chapter_list(url) if fmt in (txt, html): merge_to_txt_or_html(chapters, save_path, fmt) elif fmt epub: merge_to_epub(chapters, save_path) time.sleep(2)批量场景下最怕一个章节请求失败直接中断整个任务。我的做法是给每个章节的请求包一层try/except失败时把书名和URL写到error.log里最后统一处理。脚本跑完几十本书真正需要人工介入的可能就一两章重试一下就好。这里也顺带提一个时间成本计算。一本300章的小说每章请求加写文件大约1.5秒加上sleep 1秒总耗时约750秒也就是12分钟左右。如果你想让一个“一键获取”流程更快可以把sleep降到0.3秒并开启并发但并发会显著增加被封风险个人工具不建议。4. 常见问题与排查技巧实录4.1 提示“获取章节失败”或列表为空这是最常见的问题九成原因是选择器没匹配到。网站改版之后class名可能从chapter变成了catalog-item你原来的过滤条件就失效了。排查方法打印一次页面HTML搜索“目录”或“章”这些中文关键词定位到真实的章节列表容器再重新调整a[href*...]里的关键词。还有一个隐蔽原因页面启用了Cloudflare一类的人机验证Requests拿到的HTML里会包含cf-challenge字样这种情况说明站点有比较强的反爬拦截个人设备访问同样会触发不建议强行绕过换个源或者手动复制正文更稳妥。4.2 正文内容为空只抓到“加载中”正规站点很少出现这种情况但如果你适配的是SPA单页应用正文是JavaScript动态渲染的直接Requests就拿到一个空壳容器。判断方法是看响应文本里有没有类似window.__INITIAL_STATE__的变量如果有说明正文数据以JSON形式嵌在HTML里可以解析这个变量而不必启动浏览器。如果想快速解决而不是复杂化代码我这里有个曲线方案找到该站点提供的移动端页面地址很多SPA站点会为移动端保留服务端渲染版本URL规则通常是加/wap/或m.前缀。实际测试下来这招解决了很多动态加载导致的空白问题。4.3 生成的HTML用WPS表格打开后表格错乱发生错乱几乎都是标签闭合问题。比如某个字段值里包含未转义的或WPS在解析时把它当成了新标签的开始后面的列全部错位。解决方法是写HTML时强制对字段内容做html.escape()把换成lt;把换成amp;。另外表格的th数量必须和每行td数量严格一致WPS对不齐的行会自动补空单元格看起来就像“错位”。如果你用我上面的generate_html_report函数它已经处理了转义直接踩坑概率会小很多。4.4 生成的EPUB在手机上目录乱掉EPUB乱目录主要是章节标题层级没有规范写入阅读器无法自动提取目录。如果你用EbookLib记得给每一章设置chapter.title并且不要忘了在toc参数里传入这些标题。别嫌麻烦少了这一步手机上的阅读器只能把整本书当成一个超长文档翻页体验非常差。另外封面图如果不要就干脆不加加一个损坏的封面反而会让Apple Books这类严格校验的阅读器拒绝打开整本书。图片推荐用JPG或PNG尺寸控制在600x800以上但不至于超过2MB。4.5 网上热词的“鼠鼠格式转换”与安全提醒最近在有些社群里会看到“鼠鼠格式转换”这种说法听着像某款工具其实更多是网友对“抓取转换”这类脚本的调侃称呼。我不去深究这个词的具体出处但想借它提醒一下越是流传在小圈子里、来路不明的“格式转换器”越要警惕。你很难知道它除了转换格式还做了什么也许是上传你的文件也许是捆绑下载广告。我的原则是能用自己写的脚本完成的工作绝不随便运行网上下载的exe。如果你有不得不使用的第三方转换器建议放到虚拟机或者沙箱环境里先跑一次观察它是否有可疑的网络请求和文件写入行为。4.6 常见问题速查表问题现象排查优先级常用解法章节列表为空1. 页面结构是否改版 2. 选择器是否匹配 3. 是否触发人机验证打印HTML分析调整CSS选择器改用移动端URL请求返回4031. User-Agent是否缺失 2. Referer是否正确 3. 请求频率是否过快补全请求头降低频率增加随机延时中文乱码1. 页面编码判断 2. 写入编码是否统一resp.encoding按页面meta设置写入统一UTF-8TXT在手机阅读器里没封面阅读器不支持TXT封面转换EPUB时添加元数据和封面HTML导入WPS列错位1. 字段是否有未转义字符 2. td数量是否一致对输出字段做html.escape对齐表头和单元格我在实际写脚本的过程中最大的体会是不要一开始就想着做“全网通用”的下载器那几乎是维护无底洞。先把一个你常看的站点适配好跑通全流程再考虑抽象出可复用的函数。等到哪天网站改版了你改改选择器几秒钟就能恢复这种掌控感才是自己动手的最大回报。最后再分享一个小技巧如果你只是想快速把网页正文保存成PDF或Word不必单独写脚本直接用浏览器自带的“打印为PDF”再把PDF转成Word也行。但如果要处理几十本、上百本或者要在不同阅读器里保持统一的排版那套Python脚本方案绝对值得你花一晚上时间搭建起来。工具是死的思路是活的你完全可以在我的代码基础上改成适合自己习惯的工作流。
返回列表