今天被同事问到能不能把视频水印去掉,后面得知要去水印的是小红书上的视频……
偶然记得之前在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>
教程到这里就结束了,代码呢也没有什么高深的地方,主要还是思路。对于没有接触过这个方面的人来说还是有点值得学习的地方。其他的类似的平台完全可以举一反三,原理方面也都大差不差。