新年好
爬虫离我们生活很近,诺基亚时代正是爬虫的帮助才成就了那么多网络小说。同时,它也是一门很简单的技术,但可能涉及到编程,很多朋友就认为学之不易。
前天David来南京“跨年”,我就找到以前看网课的笔记,1小时帮助他完成了自己爬虫的编写,借着机会我也就将其整理成文,各位集中精力,抽两局吃鸡的时间不仅可以掌握一个有用的小技术,还可以熟悉python语言特性,产生对编程的兴趣。
爬虫原理
那我们现在开始计时。
首先,大家先打开链接
www.anaconda.com/download
下载对应系统的python3.7安装包,我们边下载边介绍。
这个版本的python拥有丰富的类库,让我们的使用更加便捷。
接着,我们谈谈为什么我们能爬取网页的信息。
大家打开自己的浏览器,随便搜索一个网址,比如news.baidu.com进入百度新闻的界面。
大家按F12打开浏览器的开发者工具,点击Sources可以看到左侧有个(index)的文件,里面写满了密集的代码。
对,这就是这个网页的源码,也即不考虑交互的话,如果你把这些代码复制粘贴了,就可以创建一个一模一样的网页界面。
恩,还看到了百度的直男彩蛋。
我们将网页下拉一点,用鼠标点击开发者工具左上角的那个方块+鼠标的图标,然后将鼠标挪到网页上的一条新闻上,发现开发者工具的代码也在对应变化,这里对应到一个:
我们爬虫爬的就是两个<>里面的文字内容,而这些<>就是网页的元素。
爬虫的原理就是通过网页源码的元素定位,然后来获取里面相应的文字内容,就是这么简单。
说到这里大家应该下载好了,点击安装包按默认设置安装。
等待安装的时间让我们来介绍网页的基本框架。
html
大家都听过html,这是一种网页框架协议,全称为Hyper Text Mark-up Language,也即超文本标记语言,不只是文本。
html的基本框架如下,大家可以在桌面新建一个txt文件,把这些代码复制并保存文件关闭后,将文件重命名为demo.html,打开就是一个网页的界面,可以看到,这里网页的元素是由这样前后闭合的标签组成的。
指定网页的标题但这样的网页是没有灵魂的。
网页需要有整齐的排版,大部分网页都是利用DIV(division)将自己分成很多块;
网页也需要有人机交互,所以网页加入了很多表单元素;
交互和排版的需要,使得网页源码里充满了有结构性的元素,这种结构组成了网页的DOM结构树。
并且这些元素还有着对应的标签名、id、类名和属性。如下图,p和div都是标签名,class是类名,id是id值(网页id值写的话就唯一)。
p标签
最后,也是给我们启发的一点,网页排版后要利用CSS对元素进行配色等前端设计,需要我们先对元素先进行定位。
CSS里面有着非常便捷的选择器,主要包括三大基本选择器和基于结构的扩展选择器。以上图为例,想要将"新年好"的颜色设置成红色,我们可以这么写(更多扩展选择器在PPT有介绍):
之后我们的爬虫就是依据这样的原理快速定位并提取文本信息。
jupyter notebook
现在大家应该安装完毕,准备开始编写爬虫了。
本文展示的案例是Frank之前写文献综述时总需要去知网查询实在嫌麻烦,就爬取了《保险研究》期刊历史的论文标题、作者、摘要、期次,并将其导出到excel表,只要在excel搜索即可定位。最后效果如下:
我们先打开anaconda界面里的jupyter notebook,这是一种便捷的笔记本,可以进行代码的运行。
点击右上角的new--> Python3新建我们第一个python文件
jupyter notebook代码是填写在一个个cell里面,代码写完后按Ctrl+Enter即可执行,点击图标"+"可以往下添加新的cell,便于给代码按功能分块
爬取逻辑
接着,我们打开网址,搜索保险研究并进入官网。
爬取数据要先观察一条数据和整个数据的逻辑关系,再开始着手进行代码书写。千里之行,就始于这第一条我们需要的数据。
这里大家直接点击官网左侧的过刊浏览,在新页面点击上方数字的01期。
在新界面按下F12打开上面提到的开发者工具,点击左侧上方的鼠标选择器,并移到第一篇文章的标题上,下方自然出现了对应的网页源码。
我们注意到这个标题是在标签下的
标签的标签里,借助上面我们提到的CSS选择器工具,可以很快定位获取指定数据。
再想办法将这个提取方法运用到这一期的所有文章,并最后将所有期次汇总即可。
之后编写代码过程我们仍要不断在这个网页观察如何准确定位元素。
书写代码
下面我们就来代码实现这个方法,大家按照图中代码书写即可,强调一下所有的符号都是英文标点:
我们先导入requests的包,利用requests.get方法可以获取网页地址,复制粘贴刚才2018年01期的网址:
http://bxyj.cbpt.cnki.net/WKE/WebPublication/wkTextContent.aspx?colType=4&yt=2018&st=01
在python语言中加上双引号/单引号是用来告诉函数这是一个字符串而不是变量。
接着自定义res变量来接收传入的网页,用print函数来进行输出(代码运行是按Ctrl+Enter),发现无内容,因为这里返回的是网页对象,print一下res.text发现输出了网页的源码。python是用#来添加注释的。
这里提一下为什么是request.get(url)。
浏览器向服务器传输数据的方式分为POST和GET两种,POST传输依靠表单项,比如传输账号密码;GET传输则是将参数直接附在地址栏后传输,适用于安全性要求低的传输,百度搜索就是用这个原理。
刚才print有大段输出,我们在代码前加#注释再运行隐去输出结果。
获取了网页的源码,接下来我们导入BeautifulSoup包(据说源自爱丽丝漫游奇境记的甲鱼汤),它能帮我们便捷定位和获取文本内容。自定义变量soup来接上网页的源码,用"html.parser"告诉这是一个html框架的源码。
我们用soup.select可以利用前面介绍的CSS选择器方法快速定位到目标元素。
我们先来获取文章标题,在这个过程中需要我们不断观察刚才的页面源码,利用F12+选择器分析如何准确定位到指定元素。
Frank发现第一个文章的标题在
标签的标签下,所以用soup.select方法,里面填写上面提到CSS扩展选择器中的后代选择器,这里填写"li h3"。
print一下发现输出的是图中标记3的部分,是一个包含该页所有文章标题的列表。
list(列表)是python的一种容器,用"[ ]"从0开始依次存储各种元素,元素之间用","隔开。
用list[数字]可以得到指定位置上的元素。这里title[0]即获取列表第一个元素,也就是我们要的第一个标题。
我们print一下title[0],这次的输出结果在标记2的位置,发现得到了第一个标题,可是仍然包含了html的元素格式。
BeautifulSoup提供了text方法,我们只要在title[0]后加.text,print一下,标记1处终于输出了想要的文本。
我们按照这种方法,能够依次获取标题、作者、摘要。
需要注意的是这三个都是列表,并且三个列表长度相同、列表同一位置的元素同属于一篇文章,这样在后面不会产生错位。
其中日期是Frank观察到先通过CSS选择器的类名选择器来指定到,再用a[href="#"]指定到它的特殊属性这样更精确,也能帮助大家熟悉各种选择器。
需要注意的是因为外面用双引号表示文本,所以里面的#需要用单引号指定文本,不然双引号会提前闭合。
接下来,我们需要有一个字典接入对应我们获取的内容。
字典也是python的一种容器,和list容器的有序排列不同,字典通过{"键":"值"}的方式来存储数据。类比到excel上,第一行就是我们的键,下面就是我们要存储的值。
python可以通过 字典["键名"]来设置和获取对应的值,和excel中一行数据有多个属性一样,一个字典也可以存入多个键值对。
上面提到过标题、作者、摘要是三个长度相同、位置对应的列表。
那么只要我们指定一个循环,依次从三个列表提取元素放到字典,再将这些字典一起放到列表,就可以将01期所有文章信息都提取出来。这里用到了for循环。
for循环是python的基本循环之一,可以通过指定变量和范围来设置循环次数,每次循环结束变量+1,直到变量超过这个范围停止循环。
这里设置了变量i,让其在0到len(title)的范围循环,len(title)输出的是列表的长度,也即元素个数,range(0,len(title))指定了一个大于等于0但小于(不等于)len(title)的范围,当i超过这个范围循环停止。
每次循环我们都将paper设置为空字典,往里面加上列表i位置的文本,由于都是一期的文章所以date和i无关。
因为paper每次循环都被清空,所以在循环外我们新建了一个空列表paper_list,用列表的append方法存取字典。
写完输出,发现01期的文章收录成功。
成功在望
离目标只差一步。
我们只要将01期的这个方法(也可以叫函数)运用到所有期次就能大功告成。
这就牵涉到两个问题:
Q1.如何将代码封装成一个方法?
Q2.如何获取所有期次?
先解决Q1:
在python里通过如下格式即可开始定义我们的方法。
def 方法名(传入参数):
...
return 传出参数
python严格而优美的排版要求方法和循环下的每行都要空一个tab的空位,即四个空格。
在这个例子里面,方法需要传入的参数是网页地址url,传出参数是paper_list
但是在定义方法的时候,我们并没有传入一个真正的网址,而是自定义一个变量名来代替要传入的网址形式,我们将其称之为形式参数。后续传入真正的网址叫做实际参数。
我们这里的形参叫做content,这个content代替之前操作的01期《保险研究》网址,按照之前的流程,只要在requests.get获取,之后写上之前的代码,最后return出paper_list。
方法定义好之后我们测试一下,将01期的网址赋值给url,用getcontents(url)将实参传入,发现代码输出了和之前一样的结果。
现在来看Q2:如何获取所有期次?
每个网站的设计都有自己的逻辑,我们这个网站的逻辑是什么呢?
url = "http://bxyj.cbpt.cnki.net/WKE/WebPublication/wkTextContent.aspx?colType=4&yt=2018&st=01"
大家仔细看看我们这个地址,发现结尾处有yt=2018,st=01。
这难道就是用GET方法向网站发送了一个2018年01期的请求吗?
Frank测试了一下发现果然如此,那么接下来就好办多了
只要每次向上面的getcontents方法传入带有这种规律的url作为实际参数,再将每期的输出用一个新的列表存储就可以了。
上面提到url是作为一个字符串传入,python的format可以很方便的对字符串的指定内容进行替换。我们先将url的yt=和st=的部分挖去并填入{},{}在这里起到一个标记的作用。
再利用url.format()的方法传入指定参数到标记好的{}位置,有几个{}传入几个参数,这里需要两个。
最后我们分别对年份和月份循环,每次循环都生成一个新的url,并调用getcontents(url)来获取这个url也即《保险研究》某期的文章内容。
这里 s = "%02d"%j是因为在月份为个位数的时候传入参数是1而不是01,所以用"%02d"%j将j变成两位的十进制数,不够两位在左边补0。
写完运行,因为网页比较多,所以需要代码先飞一会。左边显示In[*]表示程序正在运行,运行完毕后*会变成数字。数据太多就不print了,用len看看是否生成了那么多期。
输出数据
数据都获取了,可以着手输出了。
不过这里大家要注意一个以后会经常用到的trick。
最早我们用字典存入了一个文章的元素
paper=
{“期次”:“201801期”, “标题”: ”XXXXXX”, “作者”: “David Zhao”, “摘要”: ”大猪蹄子”}
后来我们用getcontens()返回的列表接存储了很多字典
paper_list = [paper1,paper2,paper3…]
之后我们又写了一个对url的循环,并且用列表存储,生成了:
total = [paper_list1, paper_list2…]
这就意味着total是两层列表:
total = [
[paper11,paper12,paper13…], [paper21,paper22,paper23…]...
]
而python输出到excel二维表的数据的格式应该是
[paper11,paper12,paper13...paper21,paper22,paper23...]
这样excel才能将每个字典同名的键提取出来作为表头,将具有相同键的值依次放入行中。
所以我们要去掉一层列表,不卖关子,直接用两层for循环:
第一次for循环读取total的paper_list,第二次循环读取paper_list的paper,最后将其放入total列表当中,这样就形成了可以导出到excel的数据。
这里for循环没有用in range(),而是直接用 in 列表,这是一种python的语法结构,因为列表是有序的,所以用in 列表的形式可以依次获取列表元素,而不需要再用list[i]的方法。前面没有直接这样写是帮助大家熟悉python的语法规则。
最后利用pandas包导出。pandas的名称来自于面板数据(panel data)和python数据分析(data analysis),主要用于经济金融的分析。
回到jupyter notebook的网页主界面,按时间排序一下发现生成了我们命名的文件,双击下载并打开,调整一下格式即可。
至此,案例结束,David理解并打完,掐表一共花了1个小时多一丢丢。
一个图梳理一下我们的操作:
结语
以上就是一个网络爬虫的小案例,我们发现爬虫其实很简单,只要梳理好网页逻辑,能够利用CSS选择器快速定位到元素,以及熟悉字符串的操作就能快速爬取数据。
难点是在后面对数据的清洗分析,现在Frank暂时放下金融去达内学Java大数据了,后续有空再给大家继续做教程。
以一个爬虫为线索,Frank在这里给大家介绍了html原理、CSS语法规则、python的列表、字典、循环、方法定义,以及很多小trick。
希望能帮助大家了解爬虫,喜欢上编程这个工具。本文一带而过的一些术语,比如DOM、扩展选择器等我都整理了一个PPT,大家在公众号后台回复“爬虫”即可获取网盘链接。
最后欢迎大家转发讨论~~
附上所用的代码截图:
找对象扫码