使用Python爬虫和正则表达式解析起点小说网页数据

Re解析爬虫响应数据

需求:爬取起点小说网站中某一本小说的免费章节,包括章节的标题和内容。
主要分为两步:
	1.获取每一章节的标题和对应内容详情页的请求URL
	2.获取每一章节内容详情页的章节内容

!!注意:我们获取到的网页响应数据,可能会与网页源代码中呈现的格式不同。因为有些网页文件是用JavaScript加载的,浏览器会自动将其解析成html文档格式,而我们获取到的内容是JavaScript格式的文档。所以获取到响应数据之后先要查看内容是否与网页源码中的一致,不一致的话,在编写正则表达式时则以获取到的响应数据res.text为准,否则会找不到对应数据。

一、爬取小说的标题和章节内容页的链接

在起点小说网(https://www.qidian.com/all/)打开一篇小说,发现只有免费章节的内容是完整的,收费章节非VIP只会显示部分内容。所以我们只爬取免费的章节。

1.1 指定URL

打开网页源码,查看网页的URL,请求方法为GET,文本类型是text/html。

我们想要获取的是119章免费章节的标题和章节内容页的URL。

定位发现所有的章节标题和章节内容页链接都在<div class=“catalog-volume” 标签中,所以我们可以尝试定位到该标签中,看能不能取到所有的章节标题和章节内容页链接。

1.2 发起网页请求,获取响应

接下来开始请求网页:

# 导包
import re
import requests

# 指定url
url = 'https://www.qidian.com/book/1016530091/'
headers = {
    'Host':'www.qidian.com',
    'Referer':'https://www.qidian.com/all/',
    'Sec-Ch-Ua':'"Microsoft Edge";v="117", "Not;A=Brand";v="8", "Chromium";v="117"',
    'Sec-Ch-Ua-Mobile':'?0',
    'Sec-Ch-Ua-Platform':'"Windows"',
    'Sec-Fetch-Dest':'document',
    'Sec-Fetch-Mode':'navigate',
    'Sec-Fetch-Site':'same-origin',
    'Sec-Fetch-User':'?1',
    'Upgrade-Insecure-Requests':'1',
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.31',}

response = requests.get(url,headers=headers)
print(response.encoding)     # 查看编码格式
print(response.status_code)     # 查看响应状态码
print(response.text)         # 打印响应内容
page_text = response.text

通过对比可以发现,网页响应的内容response.text与网页源码中的内容是一样的,所有的文章标题都在<ul class=“volume-chapters” 中,我们可以先定位到该标签,再进一步定位。

1.3 进行数据解析

下面开始编写正则表达式并查找对应的内容:

(1)获取小说名称

在网页源码中定位到书名,发现其在<h1 class="" id="bookName">赤心巡天</h1>中。在网页源码的搜索框查找<h1 class="" id="bookName">标签,发现只有一个结果,所以我们可以直接定位到该标签中,获取文本。

# 获取小说名称
ex = '<h1 class="" id="bookName">(.*?)</h1>'  # !!注意,要将元素复制出来,查看标签、关键字等之间有无空格或未显示的东西。如:class=""并未在html文档中显示出来
bookname = re.findall(ex,page_text)
print(bookname)

!!注意,要将元素复制出来,查看标签、关键字等之间有无空格或未显示的东西。如:class=""并未在html文档中显示出来。

输出结果是:(获取到书名)

(2)获取章节标题

我们从<ul class="volume-chapters>" 开始查找匹配它的每一个子标签

# 获取每一章节的标题
ex1 = '<ul class="volume-chapters"><li class="chapter-item".*?<a class="chapter-name".*?章节字数:\d{4}">(.*?)</a>.*?</ul>'  # 正则表达式
titles = re.findall(ex1,page_text)   # 查找所有符合条件的内容
print(titles)

结果是:发现并没有获取到内容。

接下来,我们尝试扩大查找范围,即从<ul class="volume-chapters>的下一级标签开始查找匹配。

# 获取每一章节的标题
ex1 = '<li class="chapter-item".*?<a class="chapter-name".*?章节字数:\d{4}">(.*?)</a>.*?</li>'   # 正则表达式
titles = re.findall(ex1,page_text)
print(titles)

结果是:
取到了所有的章节标题并存放在一个列表中。

但是我们只需要免费章节的内容,即从“赤心巡天世界地图设定文稿”到“第一百一十八章 白骨道子”,需要对列表进行切片。

detail_titles = titles[:120]            # 只取免费章节,即前118章的标题
print(detail_titles)

输出结果是:(获取到所有免费章节的标题)

(3)获取章节内容页的链接

章节内容页的链接和章节标题在同一个标签中,只是链接是标签属性的值,标题是标签的文本内容,因此我们可以在正则表达式ex1让进行修改。

编写正则表达式,并查找匹配响应内容。

ex2 = '<li class="chapter-item".*?<a class="chapter-name"\shref="(.*?)"\starget=.*?</li>'  # 取到所有章节的url,同样我们只要免费章节的链接
links = re.findall(ex2,page_text)         # 获取每一章详情页的部分链接 
print(links)

结果是:(获取到了所有章节的链接并存放到列表中)
点击查看确实是对应章节页面的链接。

我们需要从中截取需要的链接,并将其拼接成完整的章节内容页的请求URL。

detail_links = links[:120]
print(detail_links)
print(len(detail_links))

输出结果是:

二、爬取小说章节内容页的内容

我们需要先获取某一章节的内容,然后通过循环对每一章节内容页发起请求,获取全部章节内容。

2.1爬取某一章节的内容

以爬取第一章的内容为例
打开网页源码,在NetWork下可以发现其请求方法为get方法,文本类型为text/html,编码格式为utf-8。所以我们获得的响应的文本数据是html文档,编码格式是utf-8,我们需要对其进行数据解析。这里使用re进行数据解析。

我们之前已经获取了全部章节内容页的URL,然后需要进行UA伪装,准备好GET请求所需的参数之后,尝试对网页发起请求。响应状态码为200就说明请求成功,获取到响应。然后打印响应的内容,res.text。(建议每次发起请求后都输出状态码查看是否请求成功

# 指定url
url = detail_urls[1]    # 以爬取第一章为例
print(url)

# UA伪装
header = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.31',}

# 发起请求,获取响应
res = requests.get(url,headers=header)
page_text = res.text
print(res.status_code)        # 建议在每一次请求之后输出响应的状态码,这样可以知道我们是否请求成功
print(res.encoding)
print(res.text)

获取到响应数据res.text之后可以发现,其与网页源码的格式不同,说明该网页文件是由JavaScript加载的。所以要根据res.text的格式来编写正则表达式。

打开网页源码,选择所有的文本内容,可以发现,该章节全部内容都在<main标签下,在源代码中按下ctrl+F查找<main,只有一个结果,所以我们只要定位到main标签就可以找出全部的章节内容。
网页源码

由于网页是由JavaScript文件加载的,所以我们在res.text中查找是否有<main标签,通过对比可以发现,网页源码中p标签下还有子标签span,而网页响应数据的p标签下并没有子标签。我们需要根据网页响应内容编写正则表达式。
网页响应数据res.text
其中文本的内容如下:

#<p>  太阳悬在高天,将它的光和热,不偏不倚洒落人间。不分老幼,不辨贵贱。大爱如无情。</p>
# <p>  入此地牢者,一息呼气凝霜,二息血流冻结,三息肉身僵死。</p>

编写正则表达式获取<main标签下所有子标签p中的文本内容。


ex = '<main id=.*?<p>(.*?)</p>.*?</main>'  # 正则表达式
content = re.findall(ex,page_text)    查找所有符合要求的内容
print(content)

输出后得到的结果是:
只取到了第一个p标签中的文本内容,而且文本前出现了\u3000,是空格的ASCII码,我们需要将文本前面的内容去掉,并且减少正则表达式中的标签数,扩大查找范围,以获取更多文本。

去除文本前面的空格:

# \s表示匹配空格,+表示数量大于等于1
ex = '<main id=.*?<p>\s+(.*?)</p>.*?</main>'   # 只能取到一个p标签下的文本

输出的结果是:(已经去除掉文本前面的空格)

接下来尝试减少正则表达式中的标签数,看看能不能获取到更多内容。

ex = '<p>\s+(.*?)</p>'         # 扩大范围,取到所有文本内容

content = re.findall(ex,page_text)
print(content)

输出结果:(获取到该章节的全部文本内容并存储到列表中)

2.2 通过循环获取每个章节中的内容

我们在上节中已经获取到了某个章节的全部文本内容,想要获取全部章节的内容,可以通过for循环依次对每一章节发起请求,获取响应,然后解析出文本内容。

需要注意的是,在上一节中我们获取的某一章节的内容被存储到列表中,如果我们想将内容保存到本地,就需要将其转换成字符类型,再写入txt文档中。

content_string = ''      # 创建一个空字符串,用于存放章节内容
for j in range(len(content)):     # 循环遍历列表
	# 字符串拼接
    content_string = content_string + content[j]

通过循环爬取全部章节内容的代码如下:

# 创建文件,并将小说每一章节标题和内容写入
fp = open(f'D:\\Python\\{bookname}.txt','w',encoding='utf-8')

# 获取每一章节详情页的小说内容
for i in range(len(detail_titles)):
    detail_url = detail_urls[i]     # 依次取每个章节内容的URL
    header = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.31',}
    # 发起请求
    res = requests.get(detail_url,headers=header)
    detail_text=res.text
    detail_ex = '<p>\s+(.*?)</p>'        # 编写正则表达式
    content = re.findall(detail_ex,detail_text)      # 内容存储在列表中,需要把它逐个取出并写入
    content_string = ''             # 创建一个空字符串,用于存放章节内容
    for j in range(len(content)):
        content_string = content_string + content[j]
    # 将每一章节的标题和内容存储到txt文件中
    fp.write(detail_titles[i]+'\n'+content_string+'\n')
    print(f"{detail_titles[i]}爬取结束!")
    
# 关闭文件
fp.close()

爬取的文件保存到txt文件中:

整个程序的代码如下:

'''
爬取起点小说网的某篇小说,用re解析网页数据,只要免费章节即1-118章节
'''
# 导包
import re
import requests

# 指定url
url = 'https://www.qidian.com/book/1016530091/'
# UA伪装
headers = {
    'Host':'www.qidian.com',
    'Referer':'https://www.qidian.com/all/',
    'Sec-Ch-Ua':'"Microsoft Edge";v="117", "Not;A=Brand";v="8", "Chromium";v="117"',
    'Sec-Ch-Ua-Mobile':'?0',
    'Sec-Ch-Ua-Platform':'"Windows"',
    'Sec-Fetch-Dest':'document',
    'Sec-Fetch-Mode':'navigate',
    'Sec-Fetch-Site':'same-origin',
    'Sec-Fetch-User':'?1',
    'Upgrade-Insecure-Requests':'1',
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.31',}

# 发起请求,获取响应
response = requests.get(url,headers=headers)
print(response.encoding)       # 查看编码格式
print(response.status_code)    # 查看状态码
# print(response.text)
page_text = response.text      # 获取响应内容


# 获取小说名称
ex = '<h1 class="" id="bookName">(.*?)</h1>'      # !!注意,要将元素复制出来,查看标签、关键字等之间有无空格或未显示的东西。如:class=""并未在html文档中显示出来
bookname = re.findall(ex,page_text)
print(bookname)

# 获取每一章节的标题
# ex1 = '<ul class="volume-chapters"><li class="chapter-item".*?<a class="chapter-name".*?章节字数:\d{4}">(.*?)</a>.*?</ul>'
# ex1 = '<ul class="volume-chapters><li class="chapter-item".*?<a class="chapter-name".*?alt="(.*?)"\stitle=.*?</ul>'    
# ex1 = '<li class="chapter-item".*?<a class="chapter-name".*?alt="(.*?)"\stitle=.*?</li>'
# 正则表达式
ex1 = '<li class="chapter-item".*?<a class="chapter-name".*?章节字数:\d{4}">(.*?)</a>.*?</li>'
titles = re.findall(ex1,page_text)
# print(titles)
detail_titles = titles[:120]            # 只取免费章节,即前118章的标题
print(detail_titles)

#获取每一章详情页的链接
# ex2 = '<ul class="volume-chapters">\s+<li class="chapter-item".*?<a class="chapter-name"\shref="(.*?)"\starget=.*?</li>.*?</ul>'     # 只能取到每一卷第一章的url
# ex2 = '<a class="chapter-name"\shref="(.*?)"\starget=.*?</a>'       # !!!!注意:空格一定要用\s来匹配,否则取不到值
ex2 = '<li class="chapter-item".*?<a class="chapter-name"\shref="(.*?)"\starget=.*?</li>'      # 取到所有章节的url,同样我们只要免费章节的链接
links = re.findall(ex2,page_text)         # 获取每一章详情页的部分链接   
detail_links = links[:120]
print(detail_links)
print(len(detail_links))

# 拼接出每一章节详情页面的url
detail_urls = []
for link in detail_links:
    detail_urls.append('https:'+link)
print(detail_urls)


# 创建文件,并将小说每一章节标题和内容写入
fp = open(f'D:\\Python\\{bookname}.txt','w',encoding='utf-8')

# 获取每一章节详情页的小说内容
for i in range(len(detail_titles)):
    detail_url = detail_urls[i]
    header = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.31',}
    # 发起请求
    res = requests.get(detail_url,headers=header)
    detail_text=res.text
    detail_ex = '<p>\s+(.*?)</p>'        # 编写正则表达式
    content = re.findall(detail_ex,detail_text)      # 内容存储在列表中,需要把它逐个取出并写入
    content_string = ''             # 创建一个空字符串,用于存放章节内容
    for j in range(len(content)):
        content_string = content_string + content[j]
    fp.write(detail_titles[i]+'\n'+content_string+'\n')
    print(f"{detail_titles[i]}爬取结束!")
    
# 关闭文件
fp.close()

总结:

1、需要注意响应内容是否与网页源码格式相同
2、编写正则表达式时需要将网页源码或者响应内容中的对应元素复制出来,观察其格式,按照格式去编写正则表达式
3、如果我们查找不到对应的内容,或者只取到对应内容的一部分,则我们需要扩大查找范围,正则表达式的编写从开始标签的下级标签开始查找。
4、建议每次获取到数据都输出查看是否是我们想要的格式和内容。

物联沃分享整理
物联沃-IOTWORD物联网 » 使用Python爬虫和正则表达式解析起点小说网页数据

发表评论