网络数据抓取-简书文章阅读量分析-requests案例

智能决策上手系列教程索引

以前在简书发了一些文章,涉及的分类特别杂乱,有TensorFlow的,有Web开发的,还有一些小学生编程教程和绘图设计教程...最近又在做人工智能通识专题和智能决策系列教程的文章。
这些天很多简友关注我,但我很迷茫,并不知道哪些文章最受大家重视,对大家更有用些,而简书也没有这方面的统计功能开放给作者们使用。
我就想能不能自己把这些变化数据抓取下来,自己分析一下,于是就开动写这个案例教程了。

这个教程推荐使用Chrome浏览器和Jupyter Notebook编辑器。Notebook的安装请参照安装Anaconda:包含Python编程工具Jupyter Notebook

有哪些数据可以获取?

从自己的文章列表页面可以看到总体【关注数】和每篇文章的【观看数】都是直接获取的,我们只要汇总每天哪些文章观看数增加了,再对比粉丝数的变化,就能知道哪些文章引发的关注最多

image.png

因为目测我的文章每天总阅读量的增加数,和每天粉丝的增加数相差不太大,也就是说,大部分阅读都引发了被关注,所以两者之间是强关联的。
如果不是这样,比如每天增加阅读1万,粉丝增加100,那就不好说了,因为可能A文章被观看100次都引发了被关注,而B文章被观看了9000次却没有引发一个被关注,那么就没办法从单个文章阅读量上分析粉丝变化,也就猜不出哪些文章更受喜欢。

爬虫数据是在html里还是在动态json请求里?

首先我们要知道页面上这些数据是怎么来的,是直接html标签显示的?还是通过JavaScript动态填充的?请参阅系列教程的前4节

我们的套路:

  1. 右键【显示网页源代码】,打开的就是浏览器地址栏里面的地址请求直接从服务器拉取到的html数据,如果这里可以Ctrl+F搜索到需要的数据(比如可以搜到“人工智能通识-AI发展简史-讲义全篇”),那么用最简单的html数据提取就可以。

  2. 如果上面一个办法搜不到,那就右键【检查元素】,然后查看Network面板里面type为xhr的请求,点击每一个,看哪个Response里面可以Ctrl+F搜到我们需要的数据。(很多时候可以从请求的英文名字里面猜个八九不离十)

在这个案例里,我们需要的数据看上去就在网页源代码里面,暂且是这样。


image.png

怎么用header和params模拟浏览器?

为了不让网站的服务器知道我们是爬虫,就还要像浏览器一样发送附加的额外信息,就是header和params。

我们右键【检查】,然后切换到【Network】网络面板,然后刷新网页,我们会看到一个和网页地址一致的请求。
如下图,我的主页地址是https://www.jianshu.com/u/ae784c57b353,就看到Network最顶上的是ae784c57b353:

image.png

如果我们切换到请求的Response响应结果面板,就可以看到这个请求获取的实际就是网页源代码。它的type类型是document,也就是html文档。

就是它了,我们需要它的header头信息和params参数。
【右击-Copy-Copy Request Headers】就能复制到这个请求的头信息了。


image.png

但是,如果你留意,就会发现这个请求的Response结果(也就是网页源代码)并不是包含所有文章,而只是只包含了9个文章。但是如果我们用鼠标往下滚动页面,发现文章就会越来越多的自动添加进来。(右侧的滚动条越变越短)

我们刷新页面重置,然后清空Network网络面板,一点点轻轻往下滚动,直到列表里出现了一个xhr请求:


image.png

点击这个请求,可以在Headers里看到它的Parameters参数,其实就是请求名称的问号后面的部分:


image.png

order_by是排序,page是第几页。所以不是简书文章列表不分页,默认加载的是page=1,而当你往下滚动的时候自动添加下一页的内容page=2,page=3...

我们查看它的Response也会发现,它所得到的内容和我们上面的网页源代码格式是一致的:


image.png

设定参数

打开Jupyter Notebook,在第一个cell单元编写代码,设定相关参数(headers字段涉及到个人隐私已经被我简化了,你须要在浏览器里面复制自己的):

url = 'https://www.jianshu.com/u/ae784c57b353'
params = {'order_by': 'shared_at', 'page': '1'}
headers = '''
GET /u/ae784c57b353 HTTP/1.1
Host: www.jianshu.com
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7
Cookie: read_mode=day; default_font=font2; locale=zh-CN;....5b9fb068=1538705012
If-None-Match: W/"31291dc679ccd08938f27b1811f8b263"
'''

