第7关 初识爬虫

不想你离开。 提交于 2020-02-06 22:36:22

你造吗,今天是个大喜的日子!来到这儿,就意味着你爬虫已经入门啦!

在这个重要又喜悦的日子里,我们就干三件事:回顾前路、项目实操、展望未来。

回顾前路,是为了复习0-6关所学的知识。项目实操,是通过写一个爬虫程序把所学的知识用起来。展望未来,是预告一下我们之后会遇到的风景。

马上开始吧~

回顾前路

在前面,我们按关卡学了好多好多知识。而这么多的内容,我们用【项目实现】和【知识地图】两张图就能说清。

【项目实现】: 任何完成项目的过程,都是由以下三步构成的。

在这里插入图片描述

先需要明确自己的目标是什么,然后分析一下如何实现这个目标,最后就可以去写代码了。

当然,这不是一个线性的过程,而可能出现“代码实现”碰壁后然后折返“分析过程”,再“代码实现”的情形。

接下来是【知识地图】:前面6关所讲的爬虫原理,在本质上,是一个我们所操作的对象在不断转换的过程。

在这里插入图片描述

总体上来说,从Response对象开始,我们就分成了两条路径,一条路径是数据放在HTML里,所以我们用BeautifulSoup库去解析数据和提取数据;另一条,数据作为Json存储起来,所以我们用response.json()方法去解析,然后提取、存储数据。

你需要根据具体的情况,来确定自己应该选择哪一条路径。

也可以参考图片上的注释,帮助自己去回忆不同对象的方法和属性。不过,老师还是希望你能把这个图记在心里。

好啦,0-6关的内容就梳理完成啦~

接下来是项目实操,这是今天的重头戏。

项目实操

我们会按照刚刚提过的项目三步骤来,首先是明确目标。

0. 明确目标

为了让大家能把前面所学全都复习一遍,老师为大家准备了一个项目:爬取知乎大v张佳玮的文章“标题”、“摘要”、“链接”,并存储到本地文件。

之所以选这个项目,是为了让大家能把前面所学全都复习一遍。

知道目标后,就可以【分析过程】了。

1. 分析过程

张佳玮的知乎文章URL在这里:https://www.zhihu.com/people/zhang-jia-wei/posts?page=1。(为了尽可能让你看到的内容和我一致,我选择了按点赞数给文章排序)
在这里插入图片描述

这是我做这个项目时,网站显示的内容。

