目录

zrlong 的个人博客

希望大家都能保护好自己身上的特质,无论是五年还是十年,永远善良,不服输,热爱你所热爱。在漫长岁月的变迁里,是这些让你永远迷人,富有生命力。

X

Java爬取腾讯视频弹幕

过程

找到弹幕请求

网页上的资源肯定是通过某一个请求,然后返回信息再展示给用户,所以我们要先找到弹幕的请求资源。此篇文章根据《画江湖之不良人5》为例子,发现腾讯的弹幕是通过下面路径获取的。

https://mfm.video.qq.com/danmu?otype=json&callback=jQuery19108459447664545832_1650675513653&target_id=7642029203%26vid%3Dy00413sywan&session_key=0%2C392%2C1650675515&timestamp=435&_=1650675513691

经过一个一个参数的删减测试,最终发现只有target_id和timestamp这两个参数有用,那么这两个参数有什么规律呢。

参数的规律

  • timestamp:这个参数就是弹幕的时间戳,是以秒为单位的。经过多组请求的对比,发现时间戳是有规律的。如下图,我们能发现,这个请求的时间戳是每次增加三十的,那么我们就可以循环请求了。那么什么时候停止呢,请求返回的数据中有一个名为 count 的参数,当这个参数为0时,就是我们终止循环的时候。

image.png

  • target_id:猜想这是对应哪一个视频的id,因此多找几个视频找规律,比如不良人第一集是这个”7642029203%26vid%3Dy00413sywan“,第二集是:”7642029206%26vid%3Dp00412bp6qb“。可以发现target_Id由三部分组成,前缀 + %26vid%3D + 后缀。

找到后缀

通过观察,发现后缀就在导航栏的地址中,如:https://v.qq.com/x/cover/mzc00200gw2ez0b/y00413sywan.html。但是不幸的是,如果你看的是电视剧或者动漫,有许多集的视频,当你第一次点进去的时候,它的后缀并没有显示。但是看了看网页的源码,发现它就在网页源码中的一个标签里,视频的剧集名字也在,这就解决了。

image.png

找到前缀

那我们如何找到前缀呢?本集的前缀为 7642029203,我们直接在开发者工具中搜索前缀,结果如下图,我们发现在一个regist的请求中出现了。

image.png

image.png

它出现了!但是我们怎么通过这个请求获得呢?查看标头,发现这是个POST请求。

image.png

再看看负载,看它需要什么参数。

image.png

发现它的请求负载中有这么多参数,我们再次删除参数一个一个试一下,发现只有两个参数有用。

wRegistTypevecIdList ,这个vecIdList 不就是刚才的后缀嘛!这就简单了,wRegistType 是个固定的值2。

到此请求弹幕的所有参数就找齐了,接下来开始上代码。

代码

为了方便调用,我写成了工具类。

public class TXAnyalysisUtil {
    // 得到Document对象,方便从页面标签中取值。
    public Document getDocument(String url) throws IOException {
        return Jsoup.parse(new URL(url), 10000);
    }
    // 从页面标签中得到后缀
    public String getTxSuffixId(Document parse) throws IOException {
        // 视频完整地址在rel为canonical的link标签中
        Elements meta = parse.getElementsByTag("link");
        String suffixId = meta.select("link[rel=canonical]").attr("href");
        // 截取字符串
        return suffixId.substring(suffixId.lastIndexOf("/") + 1, suffixId.lastIndexOf("."));
    }
    // 从页面标签中得到视频名字
    public String getTxVideoName(Document parse) {
        // 视频名字在title标签中
        Elements title = parse.getElementsByTag("title");
        String name = title.text().substring(0, title.text().indexOf("_"));
        name = name.replaceAll(" ", "");
        return name;
    }
    // 从页面标签中得到前缀targetId
    public String getTxTargetId(String suffixId) throws IOException {
        String regist = "https://access.video.qq.com/danmu_manage/regist?vappid=97767206&vsecret=c0bdcbae120669fff425d0ef853674614aa659c605a613a4&raw=1";
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", "application/json; charset=UTF-8");
        Map<String, Object> params = new HashMap<>();
        List<String> list = new ArrayList<>();
        list.add(suffixId);
        params.put("vecIdList", list);
        params.put("wRegistType", 2);
        String jsonData = JSON.toJSONString(params);
//        String body = "{\"wRegistType\":2,\"vecIdList\":[" +"\""+ suffixId+"\""+ "]}";
        // 把参数放入
        HttpEntity<String> post = new HttpEntity<>(jsonData, headers);

        RestTemplate restTemplate = new RestTemplate();
        URI uri = URI.create(regist);
        String s = restTemplate.postForObject(uri, post, String.class);
        JSONObject jsonNode = JSON.parseObject(s).getJSONObject("data").getJSONObject("stMap").getJSONObject(suffixId);
        String strDanMuKey = jsonNode.getString("strDanMuKey");
        strDanMuKey = strDanMuKey.substring(strDanMuKey.lastIndexOf("=") + 1);
        return strDanMuKey;
    }
    // 获取弹幕
    public Map<Integer,String> getTxBarrage(String targetId,String suffixId) throws IOException {
        // 获取弹幕
        String danmu = "https://mfm.video.qq.com/danmu";
        RestTemplate restTemplate = new RestTemplate();
        int count = 1;
        int timestamp = 15;
        Map<Integer,String> danmuMap = new HashMap<>();
        // 本次获取弹幕数量为0时停止
        while (count != 0) {
            Map<String, Object> map = new HashMap<>();
            // 完整id
            map.put("target_id", targetId + "%26vid%3D" + suffixId);
            // 时间戳
            map.put("timestamp", timestamp);
            // tx每30秒更新一次弹幕
            timestamp += 30;
            String forObject = restTemplate.getForObject(danmu + "?target_id={target_id}&timestamp={timestamp}", String.class, map);
            ObjectMapper mapper=new ObjectMapper();
            mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
            JsonNode jsonObject = mapper.readTree(forObject);
            count = jsonObject.get("count").intValue();
            JsonNode jsonNode = jsonObject.get("comments");
            jsonNode.forEach(node -> {
                danmuMap.put(Integer.parseInt(String.valueOf(node.get("timepoint"))), node.get("content").toString().replaceAll("\"", ""));
            });
        }
        return danmuMap;
    }

}

到此就结束啦,接下来是更难的深度学习,对弹幕词语感情的分析。


标题:Java爬取腾讯视频弹幕
作者:zrlong
地址:http://blog.zrlong.top/articles/2022/04/23/1650677874958.html