但是这样的headers格式是个长串字符,我们需要把它改写成params那样的字典,手工改太麻烦也容易改错,我们再添加一个cell使用下面代码自动改写(不熟悉的话可以暂时不用理解它,以后随着学习深入就很快会看懂了):

def str2obj(s, s1=';', s2='='):
    li = s.split(s1)
    res = {}
    for kv in li:
        li2 = kv.split(s2)
        if len(li2) > 1:
            res[li2[0]] = li2[1]
    return res


headers = str2obj(headers, '\n', ': ')

发起Request请求

先用最简单的代码发送请求检查是否正确:

import requests
html = requests.get(url, params=params, headers=headers)
print(html.text)

正常的话应该输出和网页源代码差不多的内容。

获取标题数据

在页面一个文章上【右键-检查】打开Elements元素面板,我们来仔细看每个文章标准的一段:


image.png

从图上可以看到每个<li>标签对应一个文章,我们需要的三个内容(红色框):

  • 文章编号,/p/0fed5efab3e5,也是查看文章的链接地址,每文章唯一。
  • 文章的标题,人工智能通识-AI发展简史-讲义全篇
  • 文章的阅读量,272

因为数据都是在html标签里面,所以我们需要使用BeautifulSoup功能模块来把html变为容易使用的数据格式,先尝试抓到标题。把上面的代码改进一下:

import requests
from bs4 import BeautifulSoup

html = requests.get(url, params=params, headers=headers)
soup = BeautifulSoup(html.text, 'html.parser')
alist = soup.find_all('div', 'content')
for item in alist:
    title = item.find('a', 'title').string
    print(title)

全部运行输出结果如下有9个文章:

image.png

获取文章编号和阅读量

我们对上面的代码改进一下:

import requests
from bs4 import BeautifulSoup

html = requests.get(url, params=params, headers=headers)
soup = BeautifulSoup(html.text, 'html.parser')
alist = soup.find_all('div', 'content')
for item in alist:
    line = []
    titleTag = item.find('a', 'title')  #标题行a标记
    line.append(titleTag['href'])  #编号
    line.append(titleTag.string)  #标题    

    read = item.find('div', 'meta').find('a').contents[2]
    line.append(str(int(read)))  #编号,先转int去掉空格回车,再转str才能进line

    print(','.join(line))

在这里我们使用[href]的方法获取了<a class="wrap-img" href="/p/0fed5efab3e5" target="_blank">这个标记内的属性,这个方法同样适用于更多情况,比如[class]可以获得warp-img字段。

另外item.find('div', 'meta').find('a').contents[2]这里,我们利用了find只能找到内部第一个符合条件的标记的特点;contents[2]这是由于a标记内包含了多个内容,<i class="iconfont ic-list-read"></i> 20,试了几下,发觉[2]是我们想要的内容。

以上代码运行全部可以输出以下内容:


image.png

获取粉丝数和文章总数

我们把流程分成两步走:

  1. 获取文章总数和粉丝总数
  2. 根据文章总数循环获取每页的数据

把上面的cell内容都选中,按Ctrl+/都临时注释掉。然后在这个cell上面添加一个新的cell,用来读取文章总数acount和关总数afocus:

import requests
from bs4 import BeautifulSoup

html = requests.get(url, headers=headers)
soup = BeautifulSoup(html.text, 'html.parser')
afuns = soup.find('div', 'info').find_all('div','meta-block')[1].find('p').string
acount = soup.find('div', 'info').find_all('div','meta-block')[2].find('p').string
afuns=int(afuns)
acount=int(acount)
print('粉丝:',afuns,'文章', acount)

find只获取第一个符合条件的标记,find_all是获取所有符合条件的标记。
要对比着html源代码来看:

image.png

正常应该输出两个数字。

获取全部文章数据

选择刚才屏蔽掉的代码,再次按ctrl+/恢复可用。
然后修改成以下内容:

import math
import time

