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"....]
- 另一个解析具体某一章节的网页,获取小说文本
例如["这是文本",......]
解析器虽然各不相同,但可用两个模块解决大部分解析器的编写
re
和lxml
模块,一个正则表达式,一个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)
文件存储
运行过程
感觉完事大吉.
但是! 你在实际爬取过程中,会发现速度极慢,尤其在网站速度较慢的情况下.原因就是你这个程序是单线程的,也就是一章一章的下载,其中一章下载慢,会导致整体速度下降,所以将上述伪代码改造为
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是什么代码.
大致讲解一下运行过程:
这个和单线程的不同之处在哪呢?
可以看到,程序将多个下载网页的任务放在了不同的线程当中,所以线程之间不需要等待其他线程是否下载完毕,从而提高爬取速率.
运行机制理解之后,我们就可以知道,在不同的情况下,我需要改哪些东西
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,原因自己试试(笑)
总结
翻到最后,发现没有完整代码,甚至无法凑成完整项目.这有点顶,对吧
其实呢,我写这个的目的,是真的想让看的人学到点什么.若是你看懂了下载器和第一个解析器的代码,以及第二个解析器的伪代码.其实你就已经具备编写章节网页解析器的能力了,所以我就不赘述了
完整项目的伪代码也给出,所以应该没什么大问题了
若是你实在不会,可以留言问我,我大概或许有时间会回答,或者以后有时间写成完整的项目(大概吧)
第一次写博客,或许有很多错误和一些微不足道的知识点,所以大神们轻喷.
同时我也相信我的文章会给很多小小小小萌新一点点点点启示,毕竟我曾经也是其中一份子.