整本小说的爬取保存
- 目标
- 大致思路
- 页面的爬取解析—XPath
- 请求网页内容
- 解析网页内容
- 正文爬取与解析
- 单个页面数据获取
- 爬取所有页面
- 数据清洗
经过学习基础,我们学以致用一下子,爬取小说,注意这个小说本身是免费的哦,以后再进阶。
本次为实战记录,笔者很多碎碎念可忽略
目标
基于requests库和lxml中的xpath编写的爬虫,目标小说网站域名http://www.365kk.cc/
这是网上找的,小说网址很多,而且没有很多反扒机制,新手友好!
大致思路
其实也可以不写,梳理一下吧,虽然是空话
主要分三步(如同大象进冰箱hh)
- 获取网页内容,通过requests库实现;
- 解析网页内容,得到其中我们想要的部分,通过xpath实现;
- 将解析出的内容储存到文本文档中;
我们把要做的具体化,首先选取一本自己喜欢的小说,笔者以此为例,是的就是这么重口。
《我是一具尸体》
页面的爬取解析—XPath
要干嘛明确一下:
- 获取目标书籍的基本信息,包括书籍的书名、作者、简介——这些信息应该都在同一个页面中获取,即上面展示的页面;
- 获取目标书籍每一章节的标题和内容——不同章节在不同的页面,不同页面之间可以通过下一页按序跳转;
- 正文部分的存储格式应便于阅读,不能把所有文字都堆积在一起,也不能包括除了正文之外的其他无关内容;
因此,我们首先尝试请求书籍的主页,获取基本信息;紧接着再从书籍的第一章开始,不断地请求“下一页”,直到爬取整本书,并将它们以合适的格式储存在文本文档中。
笔者是用Google Chrome,在首页点击右键 —> 检查:
可以看出,浏览器下方弹出了一个窗口,这里显示的就是该页面的源代码,我们选中的内容位于一个<h1>
标签中。点击右键 -> 复制 -> 复制 XPath,即可得到书名的XPath路径,也就是书名在网页中的位置。
书名:/html/body/div[4]/div[1]/div/div/div[2]/div[1]/h1
从书籍的首页中,同理我们可以获取的信息主要包括:
- 书名 作者 最后更新时间
作者:/html/body/div[4]/div[1]/div/div/div[2]/div[1]/div/p[1]
最后更新时间:/html/body/div[4]/div[1]/div/div/div[2]/div[1]/div/p[5]
简介:/html/body/div[4]/div[1]/div/div/div[2]/div[2]
请求网页内容
使用基础的python爬虫网页请求方法: requests 库直接请求。
在请求网页时,我们需要将我们的爬虫伪装成浏览器,具体通过添加请求头 headers 实现。
请求头以字典的形式创建,可以包括很多内容,这里只设置四个字段:User-Agent, Cookie, Host 和 Conection。
在刚才打开的页面中,点击 网络(英文版是Network),刷新页面,找到其中的第一个文件 1053/ ,打开 标头 -> 请求标头 ,即可得到想要的字段数据。
import requests
# 请求头,添加你的浏览器信息后才可以正常运行
headers= {
'User-Agent': '...',
'Cookie': '...',
'Host': 'www.365kk.cc',
'Connection': 'keep-alive'
}
# 小说主页
main_url = "http://www.365kk.cc/1/1053/"
# 使用get方法请求网页
main_resp = requests.get(main_url, headers=headers)
# 将网页内容按utf-8规范解码为文本形式
main_text = main_resp.content.decode('utf-8')
print(main_text)
可以看出,我们成功请求到了网站内容,接下来只需对其进行解析,即可得到我们想要的部分。
解析网页内容
我们使用 lxml 库来解析网页内容,具体方法为将文本形式的网页内容创建为可解析的元素,再按照XPath路径访问其中的内容,代码如下:
import requests
from lxml import etree
# 请求头
headers= {
'User-Agent': '...',
'Cookie': '...',
'Host': 'www.365kk.cc',
'Connection': 'keep-alive'
}
# 小说主页
main_url = "http://www.365kk.cc/1/1053/"
# 使用get方法请求网页
main_resp = requests.get(main_url, headers=headers)
# 将网页内容按utf-8规范解码为文本形式
main_text = main_resp.content.decode('utf-8')
# 将文本内容创建为可解析元素
main_html = etree.HTML(main_text)
# 依次获取书籍的标题、作者、最近更新时间
# main_html.xpath返回的是列表,因此需要加一个[0]来表示列表中的首个元素
# /text() 表示获取文本
bookTitle = main_html.xpath('/html/body/div[4]/div[1]/div/div/div[2]/div[1]/h1/text()')[0]
author = main_html.xpath('/html/body/div[4]/div[1]/div/div/div[2]/div[1]/div/p[1]/text()')[0]
update = main_html.xpath('/html/body/div[4]/div[1]/div/div/div[2]/div[1]/div/p[5]/text()')[0]
introduction = main_html.xpath('/html/body/div[4]/div[1]/div/div/div[2]/div[2]/text()')[0]
# 输出结果以验证
print(bookTitle)
print(author)
print(update)
print(introduction)
输出结果如下:
至此,使用基本的网页请求get方法获取目标页面中的特定内容结束,接下来就是正文解析了,
go go go
正文爬取与解析
开始爬取正文。首先尝试获取单个页面的数据,再尝试设计一个循环,依次获取所有正文数据
单个页面数据获取
打开第一章,获取章节标题和正文的XPath路径如下:
书名://*[@id="container"]/div/div/div[2]/h1
内容://*[@id="content"]
按照与上文一致的方法请求并解析网页内容,代码如下:
注意到在用这个xpath路径的时候我们是只要里面的文字部分所以要多加一个/text()
import requests
from lxml import etree
# 请求头
headers= {
'User-Agent': '...',
'Cookie': '...',
'Host': 'www.365kk.cc',
'Connection': 'keep-alive'
}
# 当前页面链接
url = 'http://www.365kk.cc/1/1053/10094192.html'
resp = requests.get(url, headers)
text = resp.content.decode('utf-8')
html = etree.HTML(text)
title = html.xpath('//*[@id="container"]/div/div/div[2]/h1/text()')[0]
contents = html.xpath('//*[@id="content"]/text()')
print(title)
for content in contents:
print(content)
可以看出,我们成功获取了小说第一章第一页的标题和正文部分
接下来我们将它储存在一个txt文本文档中,关于文件读取类型主要有(并不全,只是本代码用到的)
'w'
: 清空原文档,重新写入文档 open(filename, ‘w’)'r'
: 仅读取文档,不改变其内容 open(filename, ‘r’)'a'
: 在原文档之后追加内容 open(filename, ‘a’)
文档命名为之前获取的书名 bookTitle.txt
,完整的代码如下:
import requests
from lxml import etree
# 请求头
headers= {
'User-Agent': '...',
'Cookie': '...',
'Host': 'www.365kk.cc',
'Connection': 'keep-alive'
}
# 小说主页
main_url = "http://www.365kk.cc/1/1053/"
# 使用get方法请求网页
main_resp = requests.get(main_url, headers=headers)
# 将网页内容按utf-8规范解码为文本形式
main_text = main_resp.content.decode('utf-8')
# 将文本内容创建为可解析元素
main_html = etree.HTML(main_text)
bookTitle = main_html.xpath('/html/body/div[4]/div[1]/div/div/div[2]/div[1]/h1/text()')[0]
author = main_html.xpath('/html/body/div[4]/div[1]/div/div/div[2]/div[1]/div/p[1]/text()')[0]
update = main_html.xpath('/html/body/div[4]/div[1]/div/div/div[2]/div[1]/div/p[5]/text()')[0]
introduction = main_html.xpath('/html/body/div[4]/div[1]/div/div/div[2]/div[2]/text()')[0]
# 第一章页面链接
url = 'http://www.365kk.cc/1/1053/10094192.html'
resp = requests.get(url, headers)
text = resp.content.decode('utf-8')
html = etree.HTML(text)
title = html.xpath('//*[@id="container"]/div/div/div[2]/h1/text()')[0]
contents = html.xpath('//*[@id="content"]/text()')
with open(bookTitle + '.txt', 'w', encoding='utf-8') as f:
f.write(title)
for content in contents:
f.write(content)
# 在储存文件时,每储存一段,就写入两个换行符 `\n`,避免大段文字堆积使格式更便于阅读
f.write('\n\n')
f.close()
运行结束后,可以看到在代码文件的同路径中,已经生成了一个文本文档。至此,我们已经完成了单个页面的数据爬取和存储,接下来只要设计循环,实现顺序爬取所有页面即可。
爬取所有页面
我们注意到,正文的每个页面底部,都有一个按钮下一页,其在网页中的结构为:
在XPath路径的末尾添加 @href 用于获取属性 href 的值:
//*[@id="container"]/div/div/div[2]/div[3]/a[3]/@href
注意到如果一章结束,下一页和下一章的链接就会有些差别
观察不同页面的链接,可以看出前缀是一致的,区别仅在后缀上,比如第一章第一页和第一章第二页的链接分别为:
http://www.365kk.cc/1/1053/10094192.html
http://www.365kk.cc/1/1053/10094192_2.html
因此,我们只需要获取下一页的链接后缀,再与前缀拼接,即可获得完整的访问链接。代码如下:
# 获取下一页链接的函数
def next_url(next_url_element):
nxturl = 'http://www.365kk.cc/1/1053/'
# rfind('/') 获取最后一个'/'字符的索引
index = next_url_element.rfind('/') + 1
nxturl += next_url_element[index:]
return nxturl
url1 = '/1/1053/10094192_2.html'
url2 = '10094193.html'
print(next_url(url1))
print(next_url(url2))
在爬取某一页面的内容后,我们获取下一页的链接,并请求该链接指向的网页,重复这一过程直到全部爬取完毕为止,即可实现正文的爬取。
在这一过程中,需要注意的问题有:
某一章节的内容可能分布在多个页面中,每个页面的章节标题是一致的,这一标题只需存储一次;
请求网页内容的频率不宜过高,频繁地使用同一IP地址请求网页,会触发站点的反爬虫机制,禁止你的IP继续访问网站;
爬取一次全文耗时较长,为了便于测试,我们需要先尝试爬取少量内容,代码调试完成后再爬取全文;
爬取的起点为第一章第一页,爬取的终点可以自行设置;
按照上述思想,爬取前4个页面作为测试,完整的代码如下:
import requests
from lxml import etree
import time
import random
# 获取下一页链接的函数
def next_url(next_url_element):
nxturl = 'http://www.365kk.cc/1/1053/'
# rfind('/') 获取最后一个'/'字符的索引
index = next_url_element.rfind('/') + 1
nxturl += next_url_element[index:]
return nxturl
# 请求头,需要添加你的浏览器信息才可以运行
headers= {
'User-Agent': '...',
'Cookie': '...',
'Host': 'www.365kk.cc',
'Connection': 'keep-alive'
}
# 小说主页
main_url = "http://www.365kk.cc/1/1053/"
# 使用get方法请求网页
main_resp = requests.get(main_url, headers=headers)
# 将网页内容按utf-8规范解码为文本形式
main_text = main_resp.content.decode('utf-8')
# 将文本内容创建为可解析元素
main_html = etree.HTML(main_text)
bookTitle = main_html.xpath('/html/body/div[4]/div[1]/div/div/div[2]/div[1]/h1/text()')[0]
author = main_html.xpath('/html/body/div[4]/div[1]/div/div/div[2]/div[1]/div/p[1]/text()')[0]
update = main_html.xpath('/html/body/div[4]/div[1]/div/div/div[2]/div[1]/div/p[5]/text()')[0]
# 调试期间仅爬取4个页面
maxPages = 4
cnt = 0
# 记录上一章节的标题
lastTitle = ''
# 爬取起点
url = 'http://www.365kk.cc/1/1053/10094192.html'
# 爬取终点
endurl = 'http://www.365kk.cc/1/1053/10094194.html'
while url != endurl:
cnt += 1 # 记录当前爬取的页面
if cnt > maxPages:
break # 当爬取的页面数超过maxPages时停止
resp = requests.get(url, headers)
text = resp.content.decode('utf-8')
html = etree.HTML(text)
title = html.xpath('//*[@class="title"]/text()')[0]
contents = html.xpath('//*[@id="content"]/text()')
# 输出爬取进度信息
print("cnt: {}, title = {}, url = {}".format(cnt, title, url))
with open(bookTitle + '.txt', 'a', encoding='utf-8') as f:
if title != lastTitle: # 章节标题改变
f.write(title) # 写入新的章节标题
lastTitle = title # 更新章节标题
for content in contents:
f.write(content)
f.write('\n\n')
f.close()
# 获取"下一页"按钮指向的链接
next_url_element = html.xpath('//*[@class="section-opt m-bottom-opt"]/a[3]/@href')[0]
# 传入函数next_url得到下一页链接
url = next_url(next_url_element)
sleepTime = random.randint(2, 5) # 产生一个2~5之间的随机数
time.sleep(sleepTime) # 暂停2~5之间随机的秒数
print("complete!")
运行结果如下:
数据清洗
观察我们得到的文本文档,可以发现如下问题:
- 缺乏书籍信息,如之前获取的书名、作者、最后更新时间;
- 切换页面时,尤其是同一章节的不同页面之间空行过多;
- 每章节第一段缩进与其他段落不一致;
- 不同章节之间缺乏显眼的分隔符;
为了解决这些问题,我们编写一个函数 clean_data()
来实现数据清洗,清洗后的文档中,每段段首无缩进,段与段之间仅空一行,不同章节之间插入20个字符 -
用以区分,问题得以解决。
完整代码,需要自取哦
小说爬虫