前言:
本次项目分为两部分。
第一部分编写的爬虫主要功能为爬取小说相关信息,例如小说标题、作者、简介以及小说链接等,并保存至mongoDB。随后对其增加了交互式界面,实现了小说种类的分类以及页面数限制,最后可获得感兴趣小说的完整小说内容并且自动创建文件夹保存至本地。
第二部分编写的爬虫主要是实现大规模的小说爬取,将小说网站上的所有小说爬取下来,自动创建好文件下保存至本地。
本篇文章仅介绍第一部分,对第二部分感兴趣可直接查看下一篇。
1、准备工作
本次爬虫使用的库主要是requests、使用xpath解析,数据库使用pymongo。
2、爬取目标
本次爬取得目标站点为起点网的免费小说,URL为https://www.qidian.com/free/all。需要爬取得内容为小说标题、作者、简介以及小说链接等。
3、提取信息
按照惯例,首先需要对网站进行分析。访问该网站很容易发现,小说所有信息都是属于静态加载,这样一来我们提取相关信息就非常简单了。
打开开发者模式,如图一。可以看到页面所有小说相关内容在data-rid为属性的li标签下。用xpath匹配出所有li节点即可。
图一
接下来,遍历出每一个节点,然后提取出每一个小说的相关信息。这里仅以小说标题为例子,其他信息的获取不再赘述。
图二
如图二,小说标题title在div标签下的h4的标签下,运用xpath易得到title的匹配规则为//div[@class="book-mid-info"]/h4/a/text(),这样我们就很轻松的得到了小说标题。
下面只需要用同样的方法提取出所需要的信息,然后使用一个字典包含所有信息即可,代码实现如下:
1 html= etree.HTML(response.text) 2 # 所有小说 3 items = html.xpath('//li[@data-rid]') 4 # 遍历每个小说 5 for item in items: 6 # 标题 7 title = item.xpath('.//div[@class="book-mid-info"]/h4/a/text()')[0] 8 # 作者 9 author = item.xpath('.//p[@class="author"]/a[@class="name"][1]/text()')[0] 10 # 封面 11 cover = item.xpath('.//div[@class="book-img-box"]/a/img/@src')[0] 12 # 简介 13 intro = item.xpath('.//p[@class="intro"]//text()')[0].strip() + '...' 14 # 链接 15 link = item.xpath('.//div[@class="book-img-box"]/a/@href')[0] 16 # 包含各个信息的字典 17 novel = { 18 'title': title, 19 'author': author, 20 'cover': cover, 21 'intro': intro, 22 'link': link 23 } 24 yield novel
4、spider
编写好提取小说相关信息的方法后,接下来需要完善好一个spider类,要求能够得到页面的response,然后解析提取出小说信息,并且还要将信息保存到mongoDB中。实现代码如下:
1 class Spider(object): 2 mongo_uri = 'localhost' 3 mongo_db = 'xiaoshuo' 4 5 def __init__(self, url): 6 self.url = url 7 self.client = pymongo.MongoClient(self.mongo_uri) 8 self.db = self.client[self.mongo_db] 9 print('正在爬取第%s页..' % self.page) 10 11 def get_html(self): 12 """ 13 得到页面 14 :return: 返回响应体 15 """ 16 headers = { 17 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) ' 18 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36', } 19 response = requests.get(url=self.url, headers=headers) 20 return response.text 21 22 def parse(self,html): 23 """ 24 解析页面,得到小说相关信息 25 :param html: 页面html 26 :return: 返回包含小说信息的字典novel 27 """ 28 html= etree.HTML(html) 29 # 所有小说 30 items = html.xpath('//li[@data-rid]') 31 # 遍历每个小说 32 for item in items: 33 # 标题 34 title = item.xpath('.//div[@class="book-mid-info"]/h4/a/text()')[0] 35 # 作者 36 author = item.xpath('.//p[@class="author"]/a[@class="name"][1]/text()')[0] 37 # 封面 38 cover = item.xpath('.//div[@class="book-img-box"]/a/img/@src')[0] 39 # 简介 40 intro = item.xpath('.//p[@class="intro"]//text()')[0].strip() + '...' 41 # 链接 42 link = item.xpath('.//div[@class="book-img-box"]/a/@href')[0] 43 # 包含各个信息的字典 44 novel = { 45 'title': title, 46 'author': author, 47 'cover': cover, 48 'intro': intro, 49 'link': link 50 } 51 yield novel 52 53 def write_to_mongoDB(self, novel): 54 """ 55 把结果保存到mongoDB 56 :param novel: 小说 57 """ 58 if self.db[self.collection].insert(novel): 59 print('保存成功!')
写好这个spider类后,需要创建一个实例对象,传入小说网站URL,再调用相关方法就可以得到最后的结果了。
1 spider = Spider(url=url) 2 # 调用相应方法 3 html = spider.get_html() 4 novel = spider.parse(html) 5 spider.write_to_mongoDB(novel)
打开mongoDB可以查看到获取的结果,如图三所示。
图三
6、interface
上述中,虽然我们得到了小说相关信息,但是得到的还只是一页的信息,而且没有小说分类,不太好分类整理信息。所以这里希望增加一个模块inter来实现交互式界面,提供一个可选择小说种类以及爬取的最大页面数的功能,这样就可以分类保存小说一定数量的小说了。
话不多说,首先来看小说网站,如图四。
图四
选择玄幻类的小说,可以看到页面的URL有两个比较重要的参数chanID和page。
chanID以及后面的数字就是代表就是小说的类别,如图五。
图五
玄幻类的小说chanID=21,科幻类chanID=9,多选取几个小说类别,构建一个小说类别以及对应的chanID的值的字典novel_list,如下:
1 novel_list = { 2 '奇幻': '?chanld=1', 3 '武侠': '?chanld=2', 4 '仙侠': '?chanld=22', 5 '都市': '?chanId=4', 6 '历史': '?chanId=5', 7 '游戏': '?chanId=7', 8 '科幻': '?chanId=9', 9 '灵异': '?chanId=10', 10 '短篇': '?chanId=20076', 11 '玄幻': '?chanld=21' 12 }
接下来只需要写一个参数category来接收用户输入的小说种类,将输入的值当做小说列表的键值输入,就能得到后面的chanID值了,之后把这个值和首页的URL做一个拼接,就能得到特定类别小说的URL,比如输入玄幻就能得到玄幻小说的URL。代码实现如下 :
1 # 起始URL 2 root_url = 'https://www.qidian.com/free/all' 3 4 # 小说种类URL参数列表 5 novel_list = { 6 '奇幻': '?chanld=1', 7 '武侠': '?chanld=2', 8 '仙侠': '?chanld=22', 9 '都市': '?chanId=4', 10 '历史': '?chanId=5', 11 '游戏': '?chanId=7', 12 '科幻': '?chanId=9', 13 '灵异': '?chanId=10', 14 '短篇': '?chanId=20076', 15 '玄幻': '?chanld=21' 16 } 17 18 19 def interface(): 20 print('-' * 50) 21 print('小说种类清单:') 22 print(''' 23 奇幻 24 武侠 25 仙侠 26 都市 27 历史 28 游戏 29 科幻 30 灵异 31 短篇 32 玄幻 33 ''') 34 print('-' * 50) 35 36 37 def menu(): 38 """ 39 接口函数,得到小说种类category 40 :return: 返回对应的url以及category 41 """ 42 category = str(input('请输入你要看的小说类别:')) 43 chanId = novel_list[category] 44 # 构造出完整的URL 45 url = root_url + chanId 46 return url, category
这样一来,可以根据用户的喜好来爬取相应类别的小说了。
解决了小说分类的问题,接下来就要来解决翻下一页的问题了。
其实很简单,刚刚提到过page参数,和之前很多次项目一样,page参数就是页面数,也就是说要爬多少页的内容就把page依次遍历出来,构建出新的URL即可,
即爬取100页,就可以把page从range(1,101)里遍历出来得到每一个page,在构建好每一页的URL。这里同样也可以根据用户的需要来确定页数,代码如下:
1 max_page = 1000 # 可以自定义最大页数 2 input_page = int(input('请输入你好爬取的页数(最大页数为:%s):' % max_page)) 3 if 0 < input_page <= max_page: 4 for page in range(1, input_page + 1): 5 # 构造完整URL(每一个页面) 6 new_url = url + '&page=%s' % page 7 spider = Spider(url=new_url, page=page)
这样就可以得到用户感兴趣类别的小说信息了。
运行结果如下图所示。
7、小说内容
上述爬虫仅仅只能获取小说的信息。看到茫茫多的小说简介,遇到感兴趣的小说,有事难免会想看看小说内容。所以笔者觉得在新增一个爬虫,用来实现获取感兴趣小说的小说内容。
打开目标网站,选取一本小说,如图六。
图六
可以到看该小说的URL就是之前爬取小说信息里面的link,如图七。
图七
也就是说,接下来要写的爬虫,我们希望只要把感兴趣小说的link从数据库里直接复制出来再传入,就能得到该小说的完成小说内容了,并且希望能够根据章节名自动创建小说的文件目录。
回到主题,刚刚看到打开的网页里面,点击目录,可以到小说的章节,同样也可以观察到URL后面增加了一个#Catalog字段,如图八。
图八
随意选取一个章节,点开如图九,即为小说内容。
图九
那么,我们现在的思路是,首先需要爬取所有大的章节和小说名称,例如上面的第一大章节“山寨共20章”(这里需要对大章节名和小说名称做非法字符的处理,因为需要用章节名创建文件目录,不处理会出现报错,下面小章节也是同理),在得到的大章节下,获取所有小章节,例如“第一章 山远有客自远”,得到小章节的名称和链接link。最后在请求link,使用xpath提取得到小说的文本内容保存至本地文件夹中即可。具体代码如下:
1 import re 2 3 from lxml import etree 4 import requests 5 import os 6 import random 7 8 9 class ContentSpider(object): 10 # UA列表 11 UserAgent_list = [ 12 'Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)', 13 "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", 14 "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)", 15 "Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", 16 "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)", 17 "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)", 18 "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)", 19 "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)", 20 "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)", 21 "Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6", 22 "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1", 23 "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0", 24 "Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5", 25 "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.8) Gecko Fedora/1.9.0.8-1.fc10 Kazehakase/0.5.6", 26 "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11", 27 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20", 28 ] 29 30 def __init__(self, url): 31 self.url = url 32 self.user_agent = random.choice(self.UserAgent_list) 33 34 35 def get_content(self): 36 headers = { 37 'User-Agent': self.user_agent, } 38 response = requests.get(url=self.url, headers=headers) 39 html = etree.HTML(response.text) 40 # name是小说名称 41 novel_name = html.xpath('//div[@class="book-info "]/h1/em/text()')[0] 42 # 处理非法字符 43 name = re.sub(r'[\\/:*?"<>|\r\n]+', '_', novel_name) 44 # items是所有小说所有章节目录 45 items = html.xpath('//div[@class="volume"]') 46 for item in items: 47 # 章节名 48 chapter_name = "".join(item.xpath('.//h3/text()')).strip() 49 # 处理非法字符 50 chapter = re.sub(r'[\\/:*?"<>|\r\n]+', '_', chapter_name) 51 # 章节下所有小章节 52 sections = item.xpath('.//li[@data-rid]') 53 # 遍历出每个小章节的名称和链接 54 for section in sections: 55 # 小章节名称 56 title_name = section.xpath('./a/text()')[0] 57 # 处理非法字符 58 title = re.sub(r'[\\/:*?"<>|\r\n]+', '_', title_name) 59 # 小章节链接 60 link = section.xpath('./a/@href')[0] 61 # 含有小说名称name,章节名称chapter,小章节名称title,以及链接link的字典info 62 info = { 63 'name': name, 64 'chapter': chapter, 65 'title': title, 66 'link': 'https:' + link 67 } 68 grand_path = 'D:/自选小说/' + info['name'] 69 # 小章节路径 70 father_path = grand_path + '/' + info['chapter'] 71 # 小说文件路径 72 child_path = father_path + '/' + info['title'] 73 isExist1 = os.path.exists(grand_path) 74 isExist2 = os.path.exists(father_path) 75 # 文件不存在就创建文件 76 if not isExist1: 77 os.mkdir(path=grand_path) 78 if not isExist2: 79 os.mkdir(path=father_path) 80 # 小说文本内容的response 81 child_response = requests.get(url=info['link'], headers=headers) 82 child_html = etree.HTML(child_response.text) 83 # 小说文本,换行处理 84 content = '\n'.join(child_html.xpath('//div[@class="read-content j_readContent"]//p/text()')) 85 with open(child_path + '.txt', 'w') as file: 86 file.write(content) 87 print(content) 88 89 90 91 if __name__ == '__main__': 92 try: 93 input = str(input('请输入小说的URL:')) 94 url = 'https:' + input + '#Catalog' 95 spider = ContentSpider(url) 96 spider.get_content() 97 except UnicodeEncodeError: 98 print('写入出现错误')
这样一来爬取小说文本内容的spider也写好了。
8、运行结果
下面来运行一下代码,如图十。
图十
打开mongoDB,选取一本说,以《山海八荒录》为例,选取其link并输入,回车运行,可以看到爬虫运行,如图十一:
图十一
可以看到我们获取到了小说的文本内容,接下来打开文件查看是否保存到了本地,如图十二:
图十二
可以看到《山海八荒录》已经创建好,点击进去查看更多,如图十三,图十四
图十三
图十四
(这里图十四的文本排列顺序并不是按顺序排列,但是有的小说却是按照顺序排列的,笔者暂时还不明原因。)
9、完整代码
完整代码见:Github
结语:
书到用时方恨少,事非经过不知难。
山重水复疑无路,柳暗花明又一村。