资讯动态

Python爬虫实战:从网站抓取小说并保存为文本的全流程解析

发布时间:2026/10/14 18:12:57 来源:尧图企业网站定制
1. 动手之前先想清楚一个“简单”爬虫项目的真实全貌先说结论把“任选一小说网站爬取任意一部小说以记事本形式保存”这件事真正跑通涉及的不只是会调一个网络请求库那么简单。你需要处理的细节包括页面结构解析、内容清洗、反爬机制应对、文件编码处理、断点续爬甚至还要考虑请求频率控制。我见过不少朋友拿着网上的示例代码跑通了一个网页换一个网站就全部报废原因就是只学会了“照着写”没学会“拆着看”。这个项目的输入逻辑很清晰给定一个小说网站给定一本小说输出一个干净的文本文件。听起来像是爬虫入门级任务但真正做下来你会发现“干净”二字是最大的难点。小说网站往往包含大量广告、推荐位、乱码章节、重复段落、缺章少节甚至还有防盗章节。如果只做表面功夫保存出来的“记事本”根本没法看更不用说后续拿去转电子书或做本地阅读了。适合谁来参考如果你已经掌握 Python 基础语法了解 requests 和 BeautifulSoup 的基本用法想找一个能串联起“请求-解析-清洗-存储”全流程的练手项目这篇内容非常适合。如果你完全没写过任何代码我也建议硬着头皮跟着敲一遍因为这是一个典型的“做完就能上手”的项目成就感来得很快而且踩坑的密集度足够帮你建立对网络数据的敏感度。我不打算直接甩一份代码让你复制而是把整个项目的思考过程、技术选型理由、坑点复盘全部摊开。因为后续你要往分布式爬虫、App 逆向、数据处理这些方向走的话前期建立的“拆解思维”比代码本身值钱得多。再补充一句可能扫兴但必须说的话爬虫本身是中性技术用在哪里决定了它的性质。如果是爬取盗版内容去传播盈利那不仅是法律风险问题也真的会让这个技术圈子变得更难混。我下面所有示例方案均以技术学习、个人备份、内容格式研究为前提。实际操作时我建议优先选那些已获得授权、允许爬取的内容源或者干脆拿自己发布的作品、公开协议允许抓取的资料来练手。理解机制比“什么都爬”重要得多。2. 技术选型与整体流程拆解2.1 为什么用 Python 而不是其他语言选 Python 做这个项目几乎是必然。原因有三一是生态成熟requests、BeautifulSoup、lxml、Scrapy 这些库把网络请求和 HTML 解析的复杂度降到了最低二是调试成本低交互式环境里写完一行就能立刻看到结果对“摸结构”阶段特别友好三是文本处理能力天然适配Python 的字符串方法、正则表达式、编码处理在这方面非常顺手。用 Java 或 Go 也能做但开发效率和代码量完全不是一个量级。Java 的 HttpClient 写起来啰嗦Go 的 HTML 解析器虽然性能好但社区的文档和示例数量不如 Python 丰富。对一个以“尽快跑通并理清逻辑”为目标的项目来说Python 是最优解。2.2 核心工具链的职责划分整个项目里每个工具都有明确的职责不能混着用requests负责发 HTTP 请求拿到目标页面的 HTML 源码。它是整个爬虫的“手”。BeautifulSoupbs4负责把 HTML 源码解析成可操作的对象树让我们能精准地定位章节标题、正文内容、下一页链接。它是“眼睛”。lxml作为 bs4 的解析引擎比 Python 自带的 html.parser 快很多。实际体感在大篇幅页面解析时差异明显所以推荐在 bs4 里指定 lxml。re正则表达式作为补充方案处理一些 bs4 不好搞的字符串匹配比如某些网站把正文藏在 JavaScript 变量里或者内容里混入了特殊标记。time控制请求频率避免对目标站点造成压力也降低被反爬机制盯上的概率。这是“刹车”。os处理文件路径、创建目录确保保存文件时不会因为路径不存在而报错。选型过程中最关键的决策点是“用不用 Scrapy”。我建议这个项目先不用。Scrapy 是强大的框架但它的工程化气质会在前期分散你的注意力——你会花大量时间理解 Item、Pipeline、Middleware 这些概念而不是聚焦在“怎么把小说正文干净地抠出来”这个核心问题上。等手上这个项目跑通了再迁移到 Scrapy 也就是半天的事因为核心逻辑你已经理解了。2.3 标准流程的五个阶段一个完整的小说爬虫项目按流程拆是五步目标结构分析、单页请求与解析、章节循环抓取、数据清洗与整合、文件保存与校验。目标结构分析是最容易偷懒但最不能偷懒的一步。你要搞清楚目录页 URL 的规律、正文页 URL 的规律、章节标题在哪个标签里、正文在哪个标签里、下一页按钮长什么样。这些信息搞清楚了后面的代码就是“浇上去”的事。单页请求与解析是验证环节。先固定一个 URL手动把 HTML 拉下来确认解析规则是否正确确认正文里有没有残留广告。这一步过了再写循环。章节循环抓取是整个项目的发动机。从目录页取出所有章节链接逐个请求正文页解析并写入文件。写这一步时异常处理要放进去不能因为一个章节的页面结构异常就让整个程序崩溃。数据清洗与整合往往被低估。实际上网站返回的正文可能带有多余的空行、广告段、乱码字符、章节内嵌的作者话。清洗思路要分成两层一是通用清洗比如去除空行和 HTML 标签二是站点专用清洗比如针对目标站点的广告段落模式做精确删除。文件保存与校验是收尾。编码选择很关键如果乱用 UTF-8 和 GBK生成的记事本打开会是一堆乱码。保存后还要做完整性检查比如章节数对不对、文件大小合理不合理、开头和结尾的内容完不完整。3. 完整实操全过程记录3.1 第一步分析小说网站的页面结构我先声明一点下面的步骤完全是方法演示。建议你自己选一个合法的、允许抓取或已授权的站点来练手我描述的是任何小说站都通用的分析思路。打开你选定的目标小说目录页右键“检查”打开开发者工具或者直接查看网页源代码。你需要找的信息是这几个章节链接的 HTML 结构和 URL 特征。通常情况是a href/chapter/12345.html章节名/a这样的形式你要确认所有章节链接是否都在同一个容器节点下。正文页的标题标签与正文容器。最常见的结构是h1放章节标题div idcontent放正文。是否有分页。大部分小说站一页显示全部正文但也有少数网站把一章拆成多页这种情况需要额外找“下一页”的链接特征。这一步最好的工具不是写代码而是浏览器。先用眼睛把结构看明白再动手写。我第一次做这类项目时急着写代码结果 HTML 里的标签层级搞得我焦头烂额。后来老老实实用浏览器先分析了三个页面代码半小时就写完了。3.2 第二步用 requests 拉取页面并确认状态代码从最小的验证单元开始写先不追求完整功能。目标只有一个能成功拿到页面 HTML。import requests 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, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8 } url 你选定的正文页URL response requests.get(url, headersheaders, timeout10) response.encoding response.apparent_encoding # 关键点按内容自动识别编码 print(response.status_code)这里有一个非常容易踩的坑编码问题。有些网站的页面是 GBK 编码你用默认的 UTF-8 去解码得到的 HTML 里全是乱码。response.apparent_encoding是 requests 根据内容推测的编码格式实测大部分小说站用它都能正确解码。但如果遇到个别识别错误的就需要手动确认页面源码里的 charset 声明然后硬编码成对应的编码值。headers 里的 User-Agent 必须带上。很多站点对裸请求直接返回 403加上一个浏览器 UA 就能绕过去。这只是最基础的伪装如果你发现加上 UA 仍被拦后面我会讲升级方案。3.3 第三步用 BeautifulSoup 解析并提取关键字段拉下来 HTML 之后开始解析。先用 lxml 引擎加载再用 find 和 select 方法提取内容。from bs4 import BeautifulSoup soup BeautifulSoup(response.text, lxml) # 章节标题 title soup.find(h1).get_text(stripTrue) # 正文内容 content_div soup.find(div, idcontent) if content_div: paragraphs content_div.find_all(p) content \n.join([p.get_text(stripTrue) for p in paragraphs]) else: # 有的站点正文直接用 br 换行没有 p 标签 content content_div.get_text(\n, stripTrue)这里要提醒一下get_text(stripTrue)默认是去掉字符串首尾的空格和换行但不会去掉段落间的内容。正文里如果混有广告段落你会在这一层原样取到。不要想着在解析阶段一次性搞定所有问题先取出来后面单独做清洗。如果拿到了 title 和 content你对打印结果做一次目检。如果正文很正常没有广告、没有乱码、没有多余标签那说明这个站点的结构很友善后面的路会顺畅很多。如果结果里带了一堆“推荐阅读”“最新章节”之类的干扰文字就把这些特征记下来后面清洗时要用。3.4 第四步实现目录页章节链接提取正文页验证通过后回到目录页。目录页的 URL 一般就是小说的首页章节链接通常集中在某一个节点下。我用 select 来定位并提取所有链接chapter_urls [] selector div.listmain a # 这里的 CSS 选择器按目标站点的实际结构调整 for a in soup.select(selector): href a.get(href) text a.get_text(stripTrue) if not href or not text: continue if not href.startswith(http): href urljoin(base_url, href) chapter_urls.append((text, href)) print(f共找到 {len(chapter_urls)} 个章节链接)urljoin这个函数很常用它能把相对路径自动拼接成完整 URL省得你自己写字符串拼接逻辑。提取出来的链接要检查一下范围有些站点的目录页里会混入“作品简介”“评论区”之类的非章节链接你要根据实际结构做过滤。网上很多例子代码裸用这段逻辑但实际跑到具体站上常常得自己调。还有一个细节urljoin需要从urllib.parse导入。这也是新手最容易漏掉的一个点报错的时候还一脸懵。3.5 第五步写主循环逐章抓取并保存到这里项目核心的循环逻辑可以组装了。我建议把代码拆成三个函数parse_chapter(url)负责解析单章并返回标题和正文fetch_chapter_list(url)负责解析目录页save_to_file(filename, chapters)负责把内容写进记事本。每个函数职责单一后期调试时能少掉很多头发。主循环里必须做的一件事是控制请求间隔。完全不做间隔的写法是for title, url in chapter_urls: chap_title, chap_content parse_chapter(url) all_content.append(f{chap_title}\n\n{chap_content})这段话打印出来后几乎可以肯定你很快会被封。正确的做法是加time.sleep(random.uniform(1, 2))每次请求之间休息 1 到 2 秒模拟人工阅读节奏。随机数比固定值更安全因为固定间隔本身就是机器行为特征。保存文件时用 UTF-8 编码存储并加上errorsreplace防止遇到特殊字符时整个程序崩溃with open(novel.txt, w, encodingutf-8, errorsreplace) as f: f.write(\n\n.join(all_content))Windows 记事本对 UTF-8 文件识别没问题但对 UTF-8 无 BOM 格式处理得很自然。如果你想让老版本记事本也完美显示可以手动写入 BOM 标记但我的建议是保持标准 UTF-8跨平台兼容性更好。3.6 第六步数据清洗让最终文件达到“可读”标准为什么单独拿出一节来讲清洗因为这是决定成品质量的核心环节。很多人的爬虫代码可以跑、文件也能存但打开后满屏广告和乱码根本没法直接阅读。第一步清洗空行和空白字符import re def clean_text(text): # 移除连续的空行 text re.sub(r\n\s*\n, \n\n, text) # 移除行首行尾的空白字符 text \n.join([line.strip() for line in text.split(\n)]) # 移除全角空格和特殊空白字符 text text.replace(\u3000, ).replace(\xa0, ) return text.strip()第二步清洗站点专用干扰词。比如该站频繁出现的“手机用户请访问xxx”这种广告可以用模式匹配直接删掉def remove_site_ads(text): # 按目标站点的广告特征替换 patterns [ r手机用户请访问[^\n]*, r请收藏本站[^\n]*, r最新网址[^\n]*, ] for pattern in patterns: text re.sub(pattern, , text) return text这一步没有统一公式完全依赖你对目标站点的观察。我建议在抓取时把每个章节的原始内容打印一部分出来目检一遍广告有哪些共同特征再写对应的正则表达式。需要注意的是清洗正则写得过宽会误删正文。比如有些小说的正文里确实会出现“请收藏”这样的句子但你把它当广告删了内容就不完整了。所以清洗规则一定是“越具体越好”宁可不删也不能误删。4. 反爬机制应对与请求策略优化4.1 常见反爬手段的识别方式做爬虫项目一定会遇到反爬。这不是什么神秘事情本质就是一个检测异常请求的规则系统。常见的手段可以分几个层级。第一层是基础的请求头校验。服务器检查 User-Agent、Referer、Accept-Language 等字段是否像真实浏览器。检查方式很简单如果代码里没带 headers请求直接返回 403 Forbidden那基本就是这一层。第二层是频率检测。单位时间内某个 IP 的请求次数超过阈值直接拒绝服务。表现出来的现象是程序跑着跑着忽然开始报 403 或 429重试也一样。这就是触发频率限制了。第三层是行为分析。服务器会分析访问轨迹是否像真人操作。比如正常人不会每秒钟访问一个页面持续好几天不会每次访问间隔都精确相等不会完全不访问图片 CSS 等静态资源。第四层是内容混淆。有些站点会把正文拆成乱序片段通过 JavaScript 在浏览器端重新拼接或者用图片代替文字或者给关键字段设置动态 token。这一层最麻烦因为它意味着你的爬虫已经不只是发 HTTP 请求那么简单还得模拟 JavaScript 执行环境。4.2 从“能跑”到“稳定跑”的升级路线如果带上 UA 仍然被拦可以做几件低成本高收益的事。第一补全请求头。除了 User-Agent把 Accept、Accept-Language、Referer 都加上。Referer 尤其重要因为很多站点只允许来自自身页面的请求访问详情页。你可以在请求正文页时把 Referer 设置为该小说的目录页地址。第二使用 Session 保持状态。有些站点的反爬机制会种 Cookie如果你每次都新建一个 requests.get服务器会认为你是新访客频繁变换 Cookie 很容易引起怀疑。用 session 对象管理请求session requests.Session() session.headers.update(headers) resp session.get(url, timeout10)第三引入请求间隔的随机性。真实的阅读节奏不可能固定所以间隔最好落在某个随机区间内。我实测下来的经验是1 到 2 秒的区间比较温和3 到 5 秒更安全但速度慢。短篇小说无所谓但如果你要抓几百章的长篇请求时长和被抓风险需要仔细权衡。第四如果目标站点要求 JS 渲染才能拿到内容requests 这条路就走不通了。这时候要切换方案用 Selenium 或者 Playwright 模拟真实浏览器。代价是速度和资源占用都成倍增加所以不是万不得已不用。4.3 频率控制的正确姿势很多人以为频率控制就是“每次请求后 sleep 一下”其实要考虑的细节比这多不少。首先是随机区间的选取不要用time.sleep(1)这种固定值要用random.uniform(1, 2)。其次是异常后的退避策略。如果请求失败不要立即重试而是先休息更长的时间比如 5 秒连续失败三次就停下来让网络状态恢复一下。这样能减少被误判的风险。再有一点是并发控制。这个项目不支持高并发因为文本站点的单次请求就把整章内容拿回来了没必要开线程池。如果你真的对速度有强烈需求最多做 2 到 3 个线程的顺序抓取即可并发高了对目标站点压力大对你自己也容易触发反爬。我在实际做这类项目时习惯的做法是每抓 50 章检查一下输出文件的体积确认章节数在正常增长。如果连续多个章节返回相同的错误内容说明目标站点可能已经改了结构或者封了 IP这时候再继续跑纯属浪费时间和带宽。5. 异常处理与断点续爬的工程化思维5.1 异常处理的三个层次很多初学的代码里只有一个裸的requests.get(url)网络一抖动就抛异常、程序直接崩溃。真实项目里这部分必须做分层处理。第一层是网络请求异常。包括 DNS 解析失败、连接超时、SSL 错误等。处理方式是捕获requests.RequestException记录日志后重试。第二层是解析异常。有些章节可能因为站点维护、广告位变化等原因导致结构不一样用 BeautifulSoup 提取正文时可能拿到 None。处理方式是捕获AttributeError和TypeError让当前章节重试而不是整个程序退出。第三层是业务层面的内容异常。比如标题为空、正文为空、正文长度异常短等。这类异常不会让代码报错但会让生成的记事本里出现残缺章节。处理方式是在保存之前做完整性校验。实际写起来可以按这个思路简化不追求教科书式的 try-except 嵌套而是优先保证“程序不因单点故障而死”for idx, (title, url) in enumerate(chapter_urls, start1): for retry in range(3): try: chap_title, chap_content parse_chapter(session, url) if not chap_title or not chap_content: raise ValueError(解析内容为空) break except Exception as e: print(f第 {idx} 章失败第 {retry 1} 次重试: {e}) time.sleep(5) else: print(f第 {idx} 章连续失败跳过) continue save_chapter(...) time.sleep(random.uniform(1, 2))这种“单章失败不影响全局”的思路对结构化爬虫项目非常重要。5.2 断点续爬不让前期工作白费抓长篇小说最烦的事情是已经抓了 300 章程序因为网络波动或服务器封禁中断了再从头跑又要花好几个小时。解决方法是在程序里加入进度记录机制。最简单的方案是维护一个本地进度文件。每次成功抓完一章就把这章的序号或 URL 写入进度文件。重新启动时读一下进度跳过已经完成的章节import os progress_file progress.txt completed set() if os.path.exists(progress_file): with open(progress_file, r) as f: completed set(line.strip() for line in f if line.strip()) for idx, (title, url) in enumerate(chapter_urls, start1): if url in completed: continue # 抓取并保存 ... with open(progress_file, a, encodingutf-8) as f: f.write(url \n)这里的细节是进度文件写入要用追加模式append而不是每次重写整个文件。因为如果程序中途崩溃重写模式下进度会丢失而追加模式最多丢失最后一行。说到这我得提一下目录页结构变化的风险如果目录页的章节链接顺序变了直接用enumerate记录序号就不靠谱了最好用 URL 作为唯一标识。URL 一般不会变但章节序号会变。5.3 日志打印的学问“print 大法”在调试阶段是神器但正式跑批量任务时必须升级成有分级、有上下文的日志。简单方案是写一个自定义的 log 函数把时间和消息组合起来import sys from datetime import datetime def log(msg): timestamp datetime.now().strftime(%Y-%m-%d %H:%M:%S) print(f[{timestamp}] {msg}) sys.stdout.flush()sys.stdout.flush()很关键。Python 的输出默认是有缓冲的程序非正常退出时最后的几条日志可能没来得及打印看起来像程序卡死在你没看到的地方。手动 flush 能保证每条日志实时落盘。多进程、多线程的日志处理比这个复杂很多但那个超纲了我建议在学会走之前不要跑。6. 常见问题排查手册与避坑心得6.1 高频问题速查表我在带新人做这个项目时整理过一份高频问题清单这里原样分享出来问题现象主要原因解决方案请求返回 403UA 缺失或被识别补全 headers换真实浏览器 UA页面乱码编码识别错误用apparent_encoding或检查 charset 声明抓到空正文正文由 JS 动态渲染换 Selenium / Playwright 方案抓到包含广告的正文广告在 HTML 中清洗规则处理按特征正则删除程序跑着跑着被断请求频率过高触发反爬加随机 sleep降低并发设置重试机制章节顺序错乱目录页排序变化或解析错误按 URL 去重校验章节序号保存文件中文乱码编码与打开方式不匹配统一 UTF-8并在代码中指定 encoding正文里有大量空行原 HTML 中就有换行清洗阶段合并空行这些问题的根源大多数都指向一句话对目标站点的页面结构理解不够深。所以排查问题的第一动作不是改代码而是重新打开浏览器观察页面。6.2 我踩过的最深的坑误把“能运行”当成“正确”有一回我做一个类似项目程序完美运行章节标题、章节数量都没问题文风也很顺但总感觉内容哪里不对。后来拿原站对比才发现正文里大量段落被重复拼接了因为网页里正文容器包含了一个不太显眼的叠加节点我用get_text()时把这个节点的内容也一并取到了。程序无异常报告文件大小也对得上但数据本身错了。这个教训折算成一句话就是爬虫项目的验证步骤不能只停留在“程序没报错”一定要抽样检查输出内容的正确性。具体到这个项目我建议至少检查三个位置的文件内容第一章开头的标题和首段、中间某一章的过渡处、最后一章的结尾部分。确认这些位置的文字和网站原文一致再宣布项目跑通。6.3 容易被忽略的站点结构“隐性差异”小说站的结构表面上很像但细节差异能坑死人。我举例几个你大概率会遇到的有的站点把目录页所有章节放在一个节点里有的则按分卷拆成多个节点你只选第一个节点会漏掉一部分章节。有的站点章节链接是相对路径有的带完整域名不统一处理就会有一部分链接失效。有的章节标题里带特殊符号如“·”“”等保存文件时按章节号命名会报错因为文件名不允许包含这些字符。有的正文尾部跟随类似“本章未完请点击下一页继续阅读”的标记你需要识别并跳过这些非正文段落。解决这些问题的统一思路是提取阶段尽量多观察清洗阶段以“精准规则”替代“一刀切”。6.4 关于“保存记事本”这个动作的进一步优化项目标题要求把小说保存成记事本这个动作本身不复杂。但如果你把它当作一个长期使用的工具有几个优化方向值得做。一是给文本文件加上章节序号。纯文本格式虽然不能用样式但章节名前面加“第 XXX 章”会大大方便阅读时定位。二是把抓取结果追加到一个文件而不是覆盖写。爬虫中断再续跑时不会因为每次重写文件导致数据覆盖不全。三是定期检查文件体积。如果抓了 200 章但文件只有几十 KB基本可以判断数据有问题要么清洗规则误删了正文要么解析环节大规模失败。四是可以考虑留一个“原始数据”目录和一个“清洗后”目录。原始数据保留页面拉下来的未经处理的内容清洗后再另存一份。这样后期如果发现清洗规则误删或者想重新清洗也不必重新爬一遍。这对项目效率的提升是很可观的。我记得有一次目标站点的正文结构改版我之前的清洗规则全部失效。幸好当时留了原始数据改完清洗函数后一次性重新生成省了再次抓取的几个小时。这个习惯后来被我带到了所有数据类项目里算是一条朴素但管用的经验。6.5 关于合规使用的最后叮嘱文章的最后我还是想把这个话题说透。爬虫技术本身是开发者的常规工具箱之一搜索引擎、信息聚合、数据研究都离不开它。但具体到小说爬取这个场景值得反复强调的是版权边界。网络小说是作者和平台的劳动成果未经授权的大规模抓取与传播不只是道德问题也可能带来实际的法律后果。所以我的建议是拿公共版权作品、自己创作的内容、或明确标明允许抓取的站点来练习。学习期间不要做全站批量抓取控制请求规模不给服务器带来额外压力。不要把自己写的小说爬虫工具公开打包出售或提供在线抓取服务。如果后续有真实的商业数据需求走官方 API 或授权合作是正道。技术上你把本文的方案吃透完全可以举一反三去处理新闻类网页、博客类页面、文档站等公开内容。爬虫能力是一种通用的信息获取方式思路通了载体并不重要。最后再分享一个小技巧也是我做完这类项目后养成的习惯。爬虫脚本不要写完就删至少保留一个精简版本把目录解析、正文解析、清洗规则、进度记录这四个模块独立保存一份。下次接到类似任务时真正的复用不是复制整段代码而是复制那套“分析页面 - 提取字段 - 按模块清洗 - 落盘校验”的流程。遇到新站点你在脑子里走一遍这个流程动手写代码的速度会快很多出错率也会低很多。这次的项目本身不大但把它当作工程系统来对待的收获会超出“一个记事本文件”的想象。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