20252436 赵明山 实验四 Python综合实践

爬虫实践

这是一个中文小说网站(如下图所示

image

我想要爬取其中一部小说,比如《万古神帝》。
而这需要用到有关爬虫的库,最主要的是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()

实验过程

  1. 首先,我要用request的话就需要获取到网页的url,最好连带其他的用户协议一起获取。
    这里我找到了一个非常好的工具
就是这个curl命令转代码——https://tool.lu/curl/

image

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

image
image

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

请求依旧通过,返回动态码200 访问成功了

image

2.之后就是把获取到的response.text转成lxml形式,方便后面获取信息(用bs4
soup = BeautifulSoup(response.text, "lxml")
后续的代码块中最常规的便是class类操作,其次便是文件的读写操作
image
其中我在鱼C社区中了解到了一行流代码写法,我把它写在代码注释当中,这里拎出来说是因为我没有看懂这个一行流是怎么写的

all_a = BeautifulSoup(r.text, "lxml").select("ul.info>li>a")

看起来好像是直接获取到了数据链接中的文本内容,然后可以直接写进文件操作里?

3.最后也是重点,就是beautifulsoup的使用,尤其是soup.slect的操作
这也是本人最抓麻的地方,就拿最简单的获取标题来说
image
代码写得简单 #####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,

image

当它说有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")
######缺失点2、3:章节请求失败无重试机制,断网直接丢章节
点击查看代码
    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

然后,最神的来了

image

豆包的代码真有问题,我服了,我自己来修

点击查看代码
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)
#####缺失点 4:无断点续爬,重复运行会覆盖已下载章节
点击查看代码
# 拼接规范文件路径,兼容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
###我说实话,这个规范文件路径,我还真不知道Linux和Windows的文件路径规范是啥。也判断不了这个代码写的对不对 ###尝试学习一下不同系统的文件路径规范标准 ![image](https://img2024.cnblogs.com/blog/3777252/202606/3777252-20260616170152859-363726154.png) ###但是我查到的网站都在讲正反斜杠的问题,所以这和豆包写的代码有关系吗?
缺失点 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()

posted @ 2026-06-16 17:40  mountank  阅读(26)  评论(2)    收藏  举报