BS4实现简易起点小说网爬虫

发布于 16 天前  99 次阅读


前言

前两天写了一个针对福利吧的爬虫:

https://bakaft.me/?p=164

使用BeautifulSoup和Requests实现一个简单的图片爬虫

总体来说比较简单,毕竟WordPress本身的文章页面结构比较规则,而且爬取的页面只有图片,筛选较为容易,找到图片后也不需要什么处理,下载就完事了。

这次搞个小说爬虫,可以做个练手熟悉一下这些内容

  • Json的获取和运用
  • 多级嵌套循环
  • 全局变量和局部变量
  • if else结构
  • 几种常见编码

我选择的是起点小说网,差不多是国内的老大了吧。我小学读的那本《神鬼剑士》就是起点连载的,怀念呀,可惜不知道为什么下架了。

效果

还彳亍

准备工作

先简单了解一下起点小说网

1.作品结构:书,卷,章

2.小说分类

  • VIP小说:一般都有免费章节,以及收费章节
  • 限免小说:对收费章节临时解锁内容的VIP小说
  • 免费小说:全免~~
  • 此外,强调一点,免费章节不需要登陆就可以读,收费章节必须登录并付费才能完整阅读

3.不同设备的URL适配

3.1 PC端

免费章节的URL如下:

read.qidian.com/chapter/kpwCq4fKJk01/9UndUwXIwfQex0RJOkJclQ2

chapter后,第一部分为作品唯一识别码,第二部分为章节唯一识别码
Json里会整体返回加粗的内容

而收费章节则有些不同:

vipreader.qidian.com/chapter/114559/10242607

chapter后,第一部分为作品id,第二部分为章节id
Json里会分别返回两个内容

3.2 Mobile端

有一句话。。爬东西的时候,APP>Mobile>PC。是因为,从左到右,一般数据格式会越简单。在起点小说网确实是这样。换成手机UA来看一下。

免费的章节URL:

m.qidian.com/book/114559/20203867

chapter后,第一部分为作品id,第二部分为章节id
Json里会分别返回两个内容

收费的章节URL:

m.qidian.com/book/114559/10265537

和免费的一样!

那么就会有这样一个问题,爬取限免文章的时候,不判断文章是否免费的情况下(Json里提供的数据确实不足以判断),用PC的URL规则去Get的时候,我设定

ChapterPrefix = 'https://read.qidian.com/chapter/'
Chapter_Url = ChapterPrefix+[JSON返回数据]

那么我只能爬免费章节,一爬到限时解锁的章节,会因为URL格式不符合拉跨。

但是,如果采用Mobile的URL规则去Get,免费和收费章节都一样,我设定

ChapterPrefix_Mobile = 'https://m.qidian.com/book/'
Chapter_Url_Mobile = ChapterPrefix_Mobile+str(Book_ID)+'/'+str(Chapter_ID)

这样子,就算是爬取限免作品,也可以从头爬到底了。

总结一下,PC的URL规则只适合爬取免费作品,而Mobile的URL规则可以爬免费和限免作品。

本文章只考虑使用Mobile站URL规则解析的方法,PC的解析方法会以另一个函数的形式附加在源码中。

4.Json结构

很多信息都要从Json里直接获得,通过百度,我直接找到了起点的作品目录Json接口: book.qidian.com/ajax/book/category?bookId=[这里是作品ID]

要访问接口,需要至少在Request Headers里添加UA和Rerferer两项

import json #实现Json转字典
import requests
Book_ID = 1016027977
Book_Url = 'https://book.qidian.com/info/'+str(Book_ID)+''
headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36S', 'Referer':Book_Url}
Json_Url = 'https://book.qidian.com/ajax/book/category?bookId='+str(Book_ID)+''
Json_Source = requests.get(Json_Url, headers=headers).text.encode(encoding= 'ISO-8859-1') #因为Json里含有中文的卷名和章节名,这里附加encoding参数
Dict = json.loads(Json_Source) #Json转字典
print(Dict)

这里我把得到的Json转化为字典打印出来.得到的内容里的中文不需要再转义,把字典处理一下扔到Json格式化工具看一下结构

简单做一下已知的注释,其他的感兴趣可自己摸索

开搞

