爬取了中国大学mooc平台7万多用户学习情况发现

一直都有一个想法想看看疫情期间大家在网课平台学习情况,刚好这次python结课作业是写一个网络爬虫,所以就对我用的比较多平台mooc中国大学下手了,在此感谢mooc平台嵩天老师python系列课程很好基础课,老师讲的很细,学完还是有点收获。

study.png


1.首先分享下爬取数据结果,后面再梳理下我的思路,第一次写python爬虫,有不对地方还希望指出,大家一起共同进步.
(以下数据为mooc平台中5月16日抓取7万多用户学习情况,但是由于爬取数据不到总的数据10%且只爬取一次,可能不具有权威性,大家看看就好,有兴趣可以自己爬爬看,文章后面会附上最初代码。

  1. 1mooc平台最近学习课程关键词(词云展示)
    last.png

从关键词可以看出mooc平台用户最近学习编程人还是挺多的,特别是Python,突然感到我这个就业竞争压力还是很大的.除此之外学习工科学生要多于文科的外,学习英语,数学人也不少。让我感到奇怪是心理学怎么那么多,我猜可能很多学校选修网课都选了这个课吧。

  1. 2mooc平台所有课程关键词(词云展示)
    study2.png

从所有选课关键词中可以看出,公共必修课还算占了很大份额,其他和最近所学课程差不多,都是工科偏多,计算机专业还是很突出,这个专业有这么热门么?

  1. 3学习时长排行榜(仅以我抓取7万数据处理,存在偶然性,单位:min)
    paihang.png

虽然我只爬取7万多数据,但是由于是分段爬取的,应该还是能说明一些问题,越靠前学习时长跨度越大,越靠后大家数据跟着越紧。
第一名同学居然学习998门课程,9783小时学习时间,相当于没日没夜学407天,真的强附上大佬空间图片:
链接直达

web.png

4470分钟的我连第一名零头都凑不到.关键我还在排行榜发现我们班上一位低调大佬,第29名那名同学居然是我们班上的,我是不是该反思下自己。那位同学是大二时候其他专业转过来的,原来他一直在偷偷充电,怪不得后来进步那么快。

  1. 4学习时长分段图
    学习时长分段图(min).png

这里我只提取30分钟以上数据,通过观察可以看出mooc平台学习时长少于5小时账号还是挺多的,占了一半多,有32.6%在平均值以上
但是大佬们也不少,在>2000分钟里面其实区别还是很大的,从排行榜可以看出,通过怎样合理取区间是我数学上失误
由于这些是我自己计算统计的,而且获取用户数据有限,可能存在一定问题,大家看看就好,不必太较真。


2.以下就是代码部分思考了:
在这篇博客写出来之前,我向网易提交mooc能够轻易获取到用户学习情况,得到该信息是业务上可公开的,没风险,第一天给我回复是已经确认是漏洞,第二天又说是误报,误报就误报吧,我也可以安心写我的博客了!
网易.png

写代码前我还是看了mooc平台robots协议
但是它的robots协议写的实在不伦不类,既没有指出禁止运行,就留一个站点,
robots.png

看到站点我还是不放心去检索下:
站点.png

确认爬取站点不在内我才安心!


2.1 爬取前分析:
在我还未学习python这门课之前我就发现平台一个问题,当我的cookis失效情况下(相当于没登录情况下)我打开自己个人空间,看到自己信息,我尝试手动修改Id也可以访问别人空间,我就猜测这个页面不需要验证就可以访问。
myzone.png

后面准备写爬虫时候我却发现也没那么简单,当我关闭js加载后:

js.png

我去我需要数据全不见了

那这个页面部分数据就是动态加载生成的,遇到这个问题我知道就两种方法,一是用selenium模拟浏览器抓取,另外一条便是抓包分析直接抓接口.前者抓取速度慢,但是操作简单,后者找接口麻烦一点,但是后期抓取快直接返回想要数据.而mooc平台的接口经过我抓包我找到了,我便是用的第二种方法。这里抓包过程就不累述了,直接上接口链接,全部以我的信息为例:

https://www.icourse163.org/web/j/memberBean.getMocMemberPersonalDtoById.rpc #返回用户头像,昵称等信息

{'code': 0, 'result': {'memberId': 1032127354, 'schoolName': '其他', 'schoolId': None, 'nickName': '游尚忘烦', 'largeFaceUrl': 'https://img-ph-mirror.nosdn.127.net/y-3-D05OabXYiLo3y6G00w==/6632377284050677150.jpg', 'department': '其他', 'memberType': 1, 'highestDegree': 5, 'jobName': None, 'description': None, 'richDescription': None, 'lectorTitle': None, 'realName': None, 'isTeacher': False, 'followCount': 3, 'followedCount': 0, 'schoolShortName': None, 'logoForCertUrl': None, 'supportMooc': None, 'supportSpoc': None, 'followStatus': False, 'supportCommonMooc': None, 'supportPostgradexam': None, 'lectorTag': None, 'relType': None}, 'message': '', 'traceId': '', 'sampled': False}

https://www.icourse163.org/web/j/MocActivityScholarshipRpcBean.getActivityStatisticsByUser.rpc #返回平台参与度信息,包含我需要学习时长

{'code': 0, 'result': {'isSign': None, 'statistics': {'signCount': 0, 'courseCount': 0, 'postCount': 1, 'voteCount': 0, 'replyCount': 7, 'commentCount': 0, 'learnLongTimeCount': 4470}}, 'message': '', 'traceId': '', 'sampled': False}

https://www.icourse163.org/web/j/learnerCourseRpcBean.getOtherLearnedCoursePagination.rpc #返回所学习课程信息,最近学习靠前(这个我的返回太多了,找了个短做例子)

{'code': 0, 'result': {'query': {'sortCriterial': None, 'DEFAULT_PAGE_SIZE': 10, 'DEFAULT_PAGE_INDEX': 1, 'DEFAULT_TOTLE_PAGE_COUNT': 1, 'DEFAULT_TOTLE_COUNT': 0, 'DEFAULT_OFFSET': 0, 'pageSize': 32, 'pageIndex': 1, 'totlePageCount': 1, 'totleCount': 1, 'offset': 0, 'limit': 32}, 'list': [{'uid': 1032127356, 'courseCoverUrl': 'http://edu-image.nosdn.127.net/65B575B91765DBCD593C825AB376F329.jpeg?imageView&thumbnail=426y240&quality=100', 'courseId': 53004, 'courseProductType': 1, 'courseName': '高等数学(一)', 'enrollCount': 109782, 'schoolName': '同济大学', 'schoolId': 8013, 'schoolShortName': 'TONGJI', 'whatCertGot': None, 'termId': 50003, 'supportCommonMooc': None, 'mode': 0, 'termPrice': None, 'termOriginalPrice': None}]}, 'message': '', 'traceId': '', 'sampled': False}

经过仔细观察找到接口,接口传参是以post形式提交请求返回参数的,提交过程中params中的csrfKey和浏览器cookie中NTESSTUDYSI以及请求地址末尾参数做校验,所以只要csrfkey中数值和NTESSTUDYSI一样就行,随便你填什么,只要是字符串。最让我惊喜的是返回数据是json格式的,bs4解析都不用了。
所以数据提取变得很简单了。这里以提取学习时长为例:
首先将json转dict类型,然后利用键逐级取值

nickName=json_data['result']['nickName']#昵称
largeFaceUrl=json_data['result']['largeFaceUrl']#头像

或者利用jsonpath路径直接提取也很不错(这里如何使用jsonpath,上一篇博客有笔记例子)

#nickName=jsonpath(json_data,"$..nickName")#昵称
#largeFaceUrl=jsonpath(json_data,"$..largeFaceUrl")#头像

2.2可以提取数据后便可以写代码批量爬取了。

未命名文件.png

经过前面大量分析我发现其实我要写的代码量好像并不是很多.用户id问题,经过我发现是个10位数而且新用户数值大老用户数值小,通过高位逐个递减到返回数据为空发现最小id为:1015002001
由于平台一直在注册后面新用户数据统计没意义,我便取我新注册用户id为
最大值1437038940
这里我写了4个函数:
def nickName(Id):#获取用户昵称
def learnLongTimeCount(Id):#获取学习时长
def courseName(Id):#获取用户所选课程
def insert_mysql(data):#将数据存入MySQL
由于准备在服务器中跑很长时间,后期加入异常处理,以及无用数据判断剔除
但基本流程图在上方画出.

有个这个思路便写出下面最原始代码:

import requests
import json
#全局公用信息
csrfKey='None'#随便什么都行但必须有
params = (
        ('csrfKey', csrfKey),
    )
headers = {
        
        'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36',
        'cookie': 'NTESSTUDYSI=None',
    }

#获取昵称,图像
def nickName(Id):
    memberId=Id
    data = {
    'memberId': memberId
    }
    url='https://www.icourse163.org/web/j/memberBean.getMocMemberPersonalDtoById.rpc'
    response = requests.post(url, headers=headers, params=params, data=data)
    datas=response.text#获得json信息
    json_data=json.loads(datas)#将返回json信息转化为字典类型
    nickName=json_data['result']['nickName']#昵称
    largeFaceUrl=json_data['result']['largeFaceUrl']#头像
    return nickName,largeFaceUrl
#获取学习时长
def learnLongTimeCount(Id):
    memberId=Id
    data = {
    'userId': memberId
    }
    url='https://www.icourse163.org/web/j/MocActivityScholarshipRpcBean.getActivityStatisticsByUser.rpc?csrfKey='+csrfKey
    response = requests.post(url, headers=headers, params=params, data=data)
    datas=response.text
    json_data=json.loads(datas)
    learnLongTimeCount=json_data['result']['statistics']['learnLongTimeCount']
    return learnLongTimeCount
#获取所选课程返回一个字符串列表
def courseName(Id):
    memberId=Id
    url='https://www.icourse163.org/web/j/learnerCourseRpcBean.getOtherLearnedCoursePagination.rpc'
    data = {
    'uid': memberId,
    'pageIndex': '1',
    'pageSize': '32'
    }
    response = requests.post(url, headers=headers, params=params, data=data)
    datas=response.text
    json_data=json.loads(datas)
    n=len(json_data['result']['list'])
    courseName=[]
    if(n==0):
        return'没选课'
    else:
        for i in range (n):
            courseName+=[(json_data['result']['list'][i]['courseName'])]
        return json_data
def main():
    #ID最小值为1017120000
    #下面为任意展示
    Id=1032127355
    for i in range(Id,1032127999):
            print("用户id:{}\n|昵称+头像|:\n|{}|→|学习时长{}分钟|\n所选课程↓\n{}".format(Id,nickName(Id),learnLongTimeCount(Id), courseName(Id)))
            print("=" * 100)
            Id=Id+1
main()  

写完这个代码后当我准备存数据库时候还是遇到一些问题。
1.数据量太大
经过我简单计算我发现单凭这一个python程序肯定爬不完,而且也没必要爬那么多,于是我把id分成10段爬取,
不会写多线程抢占数 据库输入问题,我便写了10个python程序把输入抢占处理交给操作系统
2.用户多课程输入问题
本来我是准备把爬取下来数据全部存一个表的,但是用户课程数量不确定,我的表头确是确定的,最后我用了两张表,
一张记录昵称,学习时长,最近学的一门课程,另外一张表专门记录所有课程
为了使平台影响最小我选择晚上爬取,此时访问人数少最不易造成网络拥堵.

为了能在服务器上跑,我做了简单异常处理和数据筛选功能,排除选课为None和学习时长为0用户,处理写的很简陋,存在很大优化地方当时为了赶时间爬数据交作业很多地方没有深究,现在看看代码真的有点烂,用很多可以改进地方。
服务器爬取版本:

import requests
import json
from jsonpath import jsonpath
import pymysql
#全局公用信息
csrfKey='text'#随便什么都行,但必须有
params = (
        ('csrfKey', csrfKey),
    )
headers = {
        
        'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36',
        'cookie': 'NTESSTUDYSI=text',
    }


conn = pymysql.connect(host='localhost', user='root', password='xxxxxxx',
                       database='m163', port=3306, charset='utf8')
# 创建游标
cursor = conn.cursor()
def insert_mysql(data):
    
    
        cursor.execute(
            'insert into information(id,nickname,time,latelystudy) VALUES("%s","%s","%s","%s")' % (
data[0],data[1],data[2],data[3]))
        # 从游标中获取结果
        cursor.fetchall()

        # 提交结果
        conn.commit()

#获取昵称
def nickName(Id):
    memberId=Id
    data = {
    'memberId': memberId
    }
    url='https://www.icourse163.org/web/j/memberBean.getMocMemberPersonalDtoById.rpc'
    response = requests.post(url, headers=headers, params=params, data=data)
    datas=response.text
    json_data=json.loads(datas)
    nickName=jsonpath(json_data,"$..nickName")
    return nickName
#获取学习时长
def learnLongTimeCount(Id):
    memberId=Id
    data = {
    'userId': memberId
    }
    url='https://www.icourse163.org/web/j/MocActivityScholarshipRpcBean.getActivityStatisticsByUser.rpc?csrfKey='+csrfKey
    response = requests.post(url, headers=headers, params=params, data=data)
    datas=response.text
    json_data=json.loads(datas)
    learnLongTimeCount=jsonpath(json_data,"$..learnLongTimeCount")
    if(learnLongTimeCount[0]==0):#学习时长为0,直接返回数字0异常爬取下一个
        return 0
    return learnLongTimeCount

#获取课程
def courseName(Id):
    memberId=Id
    url='https://www.icourse163.org/web/j/learnerCourseRpcBean.getOtherLearnedCoursePagination.rpc'
    data = {
    'uid': memberId,
    'pageIndex': '1',
    'pageSize': '32'
    }
    response = requests.post(url, headers=headers, params=params, data=data)
    datas=response.text
    json_data=json.loads(datas)
    n=jsonpath(json_data,"$..totleCount")
    if(n[0]==0):#没有课程,返回数字0异常,爬取下一个
        return 0
    courseName=jsonpath(json_data,"$..courseName")
    return [courseName[0]]
def main():
    Id=1032127300
    while(Id<1032127400):
        try:
            #得到一个字符串列表用于存储信息
            information=[str(Id)]+nickName(Id)+learnLongTimeCount(Id)+courseName(Id)
            #print(information)
            insert_mysql(information)
            Id=Id+1
        except:
            Id=Id+1


    # 关闭游标
    cursor.close()

    # 关闭数据库
    conn.close()
main()  

虽然爬取效率不算高,但是稳定性还算不错,爬取差不多12多个小时没有终止异常退出,爬取7万多条非空数据

mysql.png

我的词云是在用户所选课程经过jieba库词频分析在wordcloud输出

关于如何生成一个漂亮词云,大佬有详细介绍我也是参考大佬代码就不写了

玩转wordcloud


代码能够优化地方还有很多,如引入多进程,使用多个代理,优化存储,数据筛选闸值,数据可视化。有更好方法也欢迎交流学习,代码上有疑问也欢迎探讨,第一次写python爬虫时间也有点赶,有不对地方也欢迎指出共同进步


最后分享几个小技巧:
如何让python程序一直在后台运行(linux):

nohup python xxx.py &

终止运行python进程(linux)
先查询出后台运行进程号:ps -ef | grep python
最后kill xxxx

在当前目录打开cmd(win)
直接在目录内上方文件框打入cmd回车即可

    

添加新评论

  Timeline

我们来自五湖四海,转眼就要各奔东西。
--- updated on 2020年12月1日

  关于博主

计科学生一枚,现在变社畜了,依旧热爱分享,有趣想法也会尝试用代码实现;
建这个博客初衷在于记一些自己笔记和想法,方便自己查阅;
本博客内核采用 Typecho开源代码,平时也可能分享一些开源资源,若侵犯您版权,请联系我删除。

  近期评论

  • 暂无评论

很多东西宁缺毋滥,流星的光芒短暂而灼热闪耀。

让你变得更好的那个人,往往是你觉得很难与之相处的那个人。

觉得自己做的到和不做的到,其实只在一念之间。

路在自己脚下,没有人可以决定我的方向。

你的选择是做或不做,但不做就永远不会有机会。

凡事顺其自然,遇事处于泰然,得意之时淡然,失意之时坦然,艰辛曲折必然,历尽沧桑悟然。