点击右键——检查——Network,选All(而非XHR),然后刷新网页,点进去第0个请求:posts_by_votes,点Preview`。

在这里插入图片描述
发现有文章标题,看来数据是放在HTML里。

那么,走的应该是【知识地图】里上面那条路径:
在这里插入图片描述
那好,就可以去观察一下网页源代码了,我们点回Elements。

在这里插入图片描述

发现文章标题对应的就是元素里面的文本“大概,他们就是世上活得最明白的人吧。”还可以看到在标签里面,还有属性target="_blank",和data-za-detail-view-element_name=“Title”。

对于target="_blank"的属性,我们按下command+f(windows电脑是ctrl+f)去搜索target:
在这里插入图片描述

结果发现这个target属性并不能帮我们精准地定位到文章标题,所以排除使用这个属性。

而对于另一个属性data-za-detail-view-element_name=“Title”,你看这个属性命名如此freestyle,应该是知乎的开发人员自定义命名的,并不是html语法里标准的属性名。所以,我们不推荐使用。

好,让我们再定睛一看:
在这里插入图片描述

你会发现,这个<a标签>的上一个层级是<h2标签>,并且有class=“ContentItem-title”。仍然用command+f搜索“ContentItem-title”,发现这个属性可以帮我们精准定位目标数据,可以用。

到这里,我们的思路也差不多清晰起来。

获取数据——用requests库;解析数据——用BeautifulSoup库;提取数据——用BeautifulSoup里的find_all(),翻页的话观察第一页,到最后一页的网址特征,写循环;存储数据——用csv和openpyxl都可以。基本思路就应该是这样。

好,这就结束了分析过程,接下来就是敲代码啦。

2.代码实现

别一上来就整个大的,我们先试着拿一页的文章标题,看看能不能成功。
请阅读下面的代码,然后点击运行:

import requests
from bs4 import BeautifulSoup
#引入requests和bs
url='https://www.zhihu.com/people/zhang-jia-wei/posts/posts_by_votes?page=1'
headers={'user-agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'}
#使用headers是一种默认的习惯,默认你已经掌握啦~
res=requests.get(url,headers=headers)
#用resquest模块发起请求,将响应的结果赋值给变量res。
print(res.status_code)
#检查状态码 

发现状态码显示200,表示请求成功,放心了,可以继续往下。

请阅读下面的代码,然后点击运行:

import requests
from bs4 import BeautifulSoup
#引入request和bs
url='https://www.zhihu.com/people/zhang-jia-wei/posts/posts_by_votes?page=1'
headers={'user-agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'}
#使用headers是一种默认的习惯,默认你已经掌握啦~
res=requests.get(url,headers=headers)
#发起请求,将响应的结果赋值给变量res。
print(res.status_code)
#检查状态码 
bstitle=BeautifulSoup(res.text,'html.parser')
#用bs进行解析
title=bstitle.find_all(class_='ContentItem-title')
#提取我们想要的标签和里面的内容
print(title)
#打印title

见鬼了,为啥只有两个文章标题?

看起来是碰壁了,但遇上问题别慌,先来做个排查,使用 res.text 打印一下网页源代码。请阅读下面的代码,然后点击运行。

import requests
from bs4 import BeautifulSoup
#引入request和bs
url='https://www.zhihu.com/people/zhang-jia-wei/posts/posts_by_votes?page=1'
headers={'user-agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'}
#使用headers是一种默认的习惯,默认你已经掌握啦~
res=requests.get(url,headers=headers)
#发起请求,将响应的结果赋值给变量res。
print(res.status_code)
#检查状态码 
print(res.text) 
#打印网页源代码

打出来之后,我们把第一页的最后一篇文章的标题“出走半生,关山万里,归来仍是少女心气”在这个网页源代码里面搜索,呵呵,是搜不到的。🙃

再回过头看看:
在这里插入图片描述

原来,在HTML里面只放了两篇文章的数据,刚刚一不留神,就没注意。

这时,我们需要重新思考,完整的数据是放在哪里的?哎呀,其实我们现在已经回到分析过程了。
在这里插入图片描述

1.重新分析

我们打开Network,点开XHR,同时刷新页面,看到出现了很多个请求。

在这里插入图片描述

浏览一下,看到两个带articles的请求,感觉有戏。点开首个articles看看preview,一层层点开,看到“title:记住就是一切”,猜测这是一个文章标题。

在这里插入图片描述

在网页里面用command+f(windows电脑用ctrl+f)搜索一下“记住就是一切”,发现搜不到,奇怪。

那就看看跟首个articles请求长得很像的另一个articles的请求好啦,仍然看preview,看到title: “国产航母下水……让我想到李鸿章和北洋舰队”,仍然在网页里搜一下:
在这里插入图片描述
哎鸭!果然在这里。看来这个articles的请求里面存的是第一页的文章标题。这下妥了,我们知道向哪个url获取数据了。

那首个带articles的请求是什么?其实这是知乎的网站设计,当你刷新第一页的时候,默认你也请求了第二页的文章数据,这样你加载就会比较流畅。

通过同样的方法,我们还可以发现文章的链接、文章的摘要也是一样放在articles的请求里的,这里就不赘述了。

现在,理论上我们可以拿到第一页的文章数据了,那如果要拿到之后所有页面的数据,还不够吧。

好,我们去观察第1页对第2页的请求,和第2页里对第3页请求的参数区别,是在headers里面的query string parameters里面。
在这里插入图片描述
在这里插入图片描述

然后发现除了offset都一样,offset代表起始值,limit表示加载的限制数,通过循环我们是可以爬到所有页数的内容了。

我们的大致思路也就出来了。
在这里插入图片描述
好,分析过程搞定后,就又是写代码啦~

2.代码实现

我们可以参考上面的知识地图,一步一步来写,但老师认为,走到现在的你,应该是不用完全依赖这个图了。

接下来,想请你来写代码。不过我们不可能一下子写完,所以先请你用requests.get()获取数据,然后检查请求是否成功。

我们会用requests.get()来获取数据,但我们要先找到数据所在的具体请求的链接(Request URL);

封装headers,请在Headers面板中搜索user-agent,找到user-agent的信息;

封装params,请去Query String Parameters中复制信息;

然后再使用requests.get()发送请求,并使用response.status_code来检查请求的状态。

import requests
#引入requests
headers={'user-agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'}
#封装headers
url='https://www.zhihu.com/api/v4/members/zhang-jia-wei/articles?'
#写入网址
params={
    'include':'data[*].comment_count,suggest_edit,is_normal,thumbnail_extra_info,thumbnail,can_comment,comment_permission,admin_closed_comment,content,voteup_count,created,updated,upvoted_followees,voting,review_info,is_labeled,label_info;data[*].author.badge[?(type=best_answerer)].topics',
    'offset':'10',
    'limit':'20',
    'sort_by':'voteups',
    }
#封装参数
res=requests.get(url,headers=headers,params=params)
#发送请求,并把响应内容赋值到变量res里面
print(res.status_code)
#确认请求成功

终端显示200,请求成功,妥了。下一步是解析json数据。

请你思考一下,然后点击阅读代码,再点击运行。

import requests
#引入requests
headers={'user-agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'}
#封装headers
url='https://www.zhihu.com/api/v4/members/zhang-jia-wei/articles?'
#写入网址
params={
    'include':'data[*].comment_count,suggest_edit,is_normal,thumbnail_extra_info,thumbnail,can_comment,comment_permission,admin_closed_comment,content,voteup_count,created,updated,upvoted_followees,voting,review_info,is_labeled,label_info;data[*].author.badge[?(type=best_answerer)].topics',
    'offset':'10',
    'limit':'20',
    'sort_by':'voteups',
    }
#封装参数
res=requests.get(url,headers=headers,params=params)
#发送请求,并把响应内容赋值到变量res里面
print(res.status_code)
#确认请求成功  
articles=res.json()
#用json()方法去解析response对象,并赋值到变量articles上面。
print(articles)
#打印这个json文件

发现没问题,接下来就是要根据json数据结构来提取我们想要的文章标题数据了。

我们在preview里面一层层看看这个json文件,到底是怎么一个结构。下面三张图依次是从外到内的数据展示。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

最外层是一个很大的字典,里面有两大元素,data:和paging:,这两大元素又是键值对应的字典形式,data这个键所对应的值是一个列表,里面有10元素,每个元素又是字典形式。

这样看其实还不够直观,为了更清晰地定位到目标数据,我们把这三张图合二为一:
在这里插入图片描述

最外层是个字典,在键为data的值里面,又有一个列表,而列表的每个元素又是一个字典,在第0个元素的值里面,又有一个字典,这里面终于有了我们要拿的title: “国产航母下水……让我想到李鸿章和北洋舰队”。

那就可以一层层地取数据了,我们又要接着来写代码了,(≧▽≦)/,我们只要先拿第一页的所有文章的标题即可。

import requests
#引入requests
headers={'user-agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'}
#封装headers
url='https://www.zhihu.com/api/v4/members/zhang-jia-wei/articles?'
#写入网址
params={
    'include':'data[*].comment_count,suggest_edit,is_normal,thumbnail_extra_info,thumbnail,can_comment,comment_permission,admin_closed_comment,content,voteup_count,created,updated,upvoted_followees,voting,review_info,is_labeled,label_info;data[*].author.badge[?(type=best_answerer)].topics',
    'offset':'10',
    'limit':'20',
    'sort_by':'voteups',
    }
#封装参数
res=requests.get(url,headers=headers,params=params)
#发送请求,并把响应内容赋值到变量res里面
print(res.status_code)
#确认这个response对象状态正确    
articles=res.json()
#用response.json()方法去解析response对象,并赋值到变量articles上面,此时的articles是一个
print(articles)
#打印这个json文件

以下,是老师的参考答案:

import requests
#引入requests
headers={'user-agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'}
#封装headers
url='https://www.zhihu.com/api/v4/members/zhang-jia-wei/articles?'
#写入网址
params={
    'include':'data[*].comment_count,suggest_edit,is_normal,thumbnail_extra_info,thumbnail,can_comment,comment_permission,admin_closed_comment,content,voteup_count,created,updated,upvoted_followees,voting,review_info,is_labeled,label_info;data[*].author.badge[?(type=best_answerer)].topics',
    'offset':'10',
    'limit':'20',
    'sort_by':'voteups',
    }
#封装参数
res=requests.get(url,headers=headers,params=params)
#发送请求,并把响应内容赋值到变量res里面
print(res.status_code)
#确认这个response对象状态正确    
articles=res.json()
#用json()方法去解析response对象,并赋值到变量articles上面,此时的articles是一个
print(articles)
#打印这个json文件
data=articles['data']
#取出键为data的值。
for i in data:
    print(i['title'])
    #遍历列表,拿到的是列表里的每一个元素,这些元素都是字典,再通过键把值取出来

通过同样的方法,我们也可以定位到文章链接和文章摘要的数据。

import requests
#引入requests
headers={'user-agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'}
#封装headers
url='https://www.zhihu.com/api/v4/members/zhang-jia-wei/articles?'
#写入网址
params={
    'include':'data[*].comment_count,suggest_edit,is_normal,thumbnail_extra_info,thumbnail,can_comment,comment_permission,admin_closed_comment,content,voteup_count,created,updated,upvoted_followees,voting,review_info,is_labeled,label_info;data[*].author.badge[?(type=best_answerer)].topics',
    'offset':'10',
    'limit':'20',
    'sort_by':'voteups',
    }
#封装参数
res=requests.get(url,headers=headers,params=params)
#发送请求,并把响应内容赋值到变量res里面
print(res.status_code)
#确认这个Response对象状态正确    
articles=res.json()
#用response.json()方法去解析数据,并赋值到变量articles上面,此时的articles是一个
print(articles)
#打印这个json文件
data=articles['data']
#取出键为data的值。
for i in data:
    print(i['title'])
    print(i['url'])
    print(i['excerpt'])
    #遍历列表,拿到的是列表里的每一个元素,这些元素都是字典,再通过键把值取出来

那一页的数据,我们就拿到了,接下来是要去拿所有页面的数据。

我们在上一步已经分析过了,第一页和第二页的请求的参数区别在于offset,由此可以写一个循环。

那么如何结束这个循环呢?

这里老师有一个经验之谈,我们来看看第一页和最后一页请求的参数区别:
在这里插入图片描述
在这里插入图片描述

对比一下,你会发第一页的is_end是显示false,最后一页的is_end是显示true,这个元素可以帮我们结束循环。

至于那个totals: 919元素,我算了一下页码和每页的文章数,判断这是文章的总数,也同样可以作为结束循环的条件。两个元素都可以用,在这里我们用is_end元素。

理论上,我们就可以拿到这些数据了。但是在这里,我们并不建议全部爬取46页数据。大家想想,参加课程的同学有很多,每个人都去爬这46页的话,如此高频访问会给对方的服务器造成相当大的压力,而我们并不真正需要这么多的数据,只是为了学习爬虫。

所以,我们的循环会设置为爬两页数据就停止。代码如下:

import requests
headers={'user-agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'}
url='https://www.zhihu.com/api/v4/members/zhang-jia-wei/articles?'
articlelist=[]
#建立一个空列表,以待写入数据
offset=0
#设置offset的起始值为0
while True:
    params={
        'include':'data[*].comment_count,suggest_edit,is_normal,thumbnail_extra_info,thumbnail,can_comment,comment_permission,admin_closed_comment,content,voteup_count,created,updated,upvoted_followees,voting,review_info,is_labeled,label_info;data[*].author.badge[?(type=best_answerer)].topics',
        'offset':str(offset),
        'limit':'20',
        'sort_by':'voteups',
        }
    #封装参数
    res=requests.get(url,headers=headers,params=params)
    #发送请求,并把响应内容赋值到变量res里面
    articles=res.json()
    # print(articles)
    data=articles['data']
    #定位数据
    for i in data:
        list1=[i['title'],i['url'],i['excerpt']]
        #把数据封装成列表
        articlelist.append(list1) 
    offset=offset+20
    #在while循环内部,offset的值每次增加20
    if offset>40:
        break
    #如果offset大于40,即爬了两页,就停止
    #if articles['paging']['is_end'] == True:
    #如果键is_end所对应的值是True,就结束while循环。
        #break
print(articlelist)
#打印看看

好啦,你读完这段代码就该你去写啦:

获取、解析、提取数据都完成了。好,接下来就是存储数据了。

在我的想象中,最终的数据最好存成下面这样简明清晰的表格:
在这里插入图片描述

csv和openpyxl都可以做到,但在这里我们会选取csv,因为上一关的案例是用的openpyxl,那我们今天就来试试csv。

上一关,我们学的csv最主要的功能就是可以把列表按行写入。

反推回来,也就是我们如果可以把数据写成list=[title,url,excerpt]的样子,那就可以直接写入啦。

刚刚我们爬取到的数据也是这样的一个列表,那么我们就知道如何写代码了。

import requests
import csv
#引用csv。
csv_file=open('articles.csv','w',newline='',encoding='utf-8')
#调用open()函数打开csv文件,传入参数:文件名“articles.csv”、写入模式“w”、newline=''。
writer = csv.writer(csv_file)
# 用csv.writer()函数创建一个writer对象。
list2=['标题','链接','摘要']
#创建一个列表
writer.writerow(list2)
#调用writer对象的writerow()方法,可以在csv文件里写入一行文字 “标题”和“链接”和"摘要"。

headers={'user-agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'}
url='https://www.zhihu.com/api/v4/members/zhang-jia-wei/articles?'
offset=0
#设置offset的起始值为0
while True:
    params={
        'include':'data[*].comment_count,suggest_edit,is_normal,thumbnail_extra_info,thumbnail,can_comment,comment_permission,admin_closed_comment,content,voteup_count,created,updated,upvoted_followees,voting,review_info,is_labeled,label_info;data[*].author.badge[?(type=best_answerer)].topics',
        'offset':str(offset),
        'limit':'20',
        'sort_by':'voteups',
        }
    #封装参数
    res=requests.get(url,headers=headers,params=params)
    #发送请求,并把响应内容赋值到变量res里面
    articles=res.json()
    print(articles)
    data=articles['data']
    #定位数据
    for i in data:
        list1=[i['title'],i['url'],i['excerpt']]
        #把目标数据封装成一个列表
        writer.writerow(list1)
        #调用writerow()方法,把列表list1的内容写入
    offset=offset+20
    #在while循环内部,offset的值每次增加20
    if offset > 40:
        break
csv_file.close()
#写入完成后,关闭文件就大功告成
print('okay')    

我们的目标:续写代码,请把获取到的两页的文章标题、链接、摘要这些数据,使用csv保存到本地。

csv写入文件的步骤如下:

import csv
csv_file=open('demo.csv','w',newline='')
writer = csv.writer(csv_file)
writer.writerow(['电影','豆瓣评分'])
csv_file.close()
import requests
import csv
#引用csv。
csv_file=open('articles.csv','w',newline='',encoding='utf-8')
#调用open()函数打开csv文件,传入参数:文件名“articles.csv”、写入模式“w”、newline=''。
writer = csv.writer(csv_file)
# 用csv.writer()函数创建一个writer对象。
list2=['标题','链接','摘要']
#创建一个列表
writer.writerow(list2)
#调用writer对象的writerow()方法,可以在csv文件里写入一行文字 “标题”和“链接”和"摘要"。

headers={'user-agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'}
url='https://www.zhihu.com/api/v4/members/zhang-jia-wei/articles?'
offset=0
#设置offset的起始值为0
while True:
    params={
        'include':'data[*].comment_count,suggest_edit,is_normal,thumbnail_extra_info,thumbnail,can_comment,comment_permission,admin_closed_comment,content,voteup_count,created,updated,upvoted_followees,voting,review_info,is_labeled,label_info;data[*].author.badge[?(type=best_answerer)].topics',
        'offset':str(offset),
        'limit':'20',
        'sort_by':'voteups',
        }
    #封装参数
    res=requests.get(url,headers=headers,params=params)
    #发送请求,并把响应内容赋值到变量res里面
    articles=res.json()
    print(articles)
    data=articles['data']
    #定位数据
    for i in data:
        list1=[i['title'],i['url'],i['excerpt']]
        #把目标数据封装成一个列表
        writer.writerow(list1)
        #调用writerow()方法,把列表list1的内容写入
    offset=offset+20
    #在while循环内部,offset的值每次增加20
    if offset > 40:
        break
csv_file.close()
#写入完成后,关闭文件就大功告成
print('okay')  

展望未来

来到这里,你的爬虫就已经入门了!!!

我能看到大家在这段时间里认真上课,努力写代码的样子,也能看到大家对代码抓耳挠腮,百思不得其解的样子,还能看到你每一个豁然开朗、代码跑通后开心的样子。

接下来的关卡,你会学习更厉害的爬虫技能,以面对更多更复杂的情况。

第8关是运用cookies让浏览器记住我们,第9关是用selenium控制浏览器,第10关是让爬虫程序能定时向我们汇报结果,第11-14关是运用协程和scrapy框架来帮我们提速,并且可以爬取海量的数据,第15关是复习。

你仍然会遇到很多的问题,但我知道,现在的你已经有更强的能力、和更大的信心去解决更难更多的问题了 !

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!