先导入一下模块,定义一个计数变量,再把要爬的作品ID写成列表

from bs4 import BeautifulSoup
import requests
import json
import os

i=1
Book_IDs = [1016027977,1012807503,1015216119,1015985387,1016285482,1016824748,3536174,1015181946]

PC和Mobile的headers

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36S',
'Referer': 'https://www.qidian.com'}
headers_mobile = {
'User-Agent': 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Mobile Safari/537.36',
'Referer': 'https://www.qidian.com'}

对于列表中的每一个作品,拿到Json并转化为字典

for Book_ID in Book_IDs:
Book_Url = 'https://book.qidian.com/info/'+str(Book_ID)+''
Book_Url_Mobile = 'https://m.qidian.com/book/'+str(Book_ID)+''
Book_Name = BeautifulSoup(requests.get(Book_Url_Mobile, headers=headers).text, 'html.parser').find(attrs={"property": "og:title"})['content']
#指定作品ID并设定一些变量

Json_Url = 'https://book.qidian.com/ajax/book/category?bookId='+str(Book_ID)+''
Json_Source = requests.get(Json_Url, headers=headers).text.encode(encoding= 'ISO-8859-1')
Dict = json.loads(Json_Source)
print(Dict) #字典Get
Volumes = Dict['data']['vs'] #定义Volumes为列表vs

嵌套一个循环,对字典信息进行处理

for each1 in Chapter_Info:   #定义章节相关变量
Chapter_ID = each1['id']
Chapter_Number = '['+str(i)+']'
#自己在每个文件名前加的爬取顺序数字,防止部分奇怪标题影响排序。
ChapterPrefix = 'https://read.qidian.com/chapter/'
ChapterPrefix_Mobile = 'https://m.qidian.com/book/'
Chapter_Url = ChapterPrefix+each1['cU']
Chapter_Url_Mobile = ChapterPrefix_Mobile+str(Book_ID)+'/'+str(Chapter_ID)
Chapter_Name = Chapter_Number+each1['cN']
Chapter_Name_Replaced = Chapter_Name.replace('/', '/').replace('*', '[星号]').replace('?', '[问号]').replace(':', '[冒号]').replace('"', '双引号').replace('<', '[左尖括号]').replace('>', '[右尖括号]').replace('|', '[竖杠]')
#避免某些作者的奇怪章节名。。规则遵循Windows的命名方法。
Chapter_TextCount = each1['cnt'] #章节字数,暂时没用到

接下来用一个函数来写下载部分,套在第二个循环里

def GetBook_Mobile(): 
global i #在函数体里声明一下变量i是全局变量
Page_request = requests.get(Chapter_Url_Mobile, headers=headers_mobile)
Page_Source = Page_request.text
Page_Soup = BeautifulSoup(Page_Source, 'html.parser')
Paragraphs = Page_Soup.find('section', {'class': 'read-section jsChapterWrapper'}).find('p')#寻找第一个p标签
#这里比较难理解,起点的文段部分用Bs4解析完是一个<p>标签套娃的结构,就像俄罗斯套娃,这里只取最外层的标签即可
FormattedChapter = Paragraphs.text.replace('  ', '\n\n')
#文章格式化处理 好康一些
print('---作品保存:'+Book_Name)
if os.path.isdir(Book_Name):
pass ##防止重复写目录
else:
os.mkdir(Book_Name)
print('作品目录创建成功') #保存新作品时的提示
File_RelativePath = Book_Name+'/'+Chapter_Name_Replaced+'.txt'
#定义每一个txt文件的相对路径
print('章节保存:'+Chapter_Name_Replaced)
i=i+1
if os.path.isfile(File_RelativePath):
print('章节已存在,跳过')
pass #防止重复写文件
else:
f = open(Book_Name+'/'+Chapter_Name_Replaced+'.txt', 'w', encoding= 'utf-8')
#创建txt文件
for eachline in FormattedChapter:
f.write(eachline) #逐行写入
f.close()
print('章节保存成功')

调用一下就开搞了

GetBook_Mobile()  
附文件源码:
https://bakaft.me/uploadings/Python/QiDianNovelScrape/QiDianXiaoShuoScrape.py
如果存在乱码问题,建议下载后使用Pycharm等IDE以UTF-8编码打开