基于Webmagic的爬取B站用户数据的爬虫

github: https://github.com/Al-assad/Spider-bilibiliUser-active

数据示例样本:http://pan.baidu.com/s/1dFchDZj  验证码:b2fi

学校数据挖掘作业要弄一个聚类分析,我就想不如到我大B站搞搞事情吧,于是开始研究B站用户数据的获取接口;

B站现在大概有1亿左右的有效用户,由于时间的限制,爬取全部的用户对于作业的期限是肯定来不及,于是我选择爬取活跃用户(高关注数和高被关注数用户),B站的用户主页如下:

爬虫设计思路

打开F12调试器后,发现该页面是一个前端渲染页面,通过css或xpath无法获取到动态渲染的节点的数据,于是选择通过JsonPath获取json文件的数据,使用的时FrieFox自带的调试工具该页面的是主要用户数据接口为: http://space.bilibili.com/ajax/member/GetInfo,发送方式为POST,参数为  mid(用户ID);

这里推荐一个很不错的Web调试工具:Fiddler 4;

结果我写得的前嗅程序发现,该接口的反爬虫机制为对IP的接入频率,上限大概为 150~200 次/分钟,解决方式大概有2种:

1.  减低爬虫爬取频率;

2.  使用IP代理池,更具请求频率调整更换IP频率;

出于公共道德考虑,我选择了第一种方式(其实是因为我之前爬取的公共IP池里面大多数IP已经失效,懒得再爬取,况且这也只是一个作业而已就随便应付了);

接下来是解决请求跳转,通过调试工具的抓包,发现用户的关注对象、被关注对象的数据接口为:http://space.bilibili.com/ajax/friend/GetAttentionList?mid=633003&page=1, http://space.bilibili.com/ajax/friend/GetFansList?mid=633003&page=1,请求方式为GET,参数mid为用户ID,page为页数;

在这两个数据接口B站的反爬机制为限制页面访问,客户端最多只能访问前5个page,也就是说page取值为1~5,每page返回的json有20个用户数据对象,也就是一个用户最多只能跳转200个用户(估计B站被爬怕了),这我没想到其他的解决方法,只能暂时这样;

解决了爬取逻辑后,接下来解决数据持久化的问题,由于之后要使用rapidminer进行数据挖掘,于是使用MySQL储存数据,也方便之后对数据进行筛选,ADO层引擎使用JDBC;

爬虫引擎选用黄亿华前辈写的 webmagic  ,Webmagic是一个小巧强大、支持多线程,可定制的垂直爬虫引擎,这里给大家安利一个;

部分代码示例

首先我构造一个BiliUser类,方便在内存中储存用户的数据;

接下来是对Webmagic核心类PageProcessor的继承,PageProcessor负责描述对Url的抽取逻辑和跳转逻辑,部分代码如下:

public class BiliPageProcessor implements PageProcessor{//构建Site对象,指定请求头键值字段private Site site = Site.me().setRetryTimes(3).setTimeOut(30000).setSleepTime(1800)        //跟据试验,http://space.bilibili.com/ajax/member/GetInfo接口有IP接入限制,估计是60s内上限150次.setCycleRetryTimes(3).setUseGzip(true).addHeader("Host","space.bilibili.com").addHeader("User-Agent","Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0").addHeader("Accept","application/json, text/plain, */*").addHeader("Accept-Language","zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3").addHeader("Accept-Encoding","gzip, deflate, br").addHeader("X-Requested-With","XMLHttpRequest").addHeader("Content-Type","application/x-www-form-urlencoded").addHeader("Referer","http://space.bilibili.com/10513807/");private static final long BEGIN_MID = 2705;    //开始用户midprivate static final int LIMIT_REQUEST = 5;    //目前b站的用户关注和粉丝请求接口,对page的访问限制数为5private BiliUserDao biliUserDao = new BiliUserDao();   //持久化对象
//    private final String TARGET_URL;   //用户信息主页请求接口 postprivate final String FRIENDS_URL = "http://space.bilibili\\.com/ajax/friend/GetAttentionList\\?mid=\\d+&page=\\d+"; //用户关注信息请求接口 getprivate final String FANS_URL = "http://space.bilibili\\.com/ajax/friend/GetFansList\\?mid=\\d+&page=\\d+";   //用户粉丝信息请求接口 get@Overridepublic void process(Page page) {if(page.getUrl().regex(FRIENDS_URL).match() || page.getUrl().regex(FANS_URL).match()){/*请求url匹配 friends 和 fans 请求接口时,获取 get请求返回json中的mid数据,并添加用户主页请求到url处理队列*/List<String> mids = new JsonPathSelector("$.data.list[*].fid").selectList(page.getRawText());if (CollectionUtils.isNotEmpty(mids)) {for (String mid : mids) {//构造用户信息主页的post请求Request request = createPostRequest(mid);//添加Request对象到URL请求队列page.addTargetRequest(request);}}}else{/*请求url为用户主页请求url时,通过 post请求返回的json中的目标节点数据,并装载入数据库*/String pageRawText = page.getRawText();//跳过连接失败页if(new JsonPathSelector("$.status").select(pageRawText).equals("false"))page.setSkip(true);//使用jsonPath获取json中的有效数据,并装载入BiliUser对象BiliUser user = new BiliUser();long mid = Long.parseLong(new JsonPathSelector("$.data.mid").select(pageRawText));user.setMid(mid);user.setName(new JsonPathSelector("$.data.name").select(pageRawText));user.setSex(new JsonPathSelector("$.data.sex").select(pageRawText));user.setLevel(Integer.parseInt(new JsonPathSelector("$.data.level_info.current_level").select(pageRawText)));user.setSign(new JsonPathSelector("$.data.sign").select(pageRawText));user.setFaceUrl( new JsonPathSelector("$.data.face").select(pageRawText));int friends = Integer.parseInt(new JsonPathSelector("$.data.friend").select(pageRawText));user.setFriends(friends);int fans = Integer.parseInt(new JsonPathSelector("$.data.fans").select(pageRawText));user.setFans(fans);user.setPlayNum(Integer.parseInt(new JsonPathSelector("$.data.playNum").select(pageRawText)));user.setBirthday(new JsonPathSelector("$.data.birthday").select(pageRawText));user.setPlace(new JsonPathSelector("$.data.place").select(pageRawText));//添加friends列表请求for(int i=1;i<=((friends/20)>LIMIT_REQUEST ? LIMIT_REQUEST : friends/20);i++){page.addTargetRequest("http://space.bilibili.com/ajax/friend/GetAttentionList?mid="+mid+"&page="+i);}//添加fans列表请求for(int i=1;i<=((fans/20)>LIMIT_REQUEST ? LIMIT_REQUEST : fans/20);i++){page.addTargetRequest("http://space.bilibili.com/ajax/friend/GetFansList?mid="+mid+"&page="+i);}System.out.println("\n"+user);   //控制台打印已抓取的用户信息biliUserDao.saveUser(user);    //保存BiliUser对象到数据库}}@Overridepublic Site getSite() {return site;}//创建面向用户主页POST请求(http://space.bilibili.com/ajax/member/GetInfo)的Request对象private static Request createPostRequest(String mid){//构造post请求数据组和urlMap<String, Object> nameValuePair = new HashMap<String, Object>();NameValuePair[] values = new NameValuePair[1];values[0] = new BasicNameValuePair("mid", String.valueOf(mid));nameValuePair.put("nameValuePair", values);String url = "http://space.bilibili.com/ajax/member/GetInfo?mid="+mid;   //bilibili用户信息获取接口//构造Request请求对象Request request = new Request(url);request.setExtras(nameValuePair);request.setMethod(HttpConstant.Method.POST);return request;}//运行主方法public static void main(String[] args){Spider.create(new BiliPageProcessor()).addRequest(createPostRequest(BEGIN_MID+""))    //添加一次对BEGIN_MID主页的POST请求.addUrl("http://space.bilibili.com/ajax/friend/GetFansList?mid="+BEGIN_MID+"&page=1").addUrl("http://space.bilibili.com/ajax/friend/GetAttentionList?mid="+BEGIN_MID+"&page=1").setDownloader(new MyDownloader()).thread(2).run();}
}

这里我使用了自己定制的Downloader对象,对HttpClicent的页面下载逻辑进行部分的改写,主要是对于请求频率的控制,为了使得请求频率尽量逼近数据接口的IP限制访问频率,在超过上限的时候请求会得到返回403拒绝服务响应,此时该请求线程挂起30s,再重新发送POST请求;

ADO层的核心类如下,为了提高数据传输效率,我使用预定义模板传输SQL指令(然而整个程序运行的瓶颈其实是被限制的请求频率,就当是良好的代码习惯吧):

public class BiliUserAdo {public int saveUser(BiliUser biliUser){DBHelper dbHelper = new DBHelper();StringBuffer sql = new StringBuffer();//构造模板sql ,当插入主键重复时,忽略新数据sql.append("INSERT IGNORE INTO bilibili_user_active(mid,name,sex,level,sign,faceUrl,friends,fans,playNum,birthday,place)").append("VALUES(?,?,?,?,?,?,?,?,?,?,?)");//构造模板填充序列List<String> sqlValues = new ArrayList<String>();sqlValues.add(biliUser.getMid()+"");sqlValues.add(biliUser.getName());sqlValues.add(biliUser.getSex());sqlValues.add(biliUser.getLevel()+"");sqlValues.add(biliUser.getSign());sqlValues.add(biliUser.getFaceUrl());sqlValues.add(biliUser.getFriends()+"");sqlValues.add(biliUser.getFans()+"");sqlValues.add(biliUser.getPlayNum()+"");sqlValues.add(biliUser.getBirthday());sqlValues.add(biliUser.getPlace());int result = dbHelper.executeUpdate(sql.toString(),sqlValues);return result;}}

这里我构造了一个ADO层辅助类DBHelper,进一步减少代码耦合,方便这整个ADO层的重用;

public class DBHelper {public static final String driver_class = "com.mysql.jdbc.Driver";public static final String driver_url = "jdbc:mysql://localhost/spider_bilibili?useunicode=true&characterEncoding=utf8";public static final String user = "root";public static final String password = "root";private static Connection conn ;private PreparedStatement pst;private ResultSet rst;public DBHelper(){conn = DBHelper.getConnInstance();}/*** 创建数据库连接* 使用单例模式创建Connection对象,同时保持线程同步*/private static synchronized Connection getConnInstance(){if(conn == null){try {Class.forName(driver_class);conn = DriverManager.getConnection(driver_url,user,password);} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException e) {e.printStackTrace();}System.out.println("Connection successful.");}return conn;}/*** close 断开数据库连接* */public void close(){try{if(conn != null)DBHelper.conn.close();if(pst != null)this.pst.close();if(rst != null)this.rst.close();}catch(SQLException e){e.printStackTrace();}}/*** query sql语句运行,使用预定义模板进行* @return ResultSet*/public ResultSet executeQuery(String sql, List<String> sqlValues) {try {pst = conn.prepareStatement(sql);if (sqlValues != null && sqlValues.size() > 0) {setSqlValues(pst, sqlValues);}rst = pst.executeQuery();} catch (SQLException e) {e.printStackTrace();}return rst;}/*** update sql语句运行,使用预定义模板进行* @return 执行结果状态码* */public int executeUpdate(String sqlTemplate,List<String> sqlValues){int resultCode = -1;try{pst = conn.prepareStatement(sqlTemplate);if(sqlValues != null && sqlValues.size()>0){setSqlValues(pst,sqlValues);}resultCode = pst.executeUpdate();}catch(SQLException e){e.printStackTrace();}return resultCode;}/*** 向预定义模板填充键值对* */private void setSqlValues(PreparedStatement pst,List<String> sqlValues){for(int i=0;i<sqlValues.size();i++){try {pst.setObject(i+1,sqlValues.get(i));} catch (SQLException e) {e.printStackTrace();}}}}

整个项目的代码我放到Github中: https://github.com/Al-assad/Spider-bilibiliUser-active,如果觉得有帮助的话不妨给个Star

后续拓展

以上的代码基于深度爬取的爬虫,其实不能保证爬取到所有用户的数据,针对爬取B站的所有用户数据,其实也很简单,B站的用户id排序是很规律的,从1~10500000(大概),增长步幅为1,大概是后台数据库储存MID字段使用了Auto_Increment设置,当然这其中有大概1/5的mid是空的,我觉得可能数据库的同步回滚问题造成的(据我所知B站用户客户端并没有注销用户的功能);

于是爬取所有用户的url请求逻辑就很明了了,对数据接口 http://space.bilibili.com/ajax/member/GetInfo,参数mid由0开始,不断自增后发送请求,返回404响应就跳过该请求,到达预计上限假设10500000后,连续一定步增mid的请求(如mid自增次数100)返回404响应就判断运行结束,这样就可以遍历到B站几乎所有的用户数据;

简单实现的PageProcessor如下:

public class BiliPageProcessor implements PageProcessor{//构建Site对象,指定请求头键值字段private Site site = Site.me().setRetryTimes(3).setTimeOut(30000).setSleepTime(1500)     //跟据试验,http://space.bilibili.com/ajax/member/GetInfo接口有IP接入限制,估计是60s内上限150次.setCycleRetryTimes(3).setUseGzip(true).addHeader("Host","space.bilibili.com").addHeader("User-Agent","Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0").addHeader("Accept","application/json, text/plain, */*").addHeader("Accept-Language","zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3").addHeader("Accept-Encoding","gzip, deflate, br").addHeader("X-Requested-With","XMLHttpRequest").addHeader("Content-Type","application/x-www-form-urlencoded").addHeader("Referer","http://space.bilibili.com/10513807/");private static final long BEGIN_MID = 1;    //开始用户midprivate static final long END_MID = 100300000;      //结束用户mid,(2017-04的估计注册用户数)private BiliUserAdo biliUserDao = new BiliUserAdo();   //持久化对象@Overridepublic void process(Page page) {String pageRawText = page.getRawText();//跳过连接失败页if(new JsonPathSelector("$.status").select(pageRawText).equals("false"))page.setSkip(true);//使用jsonPath获取json中的有效数据,并装载入BiliUser对象BiliUser user = new BiliUser();user.setMid(Long.parseLong(new JsonPathSelector("$.data.mid").select(pageRawText)));user.setName(new JsonPathSelector("$.data.name").select(pageRawText));user.setSex(new JsonPathSelector("$.data.sex").select(pageRawText));user.setLevel(Integer.parseInt(new JsonPathSelector("$.data.level_info.current_level").select(pageRawText)));user.setSign(new JsonPathSelector("$.data.sign").select(pageRawText));user.setFaceUrl( new JsonPathSelector("$.data.face").select(pageRawText));user.setFriends(Integer.parseInt(new JsonPathSelector("$.data.friend").select(pageRawText)));user.setFans(Integer.parseInt(new JsonPathSelector("$.data.fans").select(pageRawText)));user.setPlayNum(Integer.parseInt(new JsonPathSelector("$.data.playNum").select(pageRawText)));user.setBirthday(new JsonPathSelector("$.data.birthday").select(pageRawText));user.setPlace(new JsonPathSelector("$.data.place").select(pageRawText));System.out.println("\n"+user);biliUserDao.saveUser(user);    //保存BiliUser对象到数据库}@Overridepublic Site getSite() {return site;}//运行主方法public static void main(String[] args){Spider spider = Spider.create(new BiliPageProcessor());//添加请求对象序列long mid;for(mid = BEGIN_MID; mid < END_MID; mid++){//构造post请求数据组和urlMap<String, Object> nameValuePair = new HashMap<String, Object>();NameValuePair[] values = new NameValuePair[1];values[0] = new BasicNameValuePair("mid", String.valueOf(mid));nameValuePair.put("nameValuePair", values);String url = "http://space.bilibili.com/ajax/member/GetInfo?mid="+mid;   //bilibili用户信息获取接口//构造Request请求对象Request request = new Request(url);request.setExtras(nameValuePair);request.setMethod(HttpConstant.Method.POST);//向Spider对象添加Request对象spider.addRequest(request);}spider.thread(2).run();  //启动60个线程}
}

Github:https://github.com/Al-assad/Spider-bilibiliuser-full

当然如果使用IP代理池后,URL请求不再是运行效率瓶颈,此时有很多个地方可以进行相应的优化;

基于Webmagic的爬取B站用户数据的爬虫相关推荐

  1. python3网络爬虫--爬取b站用户投稿视频信息(附源码)

    文章目录 一.准备工作 1.工具 二.思路 1.整体思路 2.爬虫思路 三.分析网页 1.分析数据加载方式 2.分词接口url 3.分析用户名(mid) 四.撰写爬虫 五.得到数据 六.总结 上次写了 ...

  2. python爬取b站用户_python爬取b站排行榜

    爬取b站排行榜并存到mysql中 目的 b站是我平时看得最多的一个网站,最近接到了一个爬虫的课设.首先要选择一个网站,并对其进行爬取,最后将该网站的数据存储并使其可视化. 网站的结构 目标网站:bil ...

  3. python爬取b站用户_用Python爬取bilibili全站用户信息

    教你用Python爬取哔哩哔哩全站用户信息 运行 下载 git clone https://github.com/cexll/bili_user_Spider.git 复制代码 运行环境 Window ...

  4. python爬取抖音用户数据的单位是_爬取并分析一下B站的最热视频排行榜,看看大家都喜欢看什么视频...

    前言 现在大家的生活中,已经越来越离不开B站了,2020年的第一季度,B站月活跃用户达到了1.72亿,日活跃用户也已经突破了5000万个用户.源源不断的流量让B站的up主们也是粉丝数目不断暴涨,百万粉 ...

  5. 基于webmagic实现爬取博客园的所有精品文章

    最近有一些工作上的需要,需要接触到爬虫来爬取数据.之前有使用过Python实现一个很简单的爬虫Demo,这次由于公司使用的是Java爬虫,基于webmagic框架去实现的爬虫.于是就参考了资料自己学习 ...

  6. 基于Playwright+Asyncio爬取携程网的机票数据

    本项目基于Playwright+Asyncio实现对携程网的机票数据的爬取. 基本流程 1.Playwright启动浏览器,打开携程网机票数据页面. 2.启用回调函数监听后台返回的机票数据. 3.对机 ...

  7. python爬取抖音用户数据_python批量爬取下载抖音视频

    本文实例为大家分享了python批量爬取下载抖音视频的具体代码,供大家参考,具体内容如下 import os import requests import re import sys import a ...

  8. python爬取b站搜索结果_Python爬虫实例:爬取猫眼电影——破解字体反爬,Python爬虫实例:爬取B站《工作细胞》短评——异步加载信息的爬取,Python爬虫实例:爬取豆瓣Top250...

    字体反爬 字体反爬也就是自定义字体反爬,通过调用自定义的字体文件来渲染网页中的文字,而网页中的文字不再是文字,而是相应的字体编码,通过复制或者简单的采集是无法采集到编码后的文字内容的. 现在貌似不少网 ...

  9. python爬取抖音用户数据_使用python爬取抖音视频列表信息

    如果看到特别感兴趣的抖音vlogger的视频,想全部dump下来,如何操作呢?下面介绍介绍如何使用python导出特定用户所有视频信息 抓包分析 Chrome Deveploer Tools Chro ...

最新文章

  1. spring 全局变量_[Spring]-Spring框架-02-IOC容器
  2. string 与char *的区别
  3. 卷积神经网络之 - VGGNet
  4. jdk 1.8 字符串+_JDK 9/10/11:Java字符串上+ =的副作用
  5. 第三章计算机网络,第三章-计算机网络及应用.ppt
  6. P3919 【模板】可持久化数组(可持久化线段树/平衡树)(入门第一题)
  7. C/C++编程语言中操作目录及目录中文件的方法
  8. 修改Tomcat8的默认访问端口8080
  9. 计算机桌面变小了是怎么回事啊,电脑桌面整体变小了要怎么调回来的
  10. 网站漏洞修复 被上传webshell漏洞修补
  11. 【Only one connection receive subscriber allowed with】
  12. 抖音昵称html,抖音书单账号昵称_抖音网名大全
  13. 多协议充电桩平台系统小程序方案
  14. C语言进阶——字符函数和字符串函数
  15. 登录页面渗透测试思路总结
  16. ioc的概念和实现原理
  17. 常见挖矿病毒处理方法(qW3xT/Ddgs.3011/S01wipefs/acpidtd/MSFC)
  18. 计算机排第三!新增的工学硕士博士学位
  19. 万豪国际集团于上海开设第五家福朋喜来登酒店
  20. 【MOOC课程】浙大数据结构记录(下)

热门文章

  1. Facebook主页照片和封面照片的尺寸要求
  2. Tomcat跨域配置
  3. Jekyll分页功能
  4. 合振动的初相位推导_两个同方向、同频率的简谐振动表达式为和,试求它们的合振动的振幅和初相位。...
  5. php获取农历日期节日
  6. 直播疑难杂症排查(7)— 黑屏、花屏、闪屏问题
  7. SpringCloud与微服务Ⅷ --- Hystrix断路器
  8. 锤子T1(Smartisan T1 4G)版刷成3G版,即sm705运行sm701的CM11 Android 4.4.4ROM 1
  9. wps页眉添加下划线
  10. 【LeetCode】每日一题——保持城市天际线