从爬小说开始学爬虫

1.学爬虫从烦恼开始

你是否遇到一些不具备下载功能的小说网站,比如这个网站
当你想下载到本地却无能为力时,只能抱头痛哭
要是你有这样的烦恼,请继续往下看
我只教有用且常用的知识,所有与本项目无关的知识,感兴趣自己去发现
还有就是大佬们就没必要看了,以下全是萌新经验

2.先把必要的工具安装

这步虽然无聊,但是所有的快乐都是从这一步开始的,已经安装的就可以跳过了

2.1 下载并安装python,下载链接

注意事项:

  • 打开安装包,请勾选上ADD Python 3.X to PATH,这一步的目的是方便你在控制台(cmd)上使用pyhon命令
  • 这时你已经可以通过python自带的idle编写python程序了,但它没联想代码功能,很菜!一般只用来试验你忘记的语法

2.2 安装世界上最好的python IDE PyCharm,下载链接

  • 下载community版本就可以,即免费又可以满足大部分功能,安装过程网上一大堆,就不赘述了

2.3 安装必要的python模块,毕竟python是面向模块编程(笑)

控制台(cmd)中输入

pip install requests
pip install re
pip install lxml 

BeautifulSoup就不安装了,有兴趣去百度

3.爬小说一般步骤

爬虫最简单步骤

获取地址
根据地址下载网页
从下载的网页中解析出目标内容

爬小说的一般步骤

章节网址列表
章节网页列表
获取小说
目录所在地址
下载小说
目录网页
解析小说
目录网页
单/多线程
下载网页
解析网页
存储小说内容

对比两者步骤的相似之处,融汇贯通,希望你能理解到什么
思考一番之后,其实你可以看到,我们主要需要两个东西
下载器两个解析器

3.1 下载器

给出下载器通用代码,可以自己DIY
一些不用登陆就可查看的网页,基本都可用它下载

#下载器
def get_html(url, cookies='', encoding='utf-8', origin=0):
    i = 7 #若未响应,反复请求次数
    for j in range(i):
        user_agent = [
            "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
            "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)",
            "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)",
        ]

        header = {
            'user-agent': random.choice(user_agent),
            'cookie': cookies
        }

        try:
            r = requests.get(url, headers=header, verify=False, timeout=5, cookies=cookies)
            if origin == 0:
                return r.content.decode(encoding, errors='ignore')
            else:
                return r
        except:
            pass
    return 0
            

理解和使用这个函数:

  • 学习requests模块的get方法:很舒服的教程 看完get部分就回来,一切以爬小说为最终目的
  • 关于get_html参数的含义
参数名称含义
url网页地址
cookies需要伪登录才会用到,现在没什么用
encoding网页编码,把网页解析成文本时用到的编码
origin由于requests函数返回的是Response对象,而很多情况下的网页爬取时,我们仅需其文本
所以设置此参数来控制get_html函数返回的是文本还是Response对象,默认为文本
  • 随机选择user_agent的目的
    user_agent是标识身份的一个手段,有些网址会检测你的user_agent来判断你是否为爬虫,而通过随机选择一些浏览器的user_agent可在一定程度上伪装爬虫身份
  • 加循环的目的
    有些小说网站访问极慢所以只一次访问可能会超时,所以加一个异常捕捉和循环来反复访问
  • 网页编码极为重要,特别对于爬小说来说,网页编码和你设置的encoding不同就会导致中文乱码,所以网页编码一致极为重要.
    查看网页编码方式:
    谷歌和360浏览器右键都有查看源代码的功能,找到下图位置
    在这里插入图片描述
    可以看到网页编码为gbk,常见的编码格式为utf-8和gbk
    特别提醒:不要使用requests模块自带的函数判断网站编码,必须亲力亲为

3.2 解析器

解析器各不相同,从上述爬取小说步骤可看出,我们需要两个解析器

  • 一个解析目录网页,获取所有章节的地址,用列表存起来
    例如["http://XXXX.com/XXXX/1","http://XXXX.com/XXXX/2"....]
  • 另一个解析具体某一章节的网页,获取小说文本
    例如["这是文本",......]

