今天被同事问到能不能把视频水印去掉,后面得知要去水印的是小红书上的视频……
偶然记得之前在chrome商店用过几款插件相当不错,试了一下发现失效了。去Git上面搜了一下,大都是其他语言的。瞬间不想看了,还得多学一门语言?趁着划水的时间,刚好在知乎找到一篇文章。
知乎:爬取小红书无水印视频和图片教程-非第三方接口

看完之后只觉得是醍醐灌顶,一发不可收拾。顺便看了一眼该文章评论区,发现规则变了!没错,教程已经过去一年了肯定会有点变化,然后我先按照教程瞅了一眼。嘿,你猜怎么着?变的简单了,不需要你做任何处理,它就自动来到你面前了。
所谓的无水印下载,其实是下载的是小红书在线播放的那个不带水印的视频。并不是后面通过其他的什么方式去除了水印,相信很多人都有被误导。

按图索骥就是:
1.浏览器打开小红书任意视频网页,右键-查看网页源代码。
2.按下快捷键Ctrl+F,搜索:"originVideoKey"。这就是视频真实在线播放地址对应的key。
3.通过上述文章中的两个CDN地址中的任意一个拼接这个key,直接浏览器跳转下载。
4.把下载后的文件重命名为:“.mp4”格式。(如果你打开发现不是视频也不要奇怪,是你的播放器不兼容。这个视频可能需要做个转码啥的才能变成标准的mp4格式,但是大多数软件都能自动识别。)
5.完成。

到此为止,下载无水印视频的步骤我们已经搞定了。接下来就简单的用代码来实现一下,免得人工重复这个操作~

后端:
service层:
package cc.rjl.server;

import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.springframework.stereotype.Service;

import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;

/**
 * @Author zf
 * @ClassName XHSVideoServer.java
 * @ProjectName robot
 */
@Service
public class XHSVideoServer {

    // CDN
    static final String CDN1 = "https://sns-video-al.xhscdn.com/";
    static final String CDN2 = "https://sns-video-hw.xhscdn.com/";

    /**
     * 下载视频地址
     *
     * @param url 视频URL
     * @return 包含CDN地址的JSON对象
     */
    public JSONObject downloadVideo(String url) {
        // 使用正则表达式提取URL
        url = ReUtil.get("http://[a-zA-Z0-9./]+", url, 0);
        String key = getKey(url);
        if (StrUtil.isBlank(key)){
            return new JSONObject().set("url", "输入的链接不合法!").set("backupUrl", "输入的链接不合法!");
        }
        return new JSONObject().set("url", getCdnUrl(key, CDN1)).set("backupUrl", getCdnUrl(key, CDN2));
    }

    /**
     * 从URL中获取视频Key
     *
     * @param url 视频URL
     * @return 视频Key
     */
    public String getKey(String url) {
        try {
            Document doc = Jsoup.connect(url).get();
            Element scriptElement = doc.select("script:containsData(__INITIAL_STATE__)").first();

            if (scriptElement != null) {
                String scriptContent = scriptElement.data();
                scriptContent = scriptContent.replace("window.__INITIAL_STATE__=", "");
                JSONObject initialState = new JSONObject(scriptContent);

                // 使用stream查找键
                return findKeyInJsonObject(initialState, "video")
                        .map(value -> JSONUtil.parseObj(value).getJSONObject("consumer").getStr("originVideoKey"))
                        .orElse(null);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 在JSONObject中查找指定键
     *
     * @param jsonObject 待查找的JSONObject
     * @param keyToFind   要查找的键
     * @return 包含键值的Optional
     */
    private static Optional<Object> findKeyInJsonObject(JSONObject jsonObject, String keyToFind) {
        // 使用stream查找键
        return Stream.concat(
                        Stream.of(jsonObject.get(keyToFind)),
                        jsonObject.values().stream().filter(value -> value instanceof JSONObject)
                                .map(value -> findKeyInJsonObject((JSONObject) value, keyToFind))
                                .filter(Optional::isPresent)
                                .map(Optional::get)
                ).filter(Objects::nonNull)
                .findFirst();
    }

    /**
     * 获取CDN地址
     *
     * @param key  视频Key
     * @param cdn  CDN地址
     * @return 完整的CDN URL
     */
    private static String getCdnUrl(String key, String cdn) {
        return cdn + key;
    }
}

controller层:
package cc.rjl.controller;

import cc.rjl.server.XHSVideoServer;

import cn.hutool.json.JSONObject;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @Author zf
 * @ClassName XHSVideoController.java
 * @ProjectName robot
 */
@RestController
public class XHSVideoController {

    @Resource
    private XHSVideoServer xhsVideoServer;

    @PostMapping("/downloadVideo")
    public JSONObject downloadVideo(String url){
        return xhsVideoServer.downloadVideo(url);
    }
}
防跨域配置:
package cc.rjl.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

/**
 * @Author zf
 * @ClassName WebMvcConfig.java
 * @ProjectName robot
 */
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
                .maxAge(3600)
                .allowCredentials(false);
    }
}
所需依赖:
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.16.1</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.20</version>
        </dependency>
前端:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>XHS</title>
    <!-- 引入 layui.css -->
    <link rel="stylesheet" href="https://cdn.staticfile.org/layui/2.6.8/css/layui.css">
    <style>
        body {
            padding: 20px;
            text-align: center; /* 居中 */
        }

