声明:
本系列文章原创于慕课网,作者秋名山车神,任何人不得以任何形式在不经作者允许的情况下,进行任何形式的印刷以及销售,转载需注明出处及此声明。
本系列文章更新至少每周一更,将涉及Python爬虫基础,Requests,Scrapy等主流爬虫技术。同时会介绍图片验证码,语音验证码的识别以及我自己设计的一个高并发可扩展易维护的集群爬虫架构。
对文章有任何问题请在下面留言,我会不定期的回复大家。
人非圣贤,如果文章有错别字请大家自行区分或指正出来,我将不定期修改错误的地方。
本系列能否持久更新下去离不开大家的支持与鼓励,以及对原创版权的尊重。
作者想说的话
最近一段时间特别的忙,事情也很多。有几家出版社找着写书,都让我给推了,昨天闲着没事在翻慕课网的手记时,发现了这个系列的文章。看到那么多人浏览,那么多人评论让快点更新,我觉得不能让大家失望,所以我开始更新了。
我想做的事情很很普通,就是希望我所知道的技术能够以一种力所能及的方式带给大家,我希望慕课网是一个积极进取的社区,每个人都能毫无保留的对待别人。也希望我做的这件普通的事情,能够帮助到每一个想学习python,学习爬虫的人。谢谢大家的支持。
大师的嗅觉
在上一章中,我们已经学习了如何使用 urllib 来从网络上获取信息,这一章我们来学习如果从这些信息中提取我们想要的内容。
依然以我们的慕课网为例,顺便让我们回顾一下上一章的代码。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import urllib.request
# 创建一个Request对象
req = urllib.request.Request('http://www.imooc.com')
# 使用Request对象发送请求
response = urllib.request.urlopen(req)
print(response.read())
如果运行上面的代码,就可以看到慕课网首页的静态代码了,也就是我们通常所说的HTML代码。
那这么多的数据,我们怎么能从中获取到想要的内容呢?比如我们想要获取慕课网首页实战推荐里面所有推荐的实战课名称,该怎么办呢?
此时我们就可以使用另外一个库 BeautifulSoup
安装BeautifulSoup
打开我们的命令行工具,输入:
pip install beautifulsoup4==4.6.0
如果你那里下载的速度特别慢,可以使用下面这条命令,指定本次使用豆瓣的pip镜像源来安装:
pip install beautifulsoup4==4.6.0 -i https://pypi.douban.com/simple
看到如下的信息就表示安装成功了:
Successfully installed beautifulsoup4-4.6.0
有的小伙伴可能还会看到一些黄色的文字:
You are using pip version 9.0.1, however version 10.0.1 is available.
You should consider upgrading via the 'python -m pip install --upgrade pip' command.
这是说我们的pip版本太低了,提示我们可以使用 python -m pip install --upgrade pip
这条命令来升级到最新版本,一般不需要升级,如果想要升级的可以使用这条命令,升级以后就不会再出这个提示了。
在实战中学习
不用读系列:我并不想把本教程做成一个官方文档,或者是像其他书那样,把官方文档抄下来。这样又有什么意义呢?学了以后还是不知道怎么应用,所以我希望每一个库我们都是通过一个小项目来学习的,用到的每一个方法我都会详细的说明。而用不到的方法,它都用不到了,我们还学它干嘛?
大家跟着我一步一步学习怎么狩猎吧
首先我们打开慕课网的首页:https://www.imooc.com
建议使用Chrome浏览器
然后找到我们的实战推荐,在其中一个推荐的实战课图片上,右键点击图片,选择检查。
然后会打开浏览器的开发者工具,如果还不知道这个工具怎么用的,大家可以去看一下我的免费课程:
接着我们选择开发者工具上面的选择器,使用鼠标左键点击我们想要获取的地方:
可以看到下面的代码就是我们想要获取的内容:
<h3 class="course-card-name">全网最热Python3入门+进阶 更快上手实际开发</h3>
那么其他的几个也是这样的吗?重复上面的步骤点击每个实战推荐的名字,看到如下结果:
第一个狩猎技巧
想要获取同类型的内容,就寻找他们相同的部分。
那么上面的实战推荐中,很显然他们相同的部分就是 <h3 class="course-card-name"></h3>
,那接下来就直接修改上面的代码来试着获取这部分的内容。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import urllib.request
# 导包
from bs4 import BeautifulSoup
# 创建一个Request对象
req = urllib.request.Request('http://www.imooc.com')
# 使用Request对象发送请求
response = urllib.request.urlopen(req)
soup = BeautifulSoup(response.read())
course_names = soup.find_all('h3', {'class':'course-card-name'})
print(course_names)
from bs4 import BeautifulSoup 这句代码呢,意思就是从bs4这个模块里,导入我们的 BeautifulSoup 方法,没什么其他的含义。
BeautifulSoup() 方法接收两个参数,第一个参数是一个str类型的,就是我们获取到的HTML代码。第二个参数也是一个str类型的,表示我们希望使用哪个库来解析HTML的代码,常用的都 html.parser
和 lxml
,其中lxml
效率更高一些。而我们上面的代码并没有指定第二个参数,所以它会输出一个警告信息。
UserWarning: No parser was explicitly specified, so I'm using the best available HTML parser for this system ("lxml"). This usually isn't a problem, but if you run this code on another system, or in a different virtual environment, it may use a different parser and behave differently.
The code that caused this warning is on line 5 of the file C:dreampythonAnaconda3Scriptsipython-script.py. To get rid of this warning, change code that looks like this:
BeautifulSoup(YOUR_MARKUP})
to this:
BeautifulSoup(YOUR_MARKUP, "lxml")
markup_type=markup_type))
关键在于倒数第二行 BeautifulSoup(YOUR_MARKUP, "lxml")
,这里提示的很明显,让我们增加第二个参数 lxml
。当然了,这是因为我的电脑里安装了 lxml
库,没有安装的小伙伴,这里提示的可能是 html.parser
,不管提示的是什么,你都按照BeautifulSoup提示你的来进行修改就可以了~
我这里按照提示修改以后的那句代码就成了:
soup = BeautifulSoup(response.read(), "lxml")
这个方法还会给我们返回一个bs4.BeautifulSoup的对象,我们可以使用一个变量来接收,这个变量可以是任意名字,这里我们用 soup 来表示。
soup.find_all() 这个方法接收两个参数,代表从 soup 这个对象里,搜索全部符合条件的内容。第一个参数是str类型的,表示我们要从哪个HTML标签中获取,这里我们是要从 h3 这个标签里获取。第二个dict类型的,接收key和value的键值对。代表我们要获取的这个h3标签,有那些属性可以来标识它。
<h3 class="course-card-name">全网最热Python3入门+进阶 更快上手实际开发</h3>
可以看到我们想获取的h3标签,只有一个class属性,那第二个参数就是:
{"class": "course-card-name"}
该方法返回一个 bs4.element.ResultSet
类型的对象,类似于我们python的list列表,我们可以像操作列表一样来操作这个对象。同样使用一个变量去接收它。
最终上面的代码运行以后,可能看到如下的结果:
[<h3 class="course-card-name">前端进阶:响应式开发与常用框架</h3>,
<h3 class="course-card-name">揭秘一线互联网企业 前端JavaScript高级面试</h3>,
...省略N条...
<h3 class="course-card-name">Python Flask 构建微电影视频网站</h3>,
<h3 class="course-card-name">AWS云-深度学习&amp;机器学习的AI业务应用</h3>]
提示:由于慕课网是不断在更新的,所以当你看到这篇教程的时候,相关的内容可能已经更换了名称,比如class不叫course-card-name。我希望你能够自己修改上面的代码,来达到符合你看到教程时,慕课网的样子。
我们发现,结果和我们预想的不太一样,这是为什么呢?我们再次回到慕课网,右键在网页空白的部分单击,然后选择查看网页源码:
然后在弹出来的窗口中,按下 ctrl + f
的组合快捷键,接着搜索 course-card-name
。
嗯,我相信大家知道组合键的意思就是先按下键盘上的
Ctrl
键,然后不松开这个键的情况下,再按键盘上的F
键。
看到的搜索结果如下:
出现了76个搜索结果,这是为什么呢?我们明明只是想获取实战推荐里面的课程名称,为什么出现了这么多的搜索结果?
简单思考一下我们就可以知道,我们刚才搜索是从 response.read()
里面搜索的,这代表了整个慕课网首页的所有内容。而慕课网所有的课程名称,使用的都是下面的样式:
<h3 class="course-card-name"></h3>
所以我们不仅获取了实战推荐的课程名称,还获取了其他不想要的课程名称。
爬虫并不是获取的越多越好,而是精准获取我们想要的数据,其他不想要的数据越少越好。
那么很显然,是我们狩猎的范围太大了,试着来缩小我们的狩猎范围吧。
同样使用浏览器的开发者工具,慢慢的移动我们的鼠标,直到鼠标指向的内容,刚好包裹我们想要获取的那一部分。
如图所示,最终我们指向了一个div的标签,它覆盖了我们想要获取的所有内容,而没有覆盖其他我们不想获取的内容。并且这个div也有一个class属性,通过上面学习到的,来修改一下我们的代码,看看是不是能达到我们的目的。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import urllib.request
# 导包
from bs4 import BeautifulSoup
# 创建一个Request对象
req = urllib.request.Request('http://www.imooc.com')
# 使用Request对象发送请求
response = urllib.request.urlopen(req)
soup = BeautifulSoup(response.read(), "lxml")
# 先获取我们刚才找到的div
course_div = soup.find_all('div', {'class':'clearfix types-content'})
# 再从我们的div里获取想要的内容
course_names = course_div.find_all('h3', {'class':'course-card-name'})
# 结果是否是我们想要的呢?
print(course_names)
结果就报错了~
AttributeError: ResultSet object has no attribute 'find_all'. You're probably treating a list of items like a single item. Did you call find_all() when you meant to call find()?
其实大家看到报错,不要害怕,要慢慢的看这个报错的内容,一般报错都是从倒数第一行开始看的,也只有倒数第一行往往才是有用的东西。
那么上面的这个报错很显然,find_all()方法返回的是一个ResultSet对象,这个对象不具有find_all()方法,所以在我们下面使用 course_div.find_all('h3', {'class':'course-card-name'})
时,就报错了。
但是不要慌,BeautifulSoup已经给出修改意见了,它说如果我们想要获取的是一个单一的内容,可以尝试使用find()方法,修改一下我们的代码,再次运行。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import urllib.request
# 导包
from bs4 import BeautifulSoup
# 创建一个Request对象
req = urllib.request.Request('http://www.imooc.com')
# 使用Request对象发送请求
response = urllib.request.urlopen(req)
soup = BeautifulSoup(response.read(), "lxml")
# 先获取我们刚才找到的div
course_div = soup.find('div', {'class':'clearfix types-content'})
# 再从我们的div里获取想要的内容
course_names = course_div.find_all('h3', {'class':'course-card-name'})
# 这次还会报错吗?
print(course_names)
结果如我们所料,没有报错,并且输出了如下内容:
[<h3 class="course-card-name">全网最热Python3入门+进阶 更快上手实际开发</h3>, <h3 class="course-card-name">玩转数据结构 从入门到进阶</h3>, <h3 class="course-card-name">Java企业级电商项目架 构演进之路 Tomcat集群与Redis分布式</h3>, <h3 class="course-card-name">React Native技术精讲与高质量上线APP开发</h3>, <h3 class="course-card-name"> Vue2.5开发去哪儿网App 从零基础入门到实战项目</h3>]
soup.find() 这个方法,唯一和我们find_all不同的点在于,他们返回的对象不同。find_all返回的是一个bs4.element.ResultSet列表对象。而find返回的是bs4.element.Tag。而find_all返回的列表中,包含的就是一个个的bs4.element.Tag对象。bs4.element.Tag对象,就具有了find或者是find_all的方法,以便我们不断的缩小范围,最终获取我们想要的结果。
你可能认为我们本章到此结束了,并没有结束。因为我们的目的还没达到,我上面说过了,一个好的爬虫就是除了我们想要的内容以外,别的什么都不要有。那我们输出的结果里,显然还包含了 <h3 class="course-card-name">
这些东西,那怎么样把它过滤掉呢?
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import urllib.request
# 导包
from bs4 import BeautifulSoup
# 创建一个Request对象
req = urllib.request.Request('http://www.imooc.com')
# 使用Request对象发送请求
response = urllib.request.urlopen(req)
soup = BeautifulSoup(response.read(), "lxml")
# 先获取我们刚才找到的div
course_div = soup.find('div', {'class':'clearfix types-content'})
# 再从我们的div里获取想要的内容
course_names = course_div.find_all('h3', {'class':'course-card-name'})
for course_name in course_names:
print(course_name.text)
只需要从我们的ResultSet列表对象中,取出每一个Tag对象,然后调用它的text属性,就能获取到标签中的文本内容了~
时间总是过的很快,大家可以在评论中告诉我,用本节学到的知识做了哪些有意思的事情。大家对本系列课程有任何的建议和疑问,都可以通过下方的留言告诉我,每个人的留言我都会看的。
同时大家也可以加入下面两个Python的交流群:
慕课网Python讨论群①:221828022
也可以加入我建的一个Python交流群:685024920
两个群我都有在里面,大家有任何的问题都可以在里面 @秋名山车神~
学习一下我们 liuyubobobo 老师的口头禅,大家加油~电动叉车
同时也希望大家多多支持 liuyubobobo 老师的实战课,多多强化自己的算法内功,早日成功一个优秀的 攻城狮。