解析器虽然各不相同,但可用两个模块解决大部分解析器的编写
relxml模块,一个正则表达式,一个xpath
有人会觉得正则表达式好难啊,但是…真的难
不过学习一句正则语法,便可应付大部分你写着玩玩的爬虫了
比如这是你的网页a

abc我要爬取这个东西jjjj
abc我还要爬取这个东西jjjj

你便可用以下语句

>>>import re
>>> re.search("abc(.*?)jjjj",a)
<re.Match object; span=(1, 16), match='abc我要爬取这个东西jjjj'>

>>> re.search("abc(.*?)jjjj",a).group(1)
'我要爬取这个东西'

>>> re.search("abc(.*?)jjjj",a).group()
'abc我要爬取这个东西jjjj'

>>> re.findall("abc(.*?)jjjj",a)
['我要爬取这个东西', '我还要爬取这个东西']

注意一下几点

  • 点代表任意字符,星代表重复前面的字符(0次或多次),?代表非贪婪(具体什么,感兴趣自己查)
  • (.*?)的前缀一定要特别,后缀不需要特别,因为 (.*?)为非贪婪模式
    可以理解为:遇到特殊的前缀(abc),程序开始匹配,直到匹配到后缀(jjjj),不管后面是否还有(jjjj),就到此为止了,这也是非贪婪模式的特性
  • 正则里的点(也就是.*?里面的点)默认不匹配换行符,比如你的网页是这样,那就不能得偿所愿了
abc我要爬取
这个东西jjjj

解决方法如下

>>> import re
>>> re.search("abc(.*?)jjjj",b)
>>> 
>>>> a = re.search("abc(.*?)jjjj",b,re.S)
>>> print(a)
<re.Match object; span=(1, 17), match='abc我要爬取\n这个东西jjjj'>

到这里,最最常用的用法说完了.大家可能会问了,你这么一句就能解决所有问题了?当然…不能
但是够用,不够用还有xpath呢,其他正则知识有兴趣自己去学哦
下面用正则,直接实战编写解析器,实战网址:这是链接

3.3 实战解析器

这次实战主要需要达到以下几个目的:
1.爬取所有章节的网址
2.爬取所有章节的章节名
代码如下:

## 0.导入必须的模块
import re
import requests


## 1.明确你要爬取的网址,确定网页编码为gbk
url = "http://www.quanshuwang.com/book/0/269"


## 2.下载你要爬取的网页文本

#r_text = get_html(url,encoding = "gbk")  #你们可以导入这个下载器,也可以直接使用requests模块,毕竟只访问一次,防止代码臃肿,故不加入get_html的代码

r = requests.get(url = url,
                 headers={
                     'user-agent':'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)'
                     },
                     timeout = 5
                 )
#伪装user_agent必不可少

#将字节类型的r.content转化为文本类型的r_text
r_text = r.conten.decode("gbk")

到这里先刹车,我们分析网页结构,右键查看网页源代码
在这里插入图片描述
由于我们的目的是获取所有章节的网址和名字,所以使用findall方法,继续敲代码,这次也出现了两个(.*?),记得理解其妙处,以及对比其与search方法的区别

## 3.解析目录网页,获取目标内容
result1 = re.findall('<li><a href="(.*?)" title="(.*?)">',r_text)
print(result1)
#区别search方法
result2 = re.search('<li><a href="(.*?)" title="(.*?)">',r_text)
print(result2.group(1),result2.group(2))

结果输出,省略部分输出

[('http://www.quanshuwang.com/book/0/269/78850.html', '第一章 山边小村,共2741字'),('http://www.quanshuwang.com/book/0/269/78854.html', '第二章 青牛镇,共2281字'),...]
http://www.quanshuwang.com/book/0/269/78850.html 第一章 山边小村,共2741

可以看到,findall将全部结果得到,并且将两个括号内的内容打包成元组返回
而search方法却只是返回第一个匹配到的,并且还用到了group()方法

