获取网址和Header

这次爬取轻小说是在我昨天刚发现的一个轻小说网站 真白萌Web小镇, 该网站并没有浏览器检测, 所以并不用伪装浏览器, 直接使用 requests.get(url)即可.

分析源代码

我们先以 《那个人后来》 这本轻小说为例:

主页面

我们首先分析它的主页面, 点击右键查看源代码, 这里列取比较重要的部分:

1
2
3
4
5
6
7
8
......
<title>那个人后来 - 真白萌Web小镇 - Powered by Discuz!</title>
......
<th class="common">
<a href="javascript:;" id="content_1011" class="showcontent y" title="更多操作" onclick="CONTENT_TID='1011';CONTENT_ID='normalthread_1011';showMenu({'ctrlid':this.id,'menuid':'content_menu'})"></a>
<em>[<a href="https://masiro.moe/forum.php?mod=forumdisplay&fid=74&amp;filter=typeid&typeid=190">第一章</a>]</em> <a href="https://masiro.moe/forum.php?mod=viewthread&tid=1011&extra=page%3D1" style="color: #3C9D40;" onclick="atarget(this)" class="s xst">177 积累压力的工作</a>
</th>
......

我们可以从中看到, 小说的名称储存在 tag ‘title’ 中; 小说每个章节 储存在 tag ‘a’ 的 class=”s xst” 中的 ‘herf’ 中, 但是储存在这这个块里的链接不仅仅是小说章节, 还有很多乱七八糟的链接, 这时就需要我们匹配小说链接独特的 text 了, 小说的 text 都是有规律的, 有 “第xx话”, “第xx章”, “第xx回”, “xx章”, “xx话”, “xx回”等, 我们可以使用正则匹配 text=re.compile(r”^[\d]|[第]”). 接下来要将所有章节的网址储存起来, 以便我们接下来对每个章节内容进行爬取.

这样我们可以写出以下代码

1
2
3
4
5
6
7
8
9
r = requests.request('GET', URL)
soup = BeautifulSoup(r.text, 'html.parser')
bookName = soup.title.string
infoList = [] # 储存各个章节的网址
r = requests.request('GET', url)
soup = BeautifulSoup(r.text, 'html.parser')
tag = soup.findAll('a', class_="s xst", text=re.compile(r"[^\d]|[第]"))
for j in range(len(tag) - 1, -1, -1): # 由于章节是反序的, 所有我们储存章节时也用反序
infoList.append(tag[j]['href'])

分页

有的小说的章节主页面放不下, 可能会分多页储存. 这时我们先观察各个页码链接的特征, 可以发现小说主页面第一页的 url 为 https://masiro.moe/forum.php?mod=forumdisplay&fid=74&page=1 , 第二页的 url 为 https://masiro.moe/forum.php?mod=forumdisplay&fid=74&page=2 , 只改变了 page 的值, 再看主页面的源代码, <span title="共 4 页"> 小说的总页数是储存在 tag ‘span’ 的 ‘title’ 中, 于是我们就可以写出以下代码:

1
2
pageTag = soup.find('span', title=re.compile(r"^共"))
totalPage = int(page['title'].split(' ')[1])

然后我们再根据总页数, 即可找到各个页面的网页.
到现在为止, 我们可以写出以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
r = requests.request('GET', URL)
soup = BeautifulSoup(r.text, 'html.parser')
infoList = [] # 储存章节链接
totalPage = 1 # 总页数
bookName = soup.title.string # 书名
pageTag = soup.find('span', title=re.compile(r"^共")) # 当前页面总页数的tag, 如果不存在, 总页数即为默认值 1.
if(pageTag):
totalPage = int(pageTag['title'].split(' ')[1])
for i in range(totalPage, 0, -1): # 各个页面也需要反向
url = URL + "&page={}".format(i)
r = requests.request('GET', url)
soup = BeautifulSoup(r.text, 'html.parser')
tag = soup.findAll('a', class_="s xst", text=re.compile(r"[^\d]|[第]"))
for j in range(len(tag) - 1, -1, -1):
infoList.append(tag[j]['href'])

文章

我们进入文章界面, 右键打开源代码文件, 寻找文章所在的位置; 可以看到, 文章位于 tag ‘table’ 中的第五个 ‘table’, 网页的文章排版比较乱, 我们先将换行符和空白区域全部删除, 再针对文章必有换行符</ br>, 使用正则匹配 re.compile(r"br/>[^<]+<|>[^<]+<br/") .

提取文章的完整代码为:

1
2
3
4
5
6
7
8
9
10
for i in range(len(infoList)):    # 提取所有的章节
r = requests.request('GET', infoList[i])
soup = BeautifulSoup(r.text, 'html.parser')
title = soup.title.string # 获取文章的标题
tag = soup.find_all('table')[4]
text = tag.prettify()
text = text.replace('\n', '') # 去除换行符
text = text.replace(' ', '') # 去除空格
rule = re.compile(r"br/>[^<]+<|>[^<]+<br/") #匹配文章
content = re.findall(rule, text)

写入文件

我们再将获取的文章写入txt, 需要注意的是, 我们获取的文本并不全是文章, 还包含了<br/>符号, 需要再次匹配获取纯文章.

1
2
3
4
5
6
f.write('\n\n\n\n' + title + '\n\n')    # 写入章节名, 换行保证排版整洁
for i in range(len(content)): # 写入当前章节的所有段落
rule = re.compile(r">[^<]+<")
line = rule.search(content[i]).group()
f.write(" " + line[1:-1] + "\n") # 段前空格, 保证排版整洁
f.flush()