        .layui-form {
            display: inline-block; /* 让表单水平居中 */
            text-align: left; /* 左对齐 */
        }

        .layui-form-item {
            margin-bottom: 10px;
        }

        .layui-input {
            width: 100%;
            text-align: left; /* 左对齐 */
        }

        #responseTable {
            margin-top: 20px;
            display: inline-block; /* 让表格水平居中 */
            text-align: left; /* 左对齐 */
            width: auto; /* 自适应宽度 */
        }

        .layui-table-cell {
            white-space: normal !important;
        }
        
        #copyright {
            position: fixed;
            bottom: 30px;
            width: 100%;
            text-align: center;
        }
    </style>
</head>
<body>

<div class="layui-form">
    <div class="layui-form-item">
        <label class="layui-form-label">视频URL</label>
        <div class="layui-input-inline">
            <input type="text" id="videoUrl" placeholder="请输入视频URL" class="layui-input">
        </div>
        <div class="layui-input-inline">
            <button class="layui-btn" onclick="downloadVideo()">解析视频</button>
        </div>
    </div>
</div>
<br />
<div id="responseTable" class="layui-table-container"></div>

<script src="https://cdn.staticfile.org/jquery/3.6.3/jquery.min.js"></script>
<!-- 引入 layui.js -->
<script src="https://cdn.staticfile.org/layui/2.6.8/layui.js"></script>
<script>
    function downloadVideo() {
        var videoUrl = $("#videoUrl").val();

        $.ajax({
            type: "POST",
            url: "http://192.168.5.80:9696/downloadVideo",
            data: {url: videoUrl},
            success: function (response) {
                var backupUrl = response.backupUrl;
                var downloadUrl = response.url;

                // 构建表格
                var tableHtml = '<div id="responseTable" class="layui-table-container">';
                tableHtml += '<table class="layui-table"><colgroup><col></colgroup>';
                tableHtml += '<thead><tr><th>备用地址</th></tr></thead>';
                tableHtml += '<tbody><tr><td>' + backupUrl + '</td></tr></tbody></table>';
                tableHtml += '<table class="layui-table"><colgroup><col></colgroup>';
                tableHtml += '<thead><tr><th>下载地址</th></tr></thead>';
                tableHtml += '<tbody><tr><td>' + downloadUrl + '</td></tr></tbody></table>';
                tableHtml += '</div>';

                // 显示表格
                $("#responseTable").html(tableHtml);
            },
            error: function (error) {
                console.log("Error:", error);
                layui.layer.msg("下载失败,请检查输入的URL或联系管理员!");
            }
        });
    }
</script>

<!-- 版权信息 -->
<div id="copyright">仅供学习研究,非法用途后果自负!</div>

</body>
</html>

教程到这里就结束了,代码呢也没有什么高深的地方,主要还是思路。对于没有接触过这个方面的人来说还是有点值得学习的地方。其他的类似的平台完全可以举一反三,原理方面也都大差不差。

最后修改:2024 年 01 月 16 日
给我一点小钱钱也很高兴啦!o(* ̄▽ ̄*)ブ