Python爬虫入门:超详细爬小说教程

74 阅读12分钟

导语

声明:所有爬取的皆是公开的网页数据,尊重知识产权,建议阅读原版 本文章仅为学习交流,请勿用作非法用途。

很多同学在课上或课下学了Python后却不知道能干什么,那就跟我一起来玩玩Python爬虫吧!
这篇文章我会以爬取笔趣阁小说为例,详细介绍爬虫基础步骤流程。

环境

  • PyCharm:集成开发环境,专门用作Python开发的工具
  • Python3:编程语言,我使用的版本为3.11
  • requests:用作模拟网站访问的插件
  • BeautifulSoup:从HTML或XML文件中提取数据的解析器

requestsBeautifulSoup都是在使用Python爬虫时常用的插件,从PyCharm中打开终端可以直接下载。

安装requests

pip3 install requests

安装BeautifulSoup

pip install beautifulsoup4

详细步骤

1、爬取单章全部内容

我们这次使用requests.get()来抓取数据,以下是其函数定义

def get(
    url: str | bytes,
    params: _Params | None = None,
    *,
    data: _Data | None = ...,
    headers: _HeadersMapping | None = ...,
    cookies: RequestsCookieJar | _TextMapping | None = ...,
    files: _Files | None = ...,
    auth: _Auth | None = ...,
    timeout: _Timeout | None = ...,
    allow_redirects: bool = ...,
    proxies: _TextMapping | None = ...,
    hooks: _HooksInput | None = ...,
    stream: bool | None = ...,
    verify: _Verify | None = ...,
    cert: _Cert | None = ...,
    json: Incomplete | None = ...,
) -> Response: ...

以《斩神》的第一章为例

1.png 进入开发者模式DevTools(右键点击页面选择“检查”或使用快捷键“F12”)监测网络请求,预览每个请求返回的内容,很快便能找到文字都被存在1.html这个文档中,这个文档便是我们真正要抓取的数据。

2.png 再查看标头,可知采用GET方法请求网址即可。

3.png

requests.get(url)为核心,编写代码如下:

import requests

if __name__ == '__main__':
    url = 'https://www.bq09.cc/html/39516/1.html' 
    req = requests.get(url=url)
    print(req.text)
    

这里将图中的请求网址赋值给url,再使用requests.get(url)方法将这个网页的所有内容抓取并存为req。最后用.text方法将其转换为文本打印出来。

由于我们使用的是下载的requests库,不要忘了import。但PyCharm会以波浪线提醒你的,所以在日常编程过程中,不要忘了检查每一个波浪线,这能帮你节约很多找错的时间。

运行代码,得到以下结果:

4.png

可以看到,我们很轻松地获取了HTML信息。但是,很显然,很多信息是我们不想看到的,我们只想获得如右侧所示的正文内容,我们不关心divbr这些html标签。如何把正文内容从这些众多的html标签中提取出来呢?这就是插件beautifulsoup的主要功能。

不过,在实际连续抓取多个网页信息时,只提供请求网址url时常会请求失败,所以加上请求标头headers是更好的选择。我们先来看一下这个网页有哪些请求标头:

5.png

其中最重要的是以下几个字段:
accept-language: 这个字段指定了客户端能够接受的语言类型,其中 "zh-CN" 是首选语言(简体中文),而 "zh;q=0.9"则是次选语言(也是简体中文,但优先级略低)。这个信息通常用于服务器返回适当的语言版本内容。
cache-control: 这个字段告诉缓存机制如何处理请求和响应。在这里设置为 "no-cache" 表示客户端或代理服务器应该在发送此请求之前检查服务器上的更新版本。
content-type: 指定请求体的媒体类型,在这里设置为 application/json;charset=UTF-8,表示请求的数据格式是JSON,并且字符编码使用 UTF-8。这对于向API发送JSON数据非常重要,确保接收方能够正确解析它。
User-Agent: 这个字段提供关于客户端信息,表示这个请求是由一个使用 Chrome 浏览器的 Windows 系统发送的。

将这些标头整合发送,代码会变成这样:

import requests

headers = {
        "accept-language": "zh-CN,zh;q=0.9",
        "cache-control": "no-cache",
        "content-type": "application/json;charset=UTF-8",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
    }

if __name__ == '__main__':
    url = 'https://www.bq09.cc/html/39516/1.html' 
    req = requests.get(url=url, headers=headers)
    print(req.text)
    

添加了标头后,请求就会变得稳定而快速,结果倒是不会改变,我就不展示了。

2、HTML

在使用beautifulsoup之前,我们要知道,我们打印出来的内容其实就是1.html的响应,切回调试页面,点击响应可以很明显的看到这些内容:

5.png

