Python爬虫入门实战:从零构建小说采集与TXT保存工具

📅 2026/8/17 8:56:22
Python爬虫入门实战:从零构建小说采集与TXT保存工具
1. 项目概述与核心价值最近在技术社区和论坛里看到不少朋友在问如何用Python爬取网络小说然后保存成本地TXT文件。这确实是一个非常适合Python初学者练手的项目它几乎涵盖了爬虫入门的所有核心环节发送请求、解析网页、处理数据、应对反爬、以及文件操作。我自己也经常用这个项目来带新人因为它目标明确成果可见写完代码就能立刻得到一本可以离线阅读的小说成就感直接拉满。简单来说这个项目就是编写一个自动化程序让它模拟浏览器访问小说网站把分散在各个章节页面的文字内容“抓取”下来然后按照章节顺序规整地保存到一个文本文件里。这解决了手动复制粘贴效率低下、容易出错、无法批量操作的问题。无论是为了离线阅读、文本分析还是作为学习数据这个脚本都很有用。它适合有一定Python基础想踏入爬虫领域或者想通过一个完整项目巩固requests、BeautifulSoup等库使用的朋友。接下来我会把这个项目拆解成清晰的步骤并附上我踩过坑后总结的实操要点和避坑指南。2. 项目整体设计与思路拆解2.1 核心流程与技术选型一个完整的爬虫项目其核心流程可以抽象为“请求-解析-存储”三部曲。对于爬取小说并保存为TXT这个具体场景我们需要将其细化。首先是请求环节。我们需要从小说目录页开始获取所有章节的链接。这里首选requests库因为它简单易用是HTTP请求的“瑞士军刀”。但直接使用可能会遇到网站反爬比如请求头校验、访问频率限制等。因此我们的设计思路是模拟真实浏览器的请求头User-Agent并在连续请求之间加入随机的时间间隔以降低被封IP的风险。对于更复杂的反爬如动态加载可能需要用到Selenium或Playwright但鉴于我们目标是入门到进阶先从静态页面入手所以暂不考虑。其次是解析环节。拿到网页的HTML源代码后我们需要从中“挖出”我们想要的章节标题和正文内容。这里的主力是BeautifulSoup。它的选择器语法非常直观类似于CSS选择器能快速定位到网页中的特定标签。我们的解析策略是先分析目标小说网站几个章节页面的HTML结构找到标题和正文内容所在标签的规律例如标题总是在h1 class\title\里正文总是在div id\content\里然后编写通用的提取规则。最后是存储环节。我们需要将爬取到的章节按顺序保存。TXT文件是最简单的选择使用Python内置的open()函数和文件操作即可。存储策略是以小说名创建或打开一个TXT文件然后循环遍历每一章将章节标题和正文内容以固定的格式比如标题单独一行正文空两格追加写入文件。这里要注意文件编码通常使用utf-8和写入模式使用追加模式a。2.2 目标网站分析与结构观察在写代码之前最关键的一步是“侦察”——手动分析目标网站的结构。这里我以一个常见的、结构相对清晰的小说网站为例进行说明请注意实际操作时请务必遵守网站的robots.txt协议并尊重版权。打开目录页首先找到小说的目录页面网址可能类似于https://www.example.com/book/12345/。右键点击页面选择“检查”或“查看网页源代码”。定位章节链接在源代码中搜索某个章节名如“第一章”观察它所在的HTML标签。通常章节链接会包裹在a标签中并且集中在一个列表容器里比如div class\list\下的多个li。我们需要找到这些a标签共通的CSS选择器路径。打开章节页点击一个章节链接进入内容页。同样右键检查。定位标题和正文标题观察章节标题所在的标签常见的是h1有时会有特定的class如h1 class\chapter-title\。正文这是核心。正文通常在一个div容器内这个容器的id或class名往往比较有特征如id\content\、class\chapter-content\等。你需要仔细辨别因为正文中可能混杂着广告、版权声明等无关文本它们可能被p标签包裹或者有特殊的class。我们的目标是找到只包含纯净正文的那个最内层标签。注意不同网站结构千差万别甚至同一网站的不同书籍模板也可能不同。没有一成不变的规则必须针对具体目标进行具体分析。这是爬虫工作中最具“手艺”的部分。2.3 工具与库的准备工欲善其事必先利其器。我们需要安装必要的Python库。建议使用虚拟环境如venv来管理项目依赖避免污染全局环境。# 安装核心库 pip install requests beautifulsoup4 # 可选用于添加延迟模拟人类操作 # pip install time (time是内置库无需安装) # 如果需要处理更复杂的反爬后续可考虑 # pip install seleniumrequests: 用于发送HTTP请求获取网页数据。beautifulsoup4: 用于解析HTML/XML文档提取所需数据。lxml: BeautifulSoup 的一个解析器速度较快。安装beautifulsoup4时会自动安装lxml或html.parser通常我们指定使用lxml。3. 核心代码实现与分步解析3.1 基础请求与反爬策略首先我们从发送一个简单的HTTP请求开始并为其穿上“伪装”。import requests import time import random from bs4 import BeautifulSoup def get_html(url): 发送HTTP GET请求获取网页HTML内容 :param url: 目标网址 :return: 响应文本HTML或 None如果请求失败 # 1. 设置请求头模拟浏览器访问 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } try: # 2. 发送请求设置超时时间 response requests.get(url, headersheaders, timeout10) # 3. 检查响应状态码200表示成功 response.raise_for_status() # 4. 通常需要指定编码很多中文网站是gbk或gb2312 response.encoding response.apparent_encoding # 自动判断编码或手动指定如 utf-8, gbk return response.text except requests.exceptions.RequestException as e: print(f请求失败: {url}, 错误信息: {e}) return None # 在连续请求之间添加随机延迟避免请求过快 def random_delay(min_sec1, max_sec3): delay random.uniform(min_sec, max_sec) time.sleep(delay) print(f等待 {delay:.2f} 秒...)代码解析与避坑请求头HeadersUser-Agent是必须的否则服务器可能识别为爬虫并拒绝服务。这里的值是一个常见的Chrome浏览器标识。Accept和Accept-Language让请求看起来更“自然”。异常处理网络请求充满不确定性必须用try...except包裹。response.raise_for_status()会在状态码不是200时抛出异常。编码问题中文网站乱码是常见坑。response.apparent_encoding是requests库推测的编码大多数时候准确。如果发现乱码可以查看网页源码中的meta charset...标签并手动设置response.encoding gbk等。随机延迟random_delay函数不是必须但强烈建议添加。毫无间隔地高频请求是触发反爬机制最快的方式。随机延迟模拟了人类阅读的不规律性。3.2 解析目录页获取章节链接拿到目录页的HTML后我们需要从中提取所有章节的URL和标题。def parse_index_page(html, base_url): 解析小说目录页获取所有章节的链接和标题 :param html: 目录页的HTML文本 :param base_url: 网站的基础URL用于拼接相对链接 :return: 包含(章节标题, 章节链接)的列表 if not html: return [] soup BeautifulSoup(html, lxml) chapter_list [] # 这里需要根据实际网站结构调整选择器 # 假设章节链接都在 idchapter-list 的div下的所有a标签里 chapter_links soup.select(#chapter-list a) for link in chapter_links: chapter_title link.get_text().strip() # 获取链接文本作为章节标题 chapter_url link.get(href) # 获取href属性 # 处理相对链接如果链接不是以http开头则拼接基础URL if chapter_url and not chapter_url.startswith(http): # 注意这里需要根据网站实际情况处理有时是相对根路径有时是相对当前页 # 常见处理方式如果以/开头则是相对根路径否则是相对当前目录页路径 if chapter_url.startswith(/): chapter_url base_url.rstrip(/) chapter_url else: # 更复杂的处理可能需要urljoin这里简化示例 chapter_url base_url.rstrip(/) / chapter_url if chapter_title and chapter_url: chapter_list.append((chapter_title, chapter_url)) return chapter_list # 示例用法 base_url https://www.example.com index_url https://www.example.com/book/12345/ index_html get_html(index_url) if index_html: chapters parse_index_page(index_html, base_url) print(f共找到 {len(chapters)} 个章节) for title, url in chapters[:5]: # 打印前5章看看 print(title, url)实操心得soup.select(‘CSS选择器’)是神器比find_all更简洁强大。你需要熟练使用浏览器的开发者工具F12的“检查”功能右键点击元素选择“Copy” - “Copy selector”可以快速获得该元素的CSS选择器路径但通常需要简化。链接拼接是重灾区。一定要仔细检查爬取到的链接是否能正常访问。打印出前几个拼接好的URL手动在浏览器中打开测试这是调试的关键步骤。有些网站的目录是分页的或动态加载的滚动加载这就需要更复杂的处理比如拼接分页参数或者使用Selenium。我们这个入门项目先处理最简单的单页静态目录。3.3 解析章节页提取正文内容对于每个章节链接我们请求其页面并提取标题和正文。def parse_chapter_page(html): 解析小说章节页提取章节标题和正文内容 :param html: 章节页的HTML文本 :return: (章节标题, 正文内容) 元组 if not html: return None, None soup BeautifulSoup(html, lxml) # 1. 提取标题 - 根据实际网站调整选择器 # 假设标题在 h1 classchapter-title 标签内 title_tag soup.select_one(h1.chapter-title) chapter_title title_tag.get_text().strip() if title_tag else 未知标题 # 2. 提取正文 - 这是核心选择器必须精准 # 假设正文在 div idchapter-content 标签内的所有p标签里 content_div soup.select_one(#chapter-content) if not content_div: # 如果第一个选择器没找到尝试其他可能的选择器 content_div soup.select_one(.chapter-content) chapter_content if content_div: # 获取所有段落文本并过滤掉可能存在的广告、空行等 # 注意有些网站正文直接就在div的text里有些则分在多个p标签里 paragraphs content_div.find_all(p) if paragraphs: for p in paragraphs: text p.get_text().strip() # 简单的过滤去除纯数字、非常短的文本可能是广告 if text and len(text) 5 and not text.isdigit(): chapter_content text \n\n # 段落间加两个换行 else: # 如果没有p标签直接取div的文本 chapter_content content_div.get_text().strip() # 3. 简单的文本清洗 # 替换掉一些常见的网页字符或无关内容 import re chapter_content re.sub(r\s, \n, chapter_content) # 将多个空白字符包括空格、换行替换为单个换行 chapter_content re.sub(r[ ], , chapter_content) # 去除全角空格 # 可以添加更多自定义的清洗规则比如去除特定的广告文字 return chapter_title, chapter_content注意事项选择器的健壮性select_one(‘#chapter-content’)比find(‘div’, id‘chapter-content’)更简洁。但一定要准备备用选择器因为网站可能改版。代码中的if not content_div:就是一种容错处理。内容过滤网页正文里经常掺杂“请收藏本站”、“广告”等文字。除了在解析时通过标签选择避开在获取文本后也需要进行简单的清洗。上面的代码通过判断文本长度和是否为数字做了初步过滤你可以根据目标网站的特点增加更精确的规则比如正则表达式匹配特定广告词。编码与格式确保最终提取的文本是干净的字符串。使用strip()去除首尾空白用re.sub规整内部格式。3.4 整合流程与文件保存现在我们把所有环节串联起来并实现保存到TXT文件的功能。def crawl_novel(index_url, base_url, novel_name, start_chapter0, end_chapterNone): 爬取小说的主函数 :param index_url: 小说目录页URL :param base_url: 网站基础URL :param novel_name: 小说名称用于命名保存的文件 :param start_chapter: 开始爬取的章节索引从0开始 :param end_chapter: 结束爬取的章节索引不包含None表示直到最后 # 1. 获取目录页并解析章节列表 print(f正在获取目录页: {index_url}) index_html get_html(index_url) if not index_html: print(目录页获取失败程序退出。) return all_chapters parse_index_page(index_html, base_url) if not all_chapters: print(未解析到任何章节链接请检查选择器或网站结构。) return # 2. 根据参数切片确定要爬取的范围 if end_chapter is None or end_chapter len(all_chapters): end_chapter len(all_chapters) chapters_to_crawl all_chapters[start_chapter:end_chapter] total len(chapters_to_crawl) print(f计划爬取第 {start_chapter1} 章到第 {end_chapter} 章共 {total} 章。) # 3. 创建或打开TXT文件准备写入 file_name f{novel_name}.txt # 使用utf-8编码确保中文正常保存 with open(file_name, w, encodingutf-8) as f: # 可选在文件开头写入小说标题 f.write(f《{novel_name}》\n\n) f.write( * 50 \n\n) # 4. 循环爬取每一个章节 for i, (chap_title_in_index, chap_url) in enumerate(chapters_to_crawl, 1): print(f正在爬取 [{i}/{total}]{chap_title_in_index}) # 获取章节页面内容 chap_html get_html(chap_url) random_delay(1, 3) # 请求后延迟礼貌爬取 if not chap_html: print(f 章节页面获取失败跳过: {chap_url}) # 即使失败也在文件中记录一下保持章节顺序 f.write(f【第{i}章获取失败】{chap_title_in_index}\n\n) continue # 解析章节页面获取实际标题和正文 actual_title, content parse_chapter_page(chap_html) # 如果解析失败使用目录页的标题作为备选 title_to_save actual_title if actual_title else chap_title_in_index if not content: print(f 正文内容解析为空可能选择器有误或页面结构不同。) content f此章节内容解析失败原链接{chap_url} # 5. 将章节内容格式化后写入文件 f.write(f第{i}章 {title_to_save}\n) f.write(- * 40 \n) f.write(content \n) f.write(\n * 50 \n\n) # 章节分隔符 print(f 已保存: 第{i}章 {title_to_save}) print(f\n爬取完成小说已保存至: {file_name}) # 主程序入口 if __name__ __main__: # 需要你手动修改的配置部分 TARGET_INDEX_URL https://www.biquge.com.cn/book/12345/ # 替换为实际目录页URL BASE_SITE_URL https://www.biquge.com.cn # 替换为网站根域名 NOVEL_NAME 我的第一本爬取的小说 # 你想保存的小说文件名 # 可选只爬取部分章节用于测试 # crawl_novel(TARGET_INDEX_URL, BASE_SITE_URL, NOVEL_NAME, start_chapter0, end_chapter5) # 爬取全部章节 crawl_novel(TARGET_INDEX_URL, BASE_SITE_URL, NOVEL_NAME)核心要点与技巧文件操作使用with open(...) as f:上下文管理器可以自动安全地关闭文件。模式‘w’会覆盖原有文件如果希望断点续爬可以使用‘a’追加模式但需要更复杂的逻辑来记录进度。进度与容错循环中打印进度信息 ([{i}/{total}]) 非常有用。对于失败的章节请求失败或解析为空我们选择跳过并在文件中留下标记而不是让整个程序崩溃这保证了最大程度的完成度。格式化输出在TXT文件中使用固定的标记如“”和“-”组成的行来分隔章节能让生成的文件结构清晰便于阅读。参数化将开始和结束章节作为参数方便你进行测试只爬前几章或分批爬取。4. 高级话题与常见问题排查4.1 应对常见反爬机制简单的请求头伪装和延迟可能不足以应对所有网站。以下是一些进阶反爬策略及应对思路IP封锁这是最直接的。如果短时间内请求过多服务器可能会暂时或永久封禁你的IP。应对除了加大请求间隔如random_delay(3, 10)可以考虑使用代理IP池。有免费的代理IP网站但质量不稳定稳定的代理服务通常需要付费。在requests中使用代理很简单requests.get(url, headersheaders, proxies{‘http’: ‘http://ip:port’, ‘https’: ‘https://ip:port’})。User-Agent检测与轮换有些网站会检测单一的User-Agent。应对准备一个User-Agent列表每次请求随机选择一个。user_agents [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ..., # ... 更多UA ] headers[User-Agent] random.choice(user_agents)Cookie/Session验证部分网站需要登录后才能查看内容或者通过Cookie来维持会话状态。应对使用requests.Session()对象。它可以自动处理Cookie模拟浏览器会话。session requests.Session() # 可以先POST请求登录如果需要然后session会自动保存登录后的cookies # login_response session.post(login_url, datalogin_data) # 之后的请求都用session.get response session.get(url, headersheaders)动态加载内容越来越多的网站使用JavaScript在浏览器端渲染内容初始HTML是空的或只有骨架。用requests获取的html里没有章节正文。应对这是静态爬虫的极限。此时需要用到浏览器自动化工具如Selenium或Playwright。它们可以控制一个真实的浏览器如Chrome加载页面等待JS执行完毕再获取完整的HTML。但这会大大增加资源消耗和爬取时间。4.2 代码优化与工程化建议当脚本稳定运行后可以考虑以下优化让代码更健壮、更易维护配置与常量分离将URL、选择器、请求头、延迟时间等配置项提取到文件头部或单独的配置文件中避免硬编码。日志记录使用Python的logging模块替代print可以方便地控制日志级别INFO, WARNING, ERROR并将日志输出到文件便于后期排查问题。断点续爬爬取长篇小说时网络中断或程序出错可能导致前功尽弃。可以实现一个简单的断点续爬功能将已成功爬取的章节序号记录到一个文件中如progress.txt。每次启动时读取这个文件从断点处开始爬取。异步爬取对于I/O密集型的网络请求使用异步库如aiohttp配合asyncio可以极大提升爬取速度因为它可以在等待一个请求响应时去发送另一个请求。但异步编程复杂度较高且对目标网站压力大容易触发反爬需谨慎使用。4.3 常见问题排查速查表在爬虫开发过程中你几乎一定会遇到下面这些问题。这里提供一个快速排查的思路。问题现象可能原因排查步骤与解决方案返回状态码403Forbidden1. 请求头未正确设置尤其是User-Agent。2. IP被封锁。3. 网站需要特定的Cookie或Referer。1. 检查并完善headers添加Referer等字段。2. 尝试更换IP或使用代理。3. 使用浏览器开发者工具查看一次成功请求的Headers并完整复制过来。返回状态码404Not Found章节链接拼接错误。1. 打印出拼接后的完整URL手动在浏览器中访问测试。2. 检查parse_index_page函数中的链接拼接逻辑特别是相对路径的处理。获取到的HTML是乱码网页编码与requests默认解码方式不匹配。1. 打印response.encoding和response.apparent_encoding。2. 查看网页源码的meta charset标签。3. 尝试手动设置response.encoding ‘gbk’、‘gb2312’或‘utf-8’。解析不到章节链接或正文返回空列表CSS选择器写错了或者网站结构发生变化。1.最有效的方法将获取到的index_html或chap_html保存到一个临时html文件然后用浏览器打开对照着分析结构。2. 使用print(soup.prettify()[:2000])打印部分美化后的HTML寻找目标标签。3. 在开发者工具中使用更通用但可能不够精确的选择器先试一下比如soup.find_all(‘a’)看看链接是否在其中。爬取几章后突然中断或不再返回数据触发了网站的反爬机制如频率过高。1. 大幅增加请求间隔时间例如random_delay(5, 15)。2. 检查是否需要在headers中添加更多字段如Accept-Encoding。3. 考虑使用代理IP。保存的TXT文件内容杂乱有很多无关字符正文解析时没有清洗干净包含了脚本、样式、广告文本。1. 在parse_chapter_page函数中更精细地定位正文容器。尝试更内层、更具体的CSS选择器。2. 加强文本清洗逻辑使用正则表达式过滤掉明显的广告词、链接等。3. 如果广告是固定的HTML结构可以在BeautifulSoup解析阶段就直接decompose()掉那些广告标签。程序报错SSL: CERTIFICATE_VERIFY_FAILED遇到SSL证书验证问题某些网站或代理。在requests请求中添加参数verifyFalse注意这会降低安全性仅用于测试或信任的环境。requests.get(url, verifyFalse)4.4 法律与道德边界最后也是最重要的一点我们必须清醒地认识到爬虫的边界。遵守robots.txt在网站的根目录下如https://www.example.com/robots.txt通常有一个robots.txt文件它指明了网站允许和禁止爬虫访问的路径。虽然这不是法律文件但遵守它是行业惯例和道德要求。尊重版权与网站服务条款爬取的小说内容通常受版权保护。本项目的代码和思路仅用于个人学习、研究Python爬虫技术。切勿将爬取到的大量内容用于商业用途、公开传播或对原网站服务器造成显著压力DDOS攻击效果这可能侵犯著作权并违反相关法律法规。控制爬取速度设置合理的请求延迟本质上是对网站资源的尊重。一个疯狂的、不间断的爬虫请求可以被视为攻击行为。识别公开数据与私有数据通常公开在网页上无需登录即可查看的信息风险较低。但对于需要登录才能访问的内容爬取则涉及更高的法律和道德风险。这个项目就像一把螺丝刀可以用来做手工也可以用来搞破坏。请务必将它用在正途专注于技术学习本身并在实践中不断思考和优化。当你成功运行脚本看到TXT文件一页页被填满时那种解决问题的快乐才是编程最大的乐趣所在。如果在实现过程中遇到上面没覆盖的问题多利用打印语句调试、多对比浏览器中的实际页面结构问题总能解决的。