3.3 用xpath完成解析器

同样是这个项目,更改第三步的代码,当然要记得在代码最前面加上from lxml import etree,只要用到etree
观察源代码
在这里插入图片描述
写代码

xx = etree.HTML(r_text)
urls = xx.xpath('//div[@class="clearfix dirconone"]/li/a/@href')
chapterTitles = xx.xpath('//div[@class="clearfix dirconone"]/li/a/text()')
print(urls)
print(chapterTitles)

结果输出,省略部分输出

['http://www.quanshuwang.com/book/0/269/78850.html', 'http://www.quanshuwang.com/book/0/269/78854.html',...]
['第一章 山边小村', '第二章 青牛镇', '第三章 七玄门',...]

xpath很好用,相信你从代码中已经知晓其十分之九的用法(这里指最常用的),所以只解释一下几点,其他复杂的语法形式感兴趣自己去学习

  • 为什么xpath语句前面要加//?(一般都加,不是说不加就不行)
    首先解释//的含义,比如//div,它代表从所有的节点中找名字叫div的节点,某个节点/a就代表只从某个节点的儿子里面找叫a的节点.
  • []里面的内容代表什么?
    起筛选作用,如上述代码'//div[@class="clearfix dirconone"]',可以理解为:从所有的div节点中,筛选出它的class属性等于clearfix dirconone的节点
  • 其他的从字面意思理解就可以了

3.4 多线程爬取章节网页

是不是经过上述的学习,以为自己已经可以愉快地爬取小说了?
我来猜猜你的思路,由上述我们已经获得所有章节的网址,如下:

urls = ['http://www.quanshuwang.com/book/0/269/78850.html', 'http://www.quanshuwang.com/book/0/269/78854.html',...]

然后一个for循环

for url in urls:
	chapterText = get_html(url,encoding = "gbk")
	编写一个章节网页的解析器(正则或xpath)
	文件存储

运行过程

提交一个
一个章节的网页
多个下载事务
get_html下载器
章节网页解析,
并存储小说内容

感觉完事大吉.
但是! 你在实际爬取过程中,会发现速度极慢,尤其在网站速度较慢的情况下.原因就是你这个程序是单线程的,也就是一章一章的下载,其中一章下载慢,会导致整体速度下降,所以将上述伪代码改造为

from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor(50)
#创建一个50大小的线程池
def parse(r):
	r = r.result() #返回的r不是文本,所以需要使用result方法转化为文本
	编写一个章节网页的解析器(正则或xpath)
	文件存储
for url in urls:
	pool.submit(get_html,url,encoding ="gbk").add_done_callback(parse)
	#        下载网页函数 网址 编码             返回结果至回调函数parse

很多人到这可能蒙了,这TM是什么代码.
大致讲解一下运行过程:

开始一个新线程,提交一个
一个章节的网页
多个下载事务
get_html下载器
章节网页解析,
并存储小说内容
一个线程终结

这个和单线程的不同之处在哪呢?
可以看到,程序将多个下载网页的任务放在了不同的线程当中,所以线程之间不需要等待其他线程是否下载完毕,从而提高爬取速率.
运行机制理解之后,我们就可以知道,在不同的情况下,我需要改哪些东西
1.如果你是使用的是和我一样的下载器gethtml,那么大部分都不用改
2.submit方法的参数url’和encoding全是根据get_html函数来设置的,如果你自己DIY下载器,那么需要去衡量,加入哪些参数.
3.add_done_callback(parse)表示增加回调函数
在一个线程里,该线程会将get_html的返回值打包成一个RunnableFuture对象,并将这个对象返回给回调函数parse,所以对返回的RunnableFuture对象使用result方法,可以得到get_html函数的返回文本.
关于RunnableFuture对象的其他方法和特性,在我看来是不需要了解的,同样感兴趣的话,自己百度,笔者自己也不会.

3.5文件存储

到这里很多人可能会说,文件存储谁不会?用open函数打开文件,然后一章一章地写入小说内容
虽然单线程爬取小说可以使用这种方式,比如