其实由这个文档的名字1.html就不难看出其使用的是HTML语言,接下来我会简单介绍一下HTML语言的结构,方便大家理解。

HTML是超文本标记语言(HyperText Markup Language)的缩写。它是一种用于描述网页内容的语言,告诉浏览器如何显示文字、图片、链接等元素。简单来说,HTML就是一种“标签”语言,通过这些标签来组织和展示网页的内容。

下面让我们来看一个标准的HTML网页示范:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>一个网页</title>
    </head>
    <body>
        <!-- 这是一个大的容器 -->
        <div>
            <h1>欢迎来到我的网站!</h1>
            <p>虽然我并没有网站。</p>
            <a href="https://yunxi_III.com">访问链接</a>
        </div>
    </body>
</html>

<!DOCTYPE html>声明文档类型为HTML5
<html>标签之间为网页的根元素,该标签的结束标志为</html>
<head>标签包含了文档的元数据(meta),如<meta charset="utf-8">定义网页编码格式为utf-8。
<title>标签定义文档的标题
<body>标签定义文档的主体,即网页可见的页面内容,该标签的结束标志为</body>
<div>标签定义了一个容器,容器中可以存放大量标题、段落等,该标签的结束标志为</div>
<h1>标签作为一个标题使用,该标签的结束标志为</h1>
<p>标签作为一个段落显示,该标签的结束标志为</p>
<a>标签设置超文本链接,使用href属性来描述链接的地址,该标签的结束标志为</a>

观察文档1.html可以发现,我们所需要的正文存储在<div id="chaptercontent" class="Readarea ReadAjax_content">这个容器中。观察这个div标签:

<div id="chaptercontent" class="Readarea ReadAjax_content">

不难发现,和我们刚介绍的<div>不同,它还包含idclass两个属性,这两个属性便是用来定义容器,使它与其他容器区分开来。也就是说,通过这两个属性,我们就能定位到这个容器,也就能找到我们所需要的小说正文。 而把容器中的文本从整个html信息中提取出来,就需要用到我们已经安装好的另一个插件:BeautifulSoup

3、BeautifulSoup

来,让我们直接上代码:

import requests
from bs4 import BeautifulSoup

headers = {
    "accept-language": "zh-CN,zh;q=0.9",
    "cache-control": "no-cache", 
    "content-type": "application/json;charset=UTF-8",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" 
    }

if __name__ == '__main__':
    url = 'https://www.bq09.cc/html/39516/1.html'
    req = requests.get(url=url, headers=headers)
    html_1 = req.text
    bf = BeautifulSoup(html_1)
    texts = bf.find_all('div', class_='Readarea ReadAjax_content')
    print(texts)
    

在这一段代码中,我们首先将爬取的html文件req转换为text存储为html_1。随后,在解析html之前,我们需要创建一个 Beautiful Soup 对象。运用BeautifulSoup()html_1创建为待处理对象bf。再使用find_all方法,获得html信息中所有class="Readarea ReadAjax_content"div标签。
值得一提的是,定位这个容器,使用idclass哪个标签都可以,如果将上面的find_all方法具体内容替换为texts = bf.find_all('div', id="chaptercontent")将得到相同的结果。

运行代码:

6.png 这里抛了一个错,是说我们没有明确指定解析器,因此使用了该系统可用的最佳HTML解析器lxml。这通常不是问题,但如果在另一个系统或不同的虚拟环境中运行这段代码,它可能会使用不同的解析器,行为也会不同。 所以我们要在创建 Beautiful Soup 对象时指定解释器为lxml就行了。

    bf = BeautifulSoup(html_1, 'lxml')

再重新运行代码:

7.png

可以看到就不会抛这个错了,不过很明显现在打印出来的文本也还不是我们想要的,仔细看:

8.png

我们可以很明显的发现,python无法识别HTML的换行标志<br>,所以我们要使用replace方法将其替换为pythond的换行符/n。可是,直接进行替换操作是不可行的,因为此时的texts并不是一个字符串。那如何将其转换为字符串呢,我们首先进入调试模式来看看这个texts究竟是个什么格式:

9.png

不难看出,texts是包含了两个字典source0的字典,再打开这两个字典去找,不难找到一个只包含正文的字符串text

10.png

我们直接将这个字符串打印出来:

11.png

效果非常好,不过这可不是我们日常看的小说的格式,将文本内的空格替换为两个换行符和缩进空格\n\n 就行了:

print(texts[0].text.replace('  ', '\n\n  '))

值得一提的是,这里被替换的两个空格比正常打出来的空格要长,直接从前面打印出来的文本里面复制是最好的方法。让我们看看运行结果:

12.png

很好,这就得到了符合正常阅读习惯的格式了。
第一章算是爬完了,那么如何获取每一章呢?

4、获取目录

在爬取第一章内容的时候我们直接将页面url提供给了requests以获得页面内容,也就是说,每一章都会有一个唯一的url,只要获取到了这个url,我们就能获取其内容。

现在让我们打开这本书的目录页面,监听其网络请求。我们可以在39516/的响应中找到每一章:

13.png

可以看到,通向每一章的链接地址都以href属性描述并存贮在标签<a>中。运用和前面相同的方法,可以轻松获取。来,我们直接上代码:

def get_url():
    url = 'https://www.bi05.cc/html/39516/'
    req = requests.get(url=url, headers=headers)
    html_1 = req.text
    bf = BeautifulSoup(html_1, "lxml")
    texts = bf.find_all('div', class_="listmain")
    a_bf = BeautifulSoup(str(texts[0]), "lxml")
    a = a_bf.find_all('a')
    urls = []
    for each in a:
        url_m = f"https://www.bi05.cc{each.get('href')}"
        urls.append(url_m)

if __name__ = '__main__':
    print(get_url())

由于需要先定位到div容器,再定位其中的所有<a>标签,所以我创建了俩次 Beautiful Soup 对象。再用for in遍历每一个<a>标签并用.get()方法找到属性为href的值。随后将链接拼接成请求头,我们就获得了一个包含每一章url的列表urls,运行代码:

14.png

到此为止,我们已经成功获取了每一章的url并且也会借此获取其正文,接下来就只剩将其存储为txt文件方便阅读即可。

不过,在那之前有不少眼尖的朋友肯定已经发现:每一章的url之间是有规律的!所以只要拼接一个递增数在最后也可以获得,就像这样:

def make_url():
    urls = []
    num = 1
    while num < 2034:
        url_m = f"https://www.bi05.cc/html/39516/{num},html"
        urls.append(url_m)
        num += 1
    return urls

if __name__ == '__main__':
    print(make_url())

诚然,这样确实能获得几乎相同的结果,还能跳过https://www.bi05.ccjavascript:dd_show()这条后面会抛错的无用url。但是,出于学习和锻炼能力的目的,我还是建议使用get_url来达到目的。

5、保存

将小说保存到本地需要用到open()函数,以下是其定义:

def open(file, mode='r', buffering=None, encoding=None, errors=None, newline=None, closefd=True):

在这一行函数定义下面其实添加了大量的注释文本来解释函数用法,这里我就不复制了,感兴趣的朋友可以自己去看看。
file是我们要写入文件的名字,可以指定地址,如果没有文件会创建一份。
mode是写入的方式,默认为'r'意为只读,我们这里使用'a'意为续写,将每一章的内容续写到上一章末尾。
encoding是用于对文件进行解码或编码的编码名称。
使用示例:

with open("斩神.txt", mode="a", encoding='utf-8') as f:
    f.write("斩神\n\n")
    f.write(title)
    f.writelines(text)

将我们需要写入的文件用open()打开为一个文档f,再使用.write()直接写入或writelines()逐行写入。
以此为核心就能将整篇小说保存下来。

6、整合代码

现在,所有需要的元素都有了,整合代码如下:

import requests
from bs4 import BeautifulSoup

headers = {
        "accept-language": "zh-CN,zh;q=0.9",
        "cache-control": "no-cache",
        "content-type": "application/json;charset=UTF-8",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
    }

def get_url():
    url = 'https://www.bi05.cc/html/39516/'
    req = requests.get(url=url, headers=headers)
    html = req.text
    bf = BeautifulSoup(html, "lxml")
    texts = bf.find_all('div', class_="listmain")
    a_bf = BeautifulSoup(str(texts[0]), "lxml")
    a = a_bf.find_all('a')
    for each in a:
        url_m = f"https://www.bi05.cc{each.get('href')}"
        title = each.string
        save_f(url_m, title)
    print("下载完成!")

def save_f(url, title):
    try:
        req = requests.get(url=url, headers=headers)
        html_1 = req.text
        bf = BeautifulSoup(html_1, "lxml")
        texts = bf.find_all('div', class_='Readarea ReadAjax_content')
        t = texts[0].text.replace('  ', '\n\n  ')
        with open("我在精神病院学斩神.txt", mode="a", encoding='utf-8') as f:
            f.write("我在精神病院学斩神\n\n")
            f.write(title)
            f.writelines(t)
        print(f"{title} 已下载")
    except:
        print(f"{title} 下载失败")

if __name__ == '__main__':
    get_url()
    

这里我使用了try:方法来避开https://www.bi05.ccjavascript:dd_show(),运行效果如下:

16.gif

很简单的程序,单进程跑,没有开进程池。所以下载速度略慢,冲杯咖啡休休息休息吧~
运行结果如下:

17.png

如果对我的教程还算满意,还请点赞收藏转发关注,谢谢大家~