完整代码

到这里我们就基本完成了轻小说的爬取, 这里我贴出完整代码, 事实上还有很多地方需要改进, 这里贴出的代码仅供学习:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#!/usr/bin/python

# -*- coding : utf-8 -*-

import requests
import time
from bs4 import BeautifulSoup
import re


def getInfoList(URL): # 获取所有章节网页地址和数名
totalPage = 1 # 该书总页数, 默认为一
try:
r = requests.request('GET', URL) # 获取主网页
except:
print("获取网页失败, 请稍后再试")
soup = BeautifulSoup(r.text, 'html.parser')
infoList = [] # 储存所有章节地址
pageTag = soup.find('span', title=re.compile(r"^共")) # 获取总页数
bookName = soup.title.string # 获取书名
if(pageTag): # 如果只有一页的话是没有总页数的, 此时总页数为默认的1
totalPage = int(pageTag['title'].split(' ')[1])
for i in range(totalPage, 0, -1): # 从所有网页获取全部章节, 由于章节排序反向, 所以页面反序
url = URL + "&page={}".format(i) # 按页数获取页面网址
r = requests.request('GET', url)
soup = BeautifulSoup(r.text, 'html.parser')
tag = soup.findAll('a', class_="s xst",
text=re.compile(r"^[\d]|[第]")) # 匹配章节网址
for j in range(len(tag) - 1, -1, -1): # 反向存如章节网址
infoList.append(tag[j]['href'])
return infoList, bookName


def getbook(infoList, bookName): # 获取整本书并写入文本
f = open(bookName + ".txt", 'w', encoding='utf-8')
f.write(bookName)
for i in range(len(infoList)): # 访问所有章节地址
time.sleep(0.01) # 减少频繁访问风险
try:
r = requests.request('GET', infoList[i])
except:
print("分析失败了, 稍后再试吧")
soup = BeautifulSoup(r.text, 'html.parser')
title = soup.title.string # 获取文章标题
tag = soup.find_all('table')[4] # 获取文章所在的table
text = tag.prettify() # 获取文本
text = text.replace('\n', '')
text = text.replace(' ', '')
rule = re.compile(r"br/>[^<]+<|>[^<]+<br/")
content = re.findall(rule, text) # 匹配文章段落
__write(content, title, f)
f.close()
print("下载完成")


def __write(content, title, f): # 写入文本文件
try:
f.write('\n\n\n\n' + title + '\n\n') # 写入章节名, 换行保证排版整洁
for i in range(len(content)): # 写入当前章节的所有段落
rule = re.compile(r">([^<]+)<")
line = rule.search(content[i]).group(1)
f.write(" " + line + "\n") # 段前空格, 保证排版整洁
print("完成 " + title)
f.flush()
except:
print("写入章节失败")


if __name__ == "__main__": # 主函数
URL = input(请输入要下载的网址) # 获取地址
print("url为: " + URL + "\n开始下载...")
infoList, bookName = getInfoList(URL)
getbook(infoList, bookName)

用Scrapy库写的爬虫代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# -*- coding: utf-8 -*-
import scrapy
import re
import os
from bs4 import BeautifulSoup


class DemoSpider(scrapy.Spider): # 需要继承scrapy.Spider类
name = 'demo' # 定义爬虫名
start_urls = ['https://masiro.moe/forum.php?mod=forumdisplay&fid=74']
bookName = 'none'

def parse(self, response):
soup = BeautifulSoup(response.body, 'html.parser')
totalPage = 1
pageTag = soup.find('span', title=re.compile(r"^共")) # 获取总页数
self.bookName = soup.title.string.split(' ')[0] # 获取书名
if not os.path.exists(self.bookName):
os.mkdir(self.bookName)
if(pageTag): # 如果只有一页的话是没有总页数的, 此时总页数为默认的1
totalPage = int(pageTag['title'].split(' ')[1])
for i in range(totalPage, 0, -1): # 从所有网页获取全部章节, 由于章节排序反向, 所以页面反序
url = self.start_urls[0] + "&page={}".format(i) # 按页数获取页面网址
yield scrapy.Request(url=url, callback=self.parse_pages)

def parse_pages(self, response):
soup = BeautifulSoup(response.body, 'html.parser')
tag = soup.findAll('a', class_="s xst",
text=re.compile(r"^[\d]|第")) # 匹配章节网址
infoList = [] # 储存所有章节地址
for j in range(len(tag) - 1, -1, -1): # 反向存如章节网址
infoList.append(tag[j]['href'])
for url in infoList:
yield scrapy.Request(url=url, callback=self.parse_web)

def parse_web(self, response):
soup = BeautifulSoup(response.body, 'html.parser')
title = soup.title.string # 获取文章标题
tag = soup.find_all('table')[4] # 获取文章所在的table
text = tag.prettify() # 获取文本
text = text.replace('\n', '')
text = text.replace(' ', '')
rule = re.compile(r"br/>[^<]+<|>[^<]+<br/")
content = re.findall(rule, text) # 匹配文章段落
fname = self.bookName + '/' + title.split(' ')[0] + '.txt'
with open(fname, 'wb') as f: # python文件操作
f.write(('\n\n\n' + title + '\n\n').encode('utf-8')) # 写入章节名, 换行保证排版整洁
for i in range(len(content)): # 写入当前章节的所有段落
rule = re.compile(r">[^<]+<")
line = rule.search(content[i]).group() # 段前空格, 保证排版整洁
f.write((" " + line[1:-1] + "\n").encode('utf-8'))
self.log('成功写入: %s' % title) # 打印日志