with open("novle.txt","a+",encoding = "utf-8") as f:
	f.write(某一章的标题+"\n")
	f.write(某一章的小说内容)

结果:

第一章
这是第一章的内容
第二章
这是第二章的内容
...

这在单线程是完全可以采用的,但在多线程可就不好用了.
因为很多章节的下载任务时并行的.有可能在50个线程中,分别在下载第一至第五十章的内容,最后下载内容可能是这样:

第三章
这是第三章的内容
第四章
这是第五章的内容
第一章
这是第一章的内容
...

结果完全乱序
造成这样的原因,就是线程完成的顺序是不完全取决于提交下载事务的顺序,也取决于某个章节网页下载和解析的速度.
比如第一章的网页下载极慢,同时第二章的网页下载事务已经提交并且解析完毕,那么先写入文件的就是第二章.
为了解决这个问题,我提出一种简单解决方案,当然还有很多
完整项目的伪代码:

import requests
from lxml import etree
from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor(50)
#创建一个50大小的线程池
def get_html():
	#下载器

def parse(r):
	#第二个解析器
	r,taskId = r.result() #返回的r不是文本,所以需要使用result方法转化为元组
	编写一个章节网页的解析器(正则或xpath)
	with open("novle_{}.txt".format((taskId).zfill(4)),"a+",encoding = "utf-8") as f:
		f.write(某一章的标题+"\n")
		f.write(某一章的小说内容)
	
def guodu(url,taskId,encoding):
	#过渡函数
	r = get_html(url,encoding = encoding)
	return (r,taskId)#返回元组给函数parse,包含章节文本和任务id
def main():
	#下载目录网页
	url = "http://www.quanshuwang.com/book/0/269"
	r_text = get_html(url,encoding = "gbk")
	
	#解析目录网页(第一个解析器)
	#获取
	#1.所有章节的网址:urls
	#2.所以章节的标题:chapterTitles
	xx = etree.HTML(r_text)
	urls = xx.xpath('//div[@class="clearfix dirconone"]/li/a/@href')
	chapterTitles = xx.xpath('//div[@class="clearfix dirconone"]/li/a/text()')
	
	#下载各个章节的网页,并将结果返回给parse函数解析并存储
	taskId = 0
	for url in urls:
		taskId += 1
		pool.submit(guodu,url,taskId = taskId,encoding ="gbk").add_done_callback(parse)
		#        下载网页函数 网址 编码             返回结果至回调函数parse
if __name__ == "__main__":
	main()

观察整体代码,发现没有离开一个下载器两个解析器.
爬取完成后,下载文件应该是:

novle_0001.txt   novle_0002.txt   novle_0003.txt...

同样解释几个可能存在的疑问:
1.为什么加入guodu函数?
可以通过代码得出,过渡函数本质调用的还是下载器get_html,它的存在就是传递下载顺序taskId变量,通过它我们可以得到当前线程是下载第章的线程,方便给对应的txt文件命名.
2.为什么要给txt文件命名?为什么这样命名?
第一,为了区别不同章节的文本
第二,记录章节与章节之间的顺序
3.这么多文本怎么阅读?
通过控制台命令合并.这也是为什么要保证章节文件顺序的原因
合并有很多方法:

copy *.txt out 
type *.txt >out

别把out写出out.txt,原因自己试试(笑)

总结

翻到最后,发现没有完整代码,甚至无法凑成完整项目.这有点顶,对吧
其实呢,我写这个的目的,是真的想让看的人学到点什么.若是你看懂了下载器和第一个解析器的代码,以及第二个解析器的伪代码.其实你就已经具备编写章节网页解析器的能力了,所以我就不赘述了
完整项目的伪代码也给出,所以应该没什么大问题了

若是你实在不会,可以留言问我,我大概或许有时间会回答,或者以后有时间写成完整的项目(大概吧)

第一次写博客,或许有很多错误和一些微不足道的知识点,所以大神们轻喷.
同时我也相信我的文章会给很多小小小小萌新一点点点点启示,毕竟我曾经也是其中一份子.

评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值