20252436 赵明山 实验四 Python综合实践
爬虫实践
这是一个中文小说网站(如下图所示

我想要爬取其中一部小说,比如《万古神帝》。
而这需要用到有关爬虫的库,最主要的是requests和bs4
下面的是源代码
点击查看代码
\# 爬取小说(面向流程写法)
import os
import time
import requests
from bs4 import BeautifulSoup
url = "https://www.biquge365.net/newbook/83621/"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0"
}
response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, "lxml")
\# 获取小说名
name = soup.select("div > h1")[0].text
\# print(name)
\#获取数据链接
all_a = soup.select('ul.info>li>a')
hrefs = ["https://www.biquge365.net"+a["href"] for a in all_a]
\# print(hrefs)
\# 创建文件夹
os.makedirs(name, exist_ok=True)
for h in hrefs:
r = requests.get(url=h, headers=headers)
soup = BeautifulSoup(r.text, "lxml")
\# print(soup)
\# 获取标题
title = soup.select("#neirong h1")[0].text
\# 获取内容
content = soup.select("#txt")[0].get_text("/n",strip= True )
\#保存章节
with open(name+"/"+title+".txt", "w", encoding="utf-8") as f:
f.write(content)
print(f"{title}保存成功")
class NovelSpider:
def __init__(self,url):
self.url = url
self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0"
}
self.hrefs = None
self.name = None
def get_hrefs(self):
r = requests.get(url=self.url, headers=self.headers)
\# 解析返回的html文本 提取数据
soup = BeautifulSoup(r.text, "lxml")
\# 获取数据链接
all_a = soup.select('ul.info>li>a')
self.hrefs = ["https://www.biquge365.net"+a["href"] for a in all_a]
\# 创建文件夹+获取小说名
self.name = soup.select("h1")[0].text
os.makedirs(self.name, exist_ok=True)
def get_content(self, href):
r = requests.get(url=href, headers=self.headers)
soup = BeautifulSoup(r.text, "lxml")
\# 获取标题
title = soup.select_one("#neirong h1").text
\# 获取内容
content = soup.select_one("#txt").get_text("\n",strip= True ) \#研究研究
return title, content
\# 奇技淫巧之一行流
\# all_a = BeautifulSoup(r.text, "lxml").select("ul.info>li>a")
def save_txt(self, title, content):
with open(self.name+"/"+title+".txt", "w", encoding="utf-8") as f: \#复习的时候需要做相关的题
f.write(content)
print(f"{title}保存成功")
def run(self): \ # 回头看看
self.get_hrefs()
for href in self.hrefs:
title,content = self.get_content(href)
self.save_txt(title, content)
\# https://www.biquge365.net 该网站小说任意爬取
if __name__ == '__main__':
spider = NovelSpider("https://www.biquge365.net/newbook/39884/")
spider.run()
实验过程
- 首先,我要用request的话就需要获取到网页的url,最好连带其他的用户协议一起获取。
这里我找到了一个非常好的工具
就是这个curl命令转代码——https://tool.lu/curl/

https://www.biquge365.net/newbook -> https://www.biquge365.net/newbook/83621/ 通过这个网址的变化我确定了发起请求的是83621这一栏
接下来我复制curl(bush)复制粘贴到转码工具中,可以直接获得需要的python代码。