pages=math.ceil(acount/9)
data=[]
for n in range(1,pages+1):
    params['page']=str(n)
    html = requests.get(url, params=params, headers=headers)
    soup = BeautifulSoup(html.text, 'html.parser')
    alist = soup.find_all('div', 'content')
    for item in alist:
        line = []
        titleTag = item.find('a', 'title')  #标题行a标记
        line.append(titleTag['href'])  #编号
        line.append(titleTag.string)  #标题    

        read = item.find('div', 'meta').find('a').contents[2]
        line.append(str(int(read)))  #编号,先转int去掉空格回车,再转str才能进line

        data.append(','.join(line))
        print('已获取:',len(data))
    time.sleep(1)
        
print('\n'.join(data))

这里使用了math.ceil(acount/9)的方法获取总页数,ceil是遇小数就进1,比如ceil(8.1)是9,ceil(9.0)也是9,这样即使最后一页只有1个文章也不会被遗漏。

for n in range(1,12)这个for循环中,每一次n都被自动加1,获取第一页时候n是1,第二页时候n是2...所以params['page']=str(n)就可以自动变页。

print('已获取:',len(data))这行其实没有用,因为获取页面需要十几秒钟,如果中间不打印点什么会看上去像是无反应或死机。len(data)是指data这个列表的长度length。

time.sleep(1)每读取一页就停1秒,以免被服务器发觉我们是爬虫而封禁我们。

存储文章数据

我们上面使用逗号分开文章的序号、标题和阅读量,然后再加入data列表,data.append(','.join(line));同样,最后我们输出时候使用回车把data所有文章连在一起,print('\n'.join(data))

实际上,我们可以直接把它存储为excel可以读取的.csv文件。最下面新建一个cell添加以下代码:

with open('articles.csv', 'w', encoding="gb18030") as f:
    f.write('\n'.join(data))
    f.close()

这里注意,w是write写入模式,encoding="gb18030"是为了确保中文能正常显示。

运行后就能在你对应的Notebook文件夹内多出一个articles.csv文件,用excel打开就能看到类似下面的数据:


image.png

每天使用不同的文件名存储

我们每天爬取一下所有文章的数据,每天存为一个excel表,都叫做articles.csv肯定会重名。我们应该用不同的日期来命名就好很多,比如articles-201810061230.csv表示2018年10月6日12点30分统计的记录,这样看起来就清楚多了。

如何获取当前电脑的日期时间?计算机里面记录时间的最简单方法就是,只记录从某一年开始经过了多少毫秒,大多是从1970年开始算的。只要知道距离1970年1月1日0时0分0秒0毫,过了多少毫秒,那么这个时间就能计算出是哪年哪月哪日。

我们修改一下上面的代码:

tm = str(int(time.time()))
fname = 'articles_' + tm + '.csv'
with open(fname, 'w', encoding="gb18030") as f:
    f.write('\n'.join(data))
    f.close()

这样存储的就是类似articles_1538722108.csv文件名的文件了。

如果要变回年月日的显示,需要使用datatime功能模块,例如
import time,datetime
tm=int(time.time())
print(datetime.datetime.fromtimestamp(tm).strftime('%Y-%m-%d %H:%M:%S'))
这个会输出类似2018-10-05 14:47:11这样的结果

增量存储粉丝数

我们需要把每一天抓取到的关注数和文章数存储到一个excel表里,而不是分开的,我们新增一个cell,添加如下代码,实现每次运行就向total.csv文件增加一行数据:

from os.path import exists
alabels = ['time', 'funs', 'articles']
adata = [tm, afuns,  str(acount)] #acount是数字,需要转化
afname='./articles_total.csv'
if not exists(afname):
    with open(afname, 'a', encoding="gb18030") as f:
        f.write(','.join(alabels)+'\n')
        f.close()
with open(afname, 'a', encoding="gb18030") as f:
    f.write(','.join(adata)+'\n')
    f.close()

exists是存在的意思,如果文件存在,就正常往里添加新数据,如果不存在就先添加一个表头time,focus,articles

最后回顾

这个文章里我们做了下面几个练习:

  1. 根据问题思考需要哪些数据,能不能抓取到
  2. 分析页面,找到这些数据在哪里,是html文档里还是单独的请求
  3. 找到请求,复制headers,搞清楚params
  4. 发送请求,用beautifulsoup帮助找到需要的数据
  5. 根据文章总数,循环处理分页
  6. 把获取到的数据存储为excel可以识别的csv文件
  7. 利用时间自动创建不同的文件
  8. 文件的增量添加写入,每次添加一行数据

最终整理到一起的代码如下,添加了afuns、aword、alike等数据。
注意必须更换自己的headers才能使用:

#cell-1 设置参数

url = 'https://www.jianshu.com/u/ae784c57b353'
params = {'order_by': 'shared_at', 'page': '1'}
headers = '''
GET /u/ae784c57b353 HTTP/1.1
Host: www.jianshu.com
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7
Cookie: read_mode=day; default_font=font2; locale=zh-CN; ......b9fb068=1538705012
If-None-Match: W/"31291dc679ccd08938f27b1811f8b263"
'''

#cell-2 转化headers

def str2obj(s, s1=';', s2='='):
    li = s.split(s1)
    res = {}
    for kv in li:
        li2 = kv.split(s2)
        if len(li2) > 1:
            res[li2[0]] = li2[1]
    return res


headers = str2obj(headers, '\n', ': ')

# cell-3 发送整体请求,获取基本信息、文章总数

import requests
from bs4 import BeautifulSoup

html = requests.get(url, headers=headers)
soup = BeautifulSoup(html.text, 'html.parser')
afocus = soup.find('div', 'info').find_all('div','meta-block')[0].find('p').string
afuns = soup.find('div', 'info').find_all('div','meta-block')[1].find('p').string
acount = soup.find('div', 'info').find_all('div','meta-block')[2].find('p').string
awords = soup.find('div', 'info').find_all('div','meta-block')[3].find('p').string
alike = soup.find('div', 'info').find_all('div','meta-block')[4].find('p').string
acount=int(acount)
print('>>文章总数', acount)

#cell-4 循环获取每一页数据

import math
import time

aread = 0
pages = math.ceil(acount / 9)
data = []
for n in range(1, pages + 1):
    params['page'] = str(n)
    html = requests.get(url, params=params, headers=headers)
    soup = BeautifulSoup(html.text, 'html.parser')
    alist = soup.find_all('div', 'content')
    for item in alist:
        line = []
        titleTag = item.find('a', 'title')  #标题行a标记
        line.append(titleTag['href'])  #编号
        line.append(titleTag.string)  #标题

        read = item.find('div', 'meta').find('a').contents[2]
        aread += int(read)  #计算总阅读量
        line.append(str(int(read)))  #编号,先转int去掉空格回车,再转str才能进line

        data.append(','.join(line))
        print('已获取:', len(data))
    time.sleep(1)


#cell-5 存储文章数据新文件

tm = str(int(time.time()))
fname = './data/articles_' + tm + '.csv'
with open(fname, 'w', encoding="gb18030") as f:
    f.write('\n'.join(data))
    f.close()

#cell-6 增量存储基础信息

from os.path import exists
alabels = ['time', 'focus', 'funs', 'articles', 'words', 'like', 'read']
adata = [tm, afocus, afuns, str(acount), awords, alike, str(aread)]
afname='./articles_total.csv'
if not exists(afname):
    with open(afname, 'a', encoding="gb18030") as f:
        f.write(','.join(alabels)+'\n')
        f.close()
with open(afname, 'a', encoding="gb18030") as f:
    f.write(','.join(adata)+'\n')
    f.close()

#cll-7 提示完成

print('>>完成,保存在%s'%fname)

过几天收集到一些数据之后再分享数据分析相关的内容,请留意我的文章更新~


智能决策上手系列教程索引

每个人的智能决策新时代

如果您发现文章错误,请不吝留言指正;
如果您觉得有用,请点喜欢;
如果您觉得很有用,欢迎转载~


END

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • iOS网络架构讨论梳理整理中。。。 其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单...
    yhtang阅读 5,165评论 1 23
  • 前端开发者丨http请求 https:www.rokub.com 前言见解有限, 如有描述不当之处, 请帮忙指出,...
    麋鹿_720a阅读 10,889评论 11 31
  • 白衣战士克病魔,力尽千辛救重患。 救治不成医闹狂,血刃医护年年见。 敬畏生命勿伤医,糟蹋白衣鬼神怒。 自古白衣贵天...
    徐一村阅读 165评论 2 2
  • 1. 听说公公身体又不适,上吐下泄,外加吊水。起初,以为是老人家上了年纪,心里正有点过意不去。谁能料想,这老头生病...
    独处的旅途阅读 170评论 0 0