前几天介绍了 Spring boot 发送请求:SpringBoot API 系列(八) 发送请求。今天用以练习,爬取 B 站的用户信息。

我们注意到:当未登录 B站 的时候,也可以查看用户的一些简单信息如姓名、生日、等级等,也可以查看粉丝数和关注数。打开 F12 ,查看控制台(゜-゜)つロ果然发现了部分接口。


细细查找,发现了分别获取用户基础信息的 info 接口,和获取用户粉丝数量的 sata 接口:


于是我尝试着把 B站 的用户信息爬取下来。由于用户量巨大,注册用户数在 4.3亿 以上,我担心我的小笔记本和数据库,所以仅仅把一些基础信息记录下来。

创建数据表 比利比利


创建数据库实体

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class Bilibili implements Serializable {
    private static final long serialVersionUID = 1L;
    private Integer mid;
    private String name;
    private String sex;
    private String birthday;
    private Integer level;
    private Integer follower;
    private Integer following;
}

下面开始编写主方法:

public String bili(Integer start, Integer end) {
    if (end == null || end == 0 || start == null || start == 0 || start > end) return "err";

    Bilibili bili;
    String urlInfo, urlStat;
    JSONObject objInfo, objStat, dataInfo, dataStat;
    long dt = System.currentTimeMillis();
    for (int i = start; i < end; ++i) {
        if (i % 200 == 0) {
            System.out.println("开始睡眠10秒");
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        bili = new Bilibili();
        bili.setMid(i);

        urlInfo = "https://api.bilibili.com/x/space/acc/info?mid=" + i + "&jsonp=jsonp";
        objInfo = JSONObject.parseObject(biliGet(urlInfo));
        if ("0".equals(objInfo.get("code").toString())
                && "0".equals(objInfo.get("message").toString())) {
            dataInfo = JSONObject.parseObject(objInfo.get("data").toString());
            bili.setName(dataInfo.get("name").toString())
                    .setBirthday(dataInfo.get("birthday").toString())
                    .setSex(dataInfo.get("sex").toString())
                    .setLevel(Integer.valueOf(dataInfo.get("level").toString()));
        }

        urlStat = "https://api.bilibili.com/x/relation/stat?vmid=" + i + "&jsonp=jsonp";
        objStat = JSONObject.parseObject(biliGet(urlStat));
        if ("0".equals(objStat.get("code").toString())
                && "0".equals(objStat.get("message").toString())) {
            dataStat = JSONObject.parseObject(objStat.get("data").toString());
            bili.setFollower(Integer.valueOf(dataStat.get("follower").toString()))
                    .setFollowing(Integer.valueOf(dataStat.get("following").toString()));
        }

        bilibiliMapper.insert(bili);
        if (i % 100 == 0) {
            System.out.println("第(" + i + ")个完成,本组耗时:" + (System.currentTimeMillis() - dt) + "毫秒");
            dt = System.currentTimeMillis();
        }
    }
    return "end";
}

private static String biliGet(String url) {
    CloseableHttpClient httpclient = HttpClients.createDefault();
    String responseInfo = null;
    try {
        HttpGet httpGet = new HttpGet(url);
        httpGet.addHeader("Content-Type", "application/json;charset=UTF-8");
        CloseableHttpResponse response = httpclient.execute(httpGet);
        HttpEntity entity = response.getEntity();
        int status = response.getStatusLine().getStatusCode();
        if (status == 200 && entity != null) {
            responseInfo = EntityUtils.toString(entity);
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            httpclient.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return responseInfo;
}

运行,嗯还可以。打包扔到服务器上运行:


查看数据库,已经有数据了!


如果反复请求 API 导致被 B站 暂时封禁,则可以更换 IP 或者等待 5分钟 后再启动。

抛砖引玉:可优化部分:1.采用多线程,并发执行提高效率;2.优化biliGet函数,并不是每次都关闭 http 链接;3.积攒多条数据后集中插入数据库,而不是每条执行一次;4.异步,获取数据与写入数据分离、异步执行,提高系统效率;5.采用多台机器硬件加速;


附上:

B站用户信息(前一百万条)  bilibili-100w.sql

B站源码(没错就是著名的 GIthub/openbilibili) go-common.zip