这就是我代码开头所有协议的获取办法。后来经过实践发现,这个网站特别简单,我一直删除到只剩下User-Agent都可以正常访问(那我之前的那一大长串算什么?
请求依旧通过,返回动态码200 访问成功了

2.之后就是把获取到的response.text转成lxml形式,方便后面获取信息(用bs4
soup = BeautifulSoup(response.text, "lxml")
后续的代码块中最常规的便是class类操作,其次便是文件的读写操作

其中我在鱼C社区中了解到了一行流代码写法,我把它写在代码注释当中,这里拎出来说是因为我没有看懂这个一行流是怎么写的
all_a = BeautifulSoup(r.text, "lxml").select("ul.info>li>a")
看起来好像是直接获取到了数据链接中的文本内容,然后可以直接写进文件操作里?
3.最后也是重点,就是beautifulsoup的使用,尤其是soup.slect的操作
这也是本人最抓麻的地方,就拿最简单的获取标题来说

代码写得简单 #####title = soup.select("#neirong h1")[0].text
但实际上因为我不了解前端设计,所以看这个html看的头疼。
再对比一下content = soup.select("#txt")[0].get_text("/n",strip= True )
比如soup.select.get_text后面的函数变量就需要现场去学习,还要研究select()中如何找到对应的html
当然,在获取到小说的标题和内容并进行保存文件的操作之后,我就可以正常的爬取小说内容了。
下面是代码运行的初步结果
为什么说是初步结果呢?因为我又把我写的代码丢给了AI,

当它说有5以上的核心功能都需要补充的时候我心想:开什么玩笑!(玩笑鬼复苏了bushi
接下来豆包老师开始发力了,我接受拷打并尝试理解代码
ps:我记得最新的有关python爬虫模仿用户行为的库叫 Playwright,但是我没学(网站也没反爬。
点击查看代码
# 导入需要的库
import os
import time
import requests
from bs4 import BeautifulSoup
class NovelSpider:
# 【修改1】修复初始化方法:必须是__init__双下划线,原代码init语法错误
def __init__(self, url):
self.url = url
self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0"
}
self.hrefs = None
self.name = None
self.total_chapter = 0 # 【新增功能】总章节计数
self.finish_chapter = 0 # 【新增功能】已完成章节计数
def get_hrefs(self):
try:
r = requests.get(url=self.url, headers=self.headers, timeout=10)
r.raise_for_status() # 请求异常抛出错误
r.encoding = r.apparent_encoding # 自动识别编码
soup = BeautifulSoup(r.text, "lxml")
# 获取所有章节链接
all_a = soup.select('ul.info>li>a')
self.hrefs = ["https://www.biquge365.net" + a["href"] for a in all_a]
self.total_chapter = len(self.hrefs)
# 获取小说名并创建文件夹
self.name = soup.select_one("div > h1").text.strip()
os.makedirs(self.name, exist_ok=True)
print(f"✅ 小说《{self.name}》初始化完成,共{self.total_chapter}个章节")
except Exception as e:
print(f"❌ 获取目录失败:{e}")
def get_content(self, href):
# 【新增功能2】增加延时,防反爬封禁,每章间隔0.3~0.8秒
time.sleep(0.5)
try:
r = requests.get(url=href, headers=self.headers, timeout=10)
r.raise_for_status()
r.encoding = r.apparent_encoding
soup = BeautifulSoup(r.text, "lxml")
# 获取章节标题和内容
title = soup.select_one("#neirong h1").text.strip()
content = soup.select_one("#txt").get_text("\n", strip=True)
return title, content
except Exception as e:
print(f"❌ 章节{href}获取失败:{e},3秒后重试...")
time.sleep(3)
return self.get_content(href) # 【新增功能3】失败自动重试
def save_single_txt(self, title, content):
"""分章节保存"""
file_path = os.path.join(self.name, f"{title}.txt")
# 【新增功能4】跳过已下载章节,断点续爬
if os.path.exists(file_path):
print(f"⏭️ {title}已存在,跳过")
self.finish_chapter += 1
return
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
self.finish_chapter += 1
# 【新增功能5】实时进度显示
progress = self.finish_chapter / self.total_chapter * 100
print(f"✅ {title}保存成功 | 进度:{self.finish_chapter}/{self.total_chapter} ({progress:.2f}%)")
def save_all_in_one(self):
"""【新增功能6】整本小说合并为单个TXT文件"""
all_content = ""
for href in self.hrefs:
title, content = self.get_content(href)
all_content += f"\n\n=========={title}==========\n\n{content}"
with open(f"{self.name}_全集.txt", "w", encoding="utf-8") as f:
f.write(all_content)
print(f"\n🎉 整本小说《{self.name}》全集保存完成!")
def run(self):
self.get_hrefs()
for href in self.hrefs:
title, content = self.get_content(href)
self.save_single_txt(title, content)
self.save_all_in_one()
# 【修改2】修复主函数入口语法错误,原代码if name == 'main'格式错误
if __name__ == '__main__':
# 爬取《万古神帝》
spider = NovelSpider("https://www.biquge365.net/newbook/83621/")
spider.run()
学习内容如下:
等一下,我有必要澄清一下,豆包说我的代码没有延时反爬,
其实本来是有的,结果我看网站对爬虫不敏感,为了提升效率就去掉了
豆包说我缺少异常捕获无进度、无断点续爬、无合并全文、缺少编码 / 超时处理等问题;
豆包老师的拷打来了,下面是豆包列举的缺点和我找的豆包改进的代码块
缺失点 1:无请求超时、无异常捕获(网络报错直接崩溃)
点击查看代码
try:
# 增加timeout=10,10秒无响应直接判定超时
r = requests.get(url=self.url, headers=self.headers, timeout=10)#timeout是啥内置函数,你不应该再写一个函数吗?
# 状态码非200直接抛出异常(404/500/403拦截)
r.raise_for_status()
# 自动识别网页编码,解决中文乱码问题
r.encoding = r.apparent_encoding
soup = BeautifulSoup(r.text, "lxml")
# 原有解析逻辑不变
except Exception as e:
# 捕获所有网络、解析错误并打印日志,程序不会直接终止
print(f"❌ 获取目录失败:{e}")
经过我的查找,元组timeout = (connect_timeout, read_timeout)能同时设置连接超时和读取超时,单位为秒,也可以共用一个值,就是豆包写的
这是我自己写的timeout的抛出值
try:
response = requests.get("http://google.com",timeout=0.01)
except requests.exceptions.Timeout:
print("timeout")
也可以分开抛出两个值
点击查看代码
try:
response = requests.get("http://google.com",timeout=(10,0.01))
except requests.exceptions.ConnectTimeout:
print("connect timeout")
except requests.exceptions.ReadTimeout:
print("read timeout")
点击查看代码
def get_content(self, href):
# 【新增功能2】增加延时,防反爬封禁,每章间隔0.3~0.8秒
time.sleep(0.5)
try:
r = requests.get(url=href, headers=self.headers, timeout=10)
r.raise_for_status()
r.encoding = r.apparent_encoding
soup = BeautifulSoup(r.text, "lxml")
# 获取章节标题和内容
title = soup.select_one("#neirong h1").text.strip()
content = soup.select_one("#txt").get_text("\n", strip=True)
return title, content
except Exception as e:
print(f"❌ 章节{href}获取失败:{e},3秒后重试...")
time.sleep(3)
return self.get_content(href) # 【新增功能3】失败自动重试
一眼看过来好像是用了递归,但他怎么递归到头呢?(总不能死循环吧 OVO
然后,最神的来了

豆包的代码真有问题,我服了,我自己来修
点击查看代码
except Exception as e:
max_retry = 3 # 最多重试3次
if retry_count >= max_retry:
print(f"❌ {href} 已重试{max_retry}次,彻底放弃该章节")
# 返回空标识,上层跳过保存
return None, None
print(f"❌ 章节{href}获取失败:{e},3秒后第{retry_count+1}次重试...")
time.sleep(3)
# 重试次数+1,递归调用(跳出条件:达到max_retry就return)
return self.get_content(href, retry_count + 1)
#这么一改run也要加个对空值的判断了
for href in self.hrefs:
title, content = self.get_content(href)
# 重试耗尽,跳过本章
if title is None and content is None:
continue
self.save_single_txt(title, content)
点击查看代码
# 拼接规范文件路径,兼容Windows、Linux系统
file_path = os.path.join(self.name, f"{title}.txt")
# 判断章节文件是否已经存在
if os.path.exists(file_path):
print(f"⏭️ {title}已存在,跳过")
self.finish_chapter += 1
return
# 不存在才执行写入操作
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
self.finish_chapter += 1
缺失点 5:无爬取进度统计,无法直观查看下载情况
点击查看代码
#初始化新增计数变量(__init__ 构造函数内)
self.total_chapter = 0 # 总章节数量
self.finish_chapter = 0 # 已成功下载章节
#目录解析时赋值总章节数(get_hrefs)
self.total_chapter = len(self.hrefs)
print(f"✅ 小说《{self.name}》初始化完成,共{self.total_chapter}个章节")
#保存完成后计算并打印进度
progress = self.finish_chapter / self.total_chapter * 100
print(f"✅ {title}保存成功 | 进度:{self.finish_chapter}/{self.total_chapter} ({progress:.2f}%)")
看完代码我发现豆包的这个5没啥技术含金量,但我作为新手很难想到这一点,因为我没有做项目的经验,
很难从完全不知道这个代码的外人的角度来思考问题,所以我肯定不会想着要写个进度条。(这么一看还挺有启发性的
缺失点 6:仅分章保存,无整本合并全集功能
点击查看代码
def save_all_in_one(self):
"""整本小说合并为单个TXT文件"""
all_content = ""
for href in self.hrefs:
title, content = self.get_content(href)
# 章节分割线区分内容,阅读更清晰
all_content += f"\n\n=========={title}==========\n\n{content}"
with open(f"{self.name}_全集.txt", "w", encoding="utf-8") as f:
f.write(all_content)
print(f"\n🎉 整本小说《{self.name}》全集保存完成!")
我还真不知道f-str可以这样用,我又去写了写代码来看看怎么个事,表示学到了( •̀ ω •́ )y
点击查看代码
# 模拟小说数据
titles = ["第一章", "第二章", "第三章"]
contents = ["1.开篇文字", "2.中间剧情", "3.结尾段落"]
book_name = "测试小说"
all_text = ""
# 循环拼接每一章
for t, c in zip(titles, contents):
all_text += f"\n\n=========={t}==========\n\n{c}"
# 文件名也用f-string
file_name = f"{book_name}_全集.txt"
# 打印看看拼接结果
print(all_text)
print("保存文件名:", file_name)
八、补充修复原生代码隐性 BUG(语法 / 选择器优化)
1.替换 select()[0] 为 select_one(),避免空列表下标报错
原写法:soup.select("#neirong h1")[0].text
优化写法:soup.select_one("#neirong h1").text
2.统一编码声明 encoding="utf-8",杜绝中文乱码
3.规范程序入口 if name == 'main':,保证类仅主动运行时执行
写到这里我终于完成了对源代码的优化理解(还有我自己的优化)下面的是源代码(优化版)
还有我的演示视频,(番茄好像支持txt文本输入,回头看看能不能把番茄没有的书搬到我的书架里。)
点击查看代码
# 导入需要的库
import os
import time
import requests
from bs4 import BeautifulSoup
class NovelSpider:
def __init__(self, url):
self.url = url
self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0"
}
self.hrefs = None
self.name = None
self.total_chapter = 0 # 总章节计数
self.finish_chapter = 0 # 已完成章节计数
def get_hrefs(self):
"""获取小说目录、所有章节链接、小说名称"""
try:
r = requests.get(url=self.url, headers=self.headers, timeout=10)
r.raise_for_status()
r.encoding = r.apparent_encoding
soup = BeautifulSoup(r.text, "lxml")
# 提取章节a标签
all_a = soup.select('ul.info>li>a')
self.hrefs = ["https://www.biquge365.net" + a["href"] for a in all_a]
self.total_chapter = len(self.hrefs)
# 获取小说名并清洗空格
self.name = soup.select_one("div > h1").text.strip()
# 创建存储文件夹
os.makedirs(self.name, exist_ok=True)
print(f"✅ 小说《{self.name}》初始化完成,共{self.total_chapter}个章节")
except Exception as e:
print(f"❌ 获取目录失败:{e}")
def get_content(self, href, retry_count=0):
"""获取单章标题与内容,限制最大重试3次,设置递归跳出条件"""
time.sleep(0.5)
max_retry = 3
try:
r = requests.get(url=href, headers=self.headers, timeout=10)
r.raise_for_status()
r.encoding = r.apparent_encoding
soup = BeautifulSoup(r.text, "lxml")
title = soup.select_one("#neirong h1").text.strip()
content = soup.select_one("#txt").get_text("\n", strip=True)
return title, content
except Exception as e:
# 递归跳出条件:重试次数达到上限直接放弃
if retry_count >= max_retry:
print(f"❌ {href} 已重试{max_retry}次,彻底跳过该章节")
return None, None
print(f"❌ 章节{href}获取失败:{e},3秒后第{retry_count+1}次重试...")
time.sleep(3)
return self.get_content(href, retry_count + 1)
def save_single_txt(self, title, content):
"""分章节保存,支持断点续爬"""
file_path = os.path.join(self.name, f"{title}.txt")
# 判断文件存在则跳过
if os.path.exists(file_path):
print(f"⏭️ {title}已存在,跳过")
self.finish_chapter += 1
return
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
self.finish_chapter += 1
# 计算并打印进度
progress = self.finish_chapter / self.total_chapter * 100
print(f"✅ {title}保存成功 | 进度:{self.finish_chapter}/{self.total_chapter} ({progress:.2f}%)")
def save_all_in_one(self):
"""合并所有章节为一本全集txt,使用f-string拼接内容"""
all_content = ""
for href in self.hrefs:
title, content = self.get_content(href)
if title is None and content is None:
continue
# f-string格式化拼接章节分隔内容
all_content += f"\n\n=========={title}==========\n\n{content}"
# f-string生成全集文件名
full_file_name = f"{self.name}_全集.txt"
with open(full_file_name, "w", encoding="utf-8") as f:
f.write(all_content)
print(f"\n🎉 整本小说《{self.name}》全集保存完成!")
def run(self):
"""爬虫主运行入口"""
self.get_hrefs()
if self.hrefs is None:
print("目录获取失败,爬虫终止")
return
# 循环爬取每一章
for href in self.hrefs:
title, content = self.get_content(href)
if title is None and content is None:
continue
self.save_single_txt(title, content)
# 全部分章爬完后合并全集
self.save_all_in_one()
# 程序入口
if __name__ == '__main__':
# 《万古神帝》目录地址
spider = NovelSpider("https://www.biquge365.net/newbook/83621/")
spider.run()
浙公网安备 33010602011771号