【实战】SpringBoot整合ffmpeg实现动态拉流转推

SpringBoot整合ffmpeg实现动态拉流转推

在最近的开发中,遇到一个 rtsp 协议的视频流,前端vue并不能直接播放,因此需要对流进行处理。在网上查阅后,ffmpeg和webrtc是最多的解决方案,但是使用webrtc的时候没成功,所以选择ffmpeg。下面介绍一下整体的实现步骤。

一、搭建 ffmepg

  1. 安装升级必要的编译工具和库
sudo yum install -y epel-release
sudo yum install -y \autoconf automake bzip2 cmake freetype-devel gcc gcc-c++ git libtool make \mercurial nasm pkgconfig zlib-devel
  1. 安装 yasm 和 nasm
sudo yum install -y yasm nasm
  1. 安装第三方更新源
sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm
  1. 安装 ffmpeg
yum install ffmpeg ffmpeg-devel -y
  1. 查看版本
ffmpeg -version

在这里插入图片描述

版本比较低,但是在网上的yum安装方式,版本都差不多。也可以通过官网的源码包,安装最新的版本。

  1. 测试 ffmepg 功能

如果没有可以测试的流地址,可以参考这个网站RTSP 测试地址,不确保每个都可以用,可以用vlc播放器测试一下流能不能用。

找到可以使用的流后,通过ffmpeg指令,测试转码功能。参考下面的指令。

ffmpeg -rtsp_transport tcp -analyzeduration 50000000 -probesize 50000000 -i "rtsp://stream.strba.sk:1935/strba/VYHLAD_JAZERO.stream" -c:v h264 -c:a aac -strict -2 /root/1.mp4

如果ffmpeg正常运行,那么这个指令会将流,转换成MP4类型的文件,保存在 /root 目录下。关于其他参数的作用,可以上网搜索。包括可以查看支持哪些视频编码格式以及音频编码格式。

二、创建 Spring Boot 测试项目

考虑到要动态控制拉流的流地址,所以需要用SpringBoot来控制Linux指令,也就是上文最后的测试指令。也是在网上搜索后,找到一个最简单的方案,代码如下:

@RestController
@RequestMapping("/demo")
public class DemoController {@PostMapping("/rtsp")public String rtsp(@RequestBody Map<String, String> requestParams) {String url = requestParams.get("url");String fileName = requestParams.get("fileName");String ffmpegCmd = String.format("ffmpeg -rtsp_transport tcp -analyzeduration 50000000 -probesize 50000000 -i \"%s\" -c:v h264 -c:a aac -strict -2 /root/%s", url, fileName);System.out.println(ffmpegCmd);try {Process process = Runtime.getRuntime().exec(new String[] { "bash", "-c", ffmpegCmd });} catch (IOException e) {throw new RuntimeException(e);}return fileName;}
}

编写了一个接口,入参中填入url和fileName,没做校验,一开始测试也可以直接在代码中全部写死,主要是测试 Process 类能不能正常操作 Linux。打包部署测试后,是可以成功控制的。接口方式就可以满足你的业务的话,在这基础上修改一下就可以使用了。

三、定时任务控制拉流

现在最简单的demo就已经完成了,但是这样的实现方式需要手动控制,而视频流其实是固定的几个,用接口方式会很麻烦,所以我们可以创建定时任务,从数据库中读取流和其他数据,实现自动拉流。

  1. 封装拉流方法
@Slf4j
@Service
public class FfmpegService {private static final Map<FfmpegBO, Process> PROCESS_MAP = new ConcurrentHashMap<>();public void convertStream(FfmpegBO bo) {String url = bo.getStreamUrl();String fileDirName = bo.getFileDir();/*** /opt/ffmpeg/hls/ + 文件名*/String baseDirPath = "/opt/ffmpeg/hls/ " + fileDirName;String fileCreateCmd = String.format("mkdir -p %s", baseDirPath);try {Runtime.getRuntime().exec(new String[] { "sh", "-c", fileCreateCmd });} catch (IOException e) {throw new RuntimeException(e.getMessage());}String fileName = bo.getFilename();String ffmpegCmd = String.format("ffmpeg -rtsp_transport tcp -analyzeduration 50000000 -probesize 50000000 -i \"%s\" -c:v h264 -c:a aac -strict -2 /root/%s", url, fileName);try {Process process = Runtime.getRuntime().exec(new String[] { "sh", "-c", ffmpegCmd });// 按规则生成转换后的流地址bo.setConvertStreamUrl("xxxxxxxxxxxxxxxxxxxxxxxxx");PROCESS_MAP.put(bo, process);} catch (IOException e) {throw new RuntimeException(e.getMessage());}log.info("[FfmpegServiceImpl.pushStream] pushStreamBO: {}", bo);}
}

代码中创建了一个PROCESS_MAP用来保存执行的代码,用于在后面停止进程。在流转换方法中,传入自己需要的参数,按需求执行转换指令,然后保存到PROCESS_MAP

  1. 创建定时任务
@Slf4j
@Configuration
public class PushAndPullStreamTask implements InitializingBean {@Resourceprivate FfmpegService ffmpegService;private final ScheduledThreadPoolExecutor scheduledPool = new ScheduledThreadPoolExecutor(2);public static ThreadPoolExecutor commonAsyncPool = new ThreadPoolExecutor(4,8,3,TimeUnit.SECONDS,new ArrayBlockingQueue<>(100),r -> {Thread newThread = new Thread(r);newThread.setName(" commonAsyncPool - " + ThreadLocalRandom.current().nextInt(10000));return newThread;});@Overridepublic void afterPropertiesSet() throws Exception {// 开始流转,30秒后执行第一次,然后每隔五分钟执行一次scheduledPool.scheduleAtFixedRate(new convertStreamTask(), 30, 5 * 60, TimeUnit.SECONDS);}/*** 转换流任务*/class convertStreamTask implements Runnable {@Overridepublic void run() {List<Equipment> equipmentList = new ArrayList<>();/* 填充list */equipmentList.stream().forEach(equipment -> {commonAsyncPool.execute(() -> {try {FfmpegBO ffmpegBO = new FfmpegBO();ffmpegBO.setStreamUrl(equipment.getRemark());ffmpegBO.setFileDir(equipment.getDeviceSerial());ffmpegBO.setFilename(equipment.getDeviceSerial() + "_" + equipment.getChannelId());ffmpegService.convertStream(ffmpegBO);} catch (Exception e) {// 处理异常log.error("Error processing equipment: {},  ", equipment.getPkId(), e);}});});}}
}

简单构造一个定时任务,大概每五分钟执行一次(间隔短方便测试)。方法中构造了入参需要的ffmpegBO,开启一个线程池,并发执行转换方法。

PS:定时任务需要在启动类添加注解

  1. 停止进程的定时任务
    public void stopProcess() {log.info("[FfmpegServiceImpl.stopProcess] 停止进程, {}", PROCESS_MAP);PROCESS_MAP.forEach((bo, process) -> {if (!process.isAlive()) {return;}process.destroy();PROCESS_MAP.remove(bo);// 删除文件String baseDirPath = "/opt/ffmpeg/hls/" + bo.getFileDir();String fileCreateCmd = String.format("rm -rf %s", baseDirPath);try {Runtime.getRuntime().exec(new String[] { "sh", "-c", fileCreateCmd });} catch (IOException e) {throw new RuntimeException(e.getMessage());}log.info("stopProcess: {}", bo);});}

FfmpegService 中新增停止任务方式,删除保存的文件,并且停止之前的转换流进程。在PushAndPullStreamTask中添加停止任务的定时任务。比转换任务提前20秒执行。

@Slf4j
@Configuration
public class PushAndPullStreamTask implements InitializingBean {@Resourceprivate FfmpegService ffmpegService;private final ScheduledThreadPoolExecutor scheduledPool = new ScheduledThreadPoolExecutor(2);public static ThreadPoolExecutor commonAsyncPool = new ThreadPoolExecutor(4,8,3,TimeUnit.SECONDS,new ArrayBlockingQueue<>(100),r -> {Thread newThread = new Thread(r);newThread.setName(" commonAsyncPool - " + ThreadLocalRandom.current().nextInt(10000));return newThread;});@Overridepublic void afterPropertiesSet() throws Exception {// 开始流转,30秒后执行第一次,然后每隔五分钟执行一次scheduledPool.scheduleAtFixedRate(new convertStreamTask(), 30, 5 * 60, TimeUnit.SECONDS);// 停止流转,10秒后执行第一次,然后每隔五分钟执行一次scheduledPool.scheduleAtFixedRate(new destroyStreamTask(), 10, 5 * 60, TimeUnit.SECONDS);}/*** 转换流任务*/class convertStreamTask implements Runnable {@Overridepublic void run() {List<Equipment> equipmentList = new ArrayList<>();equipmentList.add(Equipment.builder().pkId(1).deviceSerial("1002654").channelId(1).remark("rtsp://180.101.128.47:9090/dss/monitor/param?cameraid=1002654%40021%241&substream=2").build());equipmentList.stream().forEach(equipment -> {commonAsyncPool.execute(() -> {try {FfmpegBO ffmpegBO = new FfmpegBO();ffmpegBO.setStreamUrl(equipment.getRemark());ffmpegBO.setFileDir(equipment.getDeviceSerial());ffmpegBO.setFilename(equipment.getDeviceSerial() + "_" + equipment.getChannelId());ffmpegService.pushStream(ffmpegBO);} catch (Exception e) {// 处理异常log.error("Error processing equipment: {},  ", equipment.getPkId(), e);}});});}}class destroyStreamTask implements Runnable {@Overridepublic void run() {ffmpegService.stopProcess();}}
}

打包部署运行后,观察服务器上是否有文件自动生成,以及自动删除。

四、容器化部署解决方案

现在的部署方式,一般都是容器化部署。但是ffmpeg安装在宿主机中,这意味着需要在容器中操作宿主机执行指令。最简单的方案就是使用 ssh指令,执行 ssh root@xxx.xxx.xxx.xxx “指令”。

测试方案是否可行
  1. 运行容器
docker run -it alpine
  1. 安装 ssh 指令
# 镜像
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
# 下载安装
apk update && apk add --no-cache openssh-client
  1. ssh 远程控制宿主机
ssh root@ip "mkdir -p /opt/ffmpeg"

执行命令后,可以通过输入密码或者密钥的方案实现执行命令,最后宿主机成功创建了文件夹,测试结果证明这样的方案是可行的。

但是java代码没有办法输入密码,所以只能通过密钥的免密登录方式来执行命令。

免密登录测试
  1. 宿主机创建 rsa 密钥
ssh-keygen -t rsa

执行指令后,会在 /root/.ssh/文件夹下生成两个密钥,后缀 pub 的是公钥,另一个就是私钥。免密登录需要将公钥复制到被登录的目标服务器,在现在需求中,需要在容器中远程登录宿主机,所以宿主机就是目标服务器,那么换个思路,将这里生成的私钥,放在容器中,就可以从容器中远程登录宿主机。

在这里插入图片描述

  1. 宿主机添加公钥
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
  1. 编写一个 Dockerfile,构建自定义镜像
vim Dockerfile# Dockerfile 内容
FROM alpineCOPY ./.ssh/id_rsa /root/.ssh/id_rsaRUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk update && apk add --no-cache openssh-client \&& chmod 600 /root/.ssh/id_rsa \&& ssh-keyscan -H 【宿主机ip】 >> /root/.ssh/known_hosts# 构建镜像
docker build -t [镜像名] .

在这里插入图片描述

  1. 运行容器,测试免密登录

在这里插入图片描述

准备工作都完成后,就修改最初的ffmpeg任务代码,通过ssh的方式调用宿主机执行命令

修改ffmpeg指令

将一些固定配置,抽离到配置文件中,封装config类,灵活控制。参考如下代码。

@Data
@Component
public class FfmpegConfig {@Value(value = "${ffmpeg.baseDirPath}")private String baseDirPath;@Value(value = "${ffmpeg.ipAddr}")private String ipAddr;@Value(value = "${ffmpeg.baseUrl}")private String baseUrl;@Value(value = "${ffmpeg.fileSuffix}")private String fileSuffix;
}

结合配置,修改service代码,参考代码。

@Slf4j
@Service
public class FfmpegService {private static final Map<FfmpegBO, Process> PROCESS_MAP = new ConcurrentHashMap<>();@Resourceprivate FfmpegConfig ffmpegConfig;public void pushStream(FfmpegBO bo) {String url = bo.getStreamUrl();String fileDirName = bo.getFileDir();/*** /opt/ffmpeg/hls/ + 文件名*/String baseDirPath = ffmpegConfig.getBaseDirPath() + fileDirName;String fileCreateCmd = String.format("mkdir -p %s", baseDirPath);String sshCmd = String.format("ssh root@%s \"%s\"", ffmpegConfig.getIpAddr(), fileCreateCmd);log.info("[FfmpegServiceImpl.pushStream] ssh :{}, 执行创建目录指令:{} ", sshCmd ,fileCreateCmd);try {Runtime.getRuntime().exec(new String[] { "sh", "-c", sshCmd });} catch (IOException e) {throw new RuntimeException(e.getMessage());}String fileName = bo.getFilename();String outputM3u8 = baseDirPath  +  "/" + fileName + ffmpegConfig.getFileSuffix();String ffmpegCmd = String.format(" ffmpeg -rtsp_transport tcp -analyzeduration 50000000 -probesize 50000000 -i \"%s\" -c:v h264  -c:a aac -strict -2 -f hls -hls_time 10 -hls_list_size 0 -hls_segment_filename \"%s/%s_segment_%%03d.ts\" %s",url, baseDirPath, fileName, outputM3u8);sshCmd = String.format("ssh root@%s \"%s\"", ffmpegConfig.getIpAddr(), ffmpegCmd);log.info("[FfmpegServiceImpl.pushStream] ssh :{}, 执行ffmpeg指令:{} ", sshCmd ,ffmpegCmd);try {Process process = Runtime.getRuntime().exec(new String[] { "sh", "-c", sshCmd });bo.setConvertStreamUrl(ffmpegConfig.getBaseUrl() + fileDirName + "/" + fileName + ffmpegConfig.getFileSuffix());PROCESS_MAP.put(bo, process);} catch (IOException e) {throw new RuntimeException(e.getMessage());}log.info("[FfmpegServiceImpl.pushStream] pushStreamBO: {}", bo);}public void stopProcess() {log.info("[FfmpegServiceImpl.stopProcess] 停止进程, {}", PROCESS_MAP);PROCESS_MAP.forEach((bo, process) -> {if (!process.isAlive()) {return;}process.destroy();PROCESS_MAP.remove(bo);// 删除文件String baseDirPath = ffmpegConfig.getBaseDirPath() + bo.getFileDir();String fileCreateCmd = String.format("rm -rf %s", baseDirPath);String sshCmd = String.format("ssh root@%s \"%s\"", ffmpegConfig.getIpAddr(), fileCreateCmd);log.info("[FfmpegServiceImpl.pushStream] ssh :{}, 执行删除目录指令:{} ", sshCmd ,fileCreateCmd);try {Runtime.getRuntime().exec(new String[] { "sh", "-c", sshCmd });} catch (IOException e) {throw new RuntimeException(e.getMessage());}log.info("stopProcess: {}", bo);});}
}
本次业务最终代码

调整后的代码,抽离封装了一些方法,并且将指令执行后的内容打印出来,方便观察执行效果

@Slf4j
@Service
public class FfmpegServiceImpl implements FfmpegService {private static final Map<FfmpegBO, Process> PROCESS_MAP = new ConcurrentHashMap<>();@Resourceprivate FfmpegConfig ffmpegConfig;@Overridepublic void pushStream(FfmpegBO bo) {String url = bo.getStreamUrl();String baseUrl = ffmpegConfig.getBaseUrl();String ipAddr = ffmpegConfig.getIpAddr();String fileDirName = bo.getFileDir();String baseDirPath = ffmpegConfig.getBaseDirPath() + fileDirName;String fileName = bo.getFilename();String outputM3u8 = baseDirPath + "/" + fileName + ffmpegConfig.getFileSuffix();// 创建远程目录createRemoteDirectory(ipAddr, baseDirPath);// 执行 FFmpeg 推流命令Process process = executeFfmpegCommand(ipAddr, url, baseDirPath, fileName, outputM3u8);// 设置转换后的流地址bo.setConvertStreamUrl(baseUrl + fileDirName + "/" + fileName + ffmpegConfig.getFileSuffix());log.info("[FfmpegServiceImpl.pushStream] pushStreamBO: {}", bo);// 将进程对象存入 PROCESS_MAPPROCESS_MAP.put(bo, process);}/*** 停止所有推流进程,并删除远程目录*/@Overridepublic void stopProcess() {log.info("[FfmpegServiceImpl.stopProcess] 停止进程, {}", PROCESS_MAP);PROCESS_MAP.forEach((bo, process) -> {if (!process.isAlive()) {log.warn("[FfmpegServiceImpl.stopProcess] 进程已停止, {}", bo);PROCESS_MAP.remove(bo);return;}// 终止进程process.destroy();PROCESS_MAP.remove(bo);// 删除远程目录deleteRemoteDirectory(ffmpegConfig.getIpAddr(), ffmpegConfig.getBaseDirPath() + bo.getFileDir());log.info("stopProcess: {}", bo);});}/*** 创建远程目录* @param ipAddr 远程服务器 IP 地址* @param baseDirPath 远程目录路径*/private void createRemoteDirectory(String ipAddr, String baseDirPath) {String fileCreateCmd = String.format("mkdir -p %s", baseDirPath);String sshCmd = String.format("ssh root@%s \"%s\"", ipAddr, fileCreateCmd);log.info("[FfmpegServiceImpl.createRemoteDirectory] ssh :{}, 执行创建目录指令:{} ", sshCmd, fileCreateCmd);executeCommand(sshCmd);}/*** 删除远程目录* @param ipAddr 远程服务器 IP 地址* @param baseDirPath 远程目录路径*/private void deleteRemoteDirectory(String ipAddr, String baseDirPath) {String fileDeleteCmd = String.format("rm -rf %s", baseDirPath);String sshCmd = String.format("ssh root@%s \"%s\"", ipAddr, fileDeleteCmd);log.info("[FfmpegServiceImpl.deleteRemoteDirectory] ssh :{}, 执行删除目录指令:{} ", sshCmd, fileDeleteCmd);executeCommand(sshCmd);}/*** 执行 FFmpeg 推流命令* @param ipAddr 远程服务器 IP 地址* @param url 推流 URL* @param baseDirPath 远程目录路径* @param fileName 文件名* @param outputM3u8 输出的 M3U8 文件路径* @return 返回启动的进程对象*/private Process executeFfmpegCommand(String ipAddr, String url, String baseDirPath, String fileName, String outputM3u8) {String ffmpegCmd = String.format("ffmpeg -rtsp_transport tcp -analyzeduration 50000000 -probesize 50000000 -i \"%s\" -c:v h264 -c:a aac -strict -2 -f hls -hls_time 10 -hls_list_size 0 -hls_segment_filename \"%s/%s_segment_%%03d.ts\" %s",url, baseDirPath, fileName, outputM3u8);String sshCmd = String.format("ssh root@%s \"%s\"", ipAddr, ffmpegCmd);log.info("[FfmpegServiceImpl.executeFfmpegCommand] ssh :{}, 执行ffmpeg指令:{} ", sshCmd, ffmpegCmd);Process process = executeCommand(sshCmd);// 启动线程处理标准输出和错误输出,防止进程阻塞handleProcessOutput(process);return process;}/*** 执行 Shell 命令* @param command 要执行的命令* @return 进程对象*/private Process executeCommand(String command) {try {return Runtime.getRuntime().exec(new String[]{"sh", "-c", command});} catch (IOException e) {log.error("执行命令失败:{}", command, e);throw new RuntimeException("执行命令失败:" + e.getMessage());}}/*** 处理进程的标准输出和错误输出* @param process 需要处理的进程*/private void handleProcessOutput(Process process) {new Thread(() -> {try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {String line;while ((line = reader.readLine()) != null) {log.info("[FfmpegServiceImpl.handleProcessOutput] Process output: {}", line);}} catch (IOException e) {log.error("[FfmpegServiceImpl.handleProcessOutput] 读取进程输出失败", e);}}).start();new Thread(() -> {try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {String line;while ((line = reader.readLine()) != null) {log.error("[FfmpegServiceImpl.handleProcessOutput] Process error: {}", line);}} catch (IOException e) {log.error("[FfmpegServiceImpl.handleProcessOutput] 读取进程错误输出失败", e);}}).start();}
}

五、Nginx 推流

拉流的流程都成功以后,就需要将流推出去,这边用nginx进行推流。修改nginx配置文件。在server节点中添加下面的配置,root的值根据自己的文件保存位置填写,现在的配置代表文件位于 /opt/ffmpeg/hls/目录下。

location /hls {types {application/vnd.apple.mpegurl m3u8;video/mp2t ts;}root /opt/ffmpeg;add_header Cache-Control no-cache;
}

修改完配置后,nginx -s reload 使配置生效。

六、前端参考代码

转换后的流是hls格式,使用 vue3-video-play 组件,demo代码如下

<template><div class="login-container"><videoPlay :src="streamUrl" type="application/vnd.apple.mpegurl"></videoPlay></div>
</template><script setup>
import { ref } from 'vue'
import 'vue3-video-play/dist/style.css'
import videoPlay from 'vue3-video-play'const streamUrl = ref("https://xxxxxxxxxxxxxx.m3u8") 
</script><style lang="scss" scoped></style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/388489.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

layui table 重新设置表格的高度

在layui的table模块中&#xff0c;如果使用table.render({})渲染了一个表格实例时&#xff0c;确定了height配置&#xff0c;后续用table.resize(id)方法重置表格尺寸时&#xff0c;表格的高度是不会变化的&#xff08;如果我的理解没有错的话&#xff09;。 有时我们希望根据…

k8s核心知识总结

写在前面 时间一下子到了7月份尾&#xff1b;整个7月份都乱糟糟的&#xff0c;不管怎么样&#xff0c;日子还是得过啊&#xff0c; 1、7月份核心了解个关于k8s&#xff0c;iceberg等相关技术&#xff0c;了解了相关的基础逻辑&#xff0c;虽然和数开主线有点偏&#xff0c;但是…

传统自然语言处理(NLP)与大规模语言模型(LLM)详解

自然语言处理&#xff08;NLP&#xff09;和大规模语言模型&#xff08;LLM&#xff09;是理解和生成人类语言的两种主要方法。本文将介绍传统NLP和LLM的介绍、运行步骤以及它们之间的比较&#xff0c;帮助新手了解这两个领域的基础知识。 传统自然语言处理&#xff08;NLP&…

OpenEuler安装部署教程

目录 OpenEuler安装部署教程 MobaXterm一款全能的远程工具 yum安装软件 vim编辑器&#xff08;了解&#xff09; 防火墙 常用命令 网络工具netstat & telnet 进程管理工具top ps 磁盘free、fdisk 用户、组&#xff08;了解&#xff09; 权限&#xff08;了解&am…

君正T41开发板环境搭建_串口登陆_配置IP_telnet登陆_mount挂载_安装交叉编译工具链

目录 1 开发板外观 2 串口连接 3芯片内存情况 4 配置IP地址 5 telnet登陆 6 mount挂载目录 7 安装交叉编译工具链 1 开发板外观 2 串口连接 我直接用MobaXterm连接&#xff0c;虽然我还没有文档&#xff0c;但是我觉得波特率大概率就是115200&#xff0c;试了下确实可以…

webstorm配置项目Typescript编译环境

使用npm命令安装typeScript编译器 npm install typescript -g 安装好&#xff0c;在命令行可以查看编译器的版本 tsc --version 用Webstorm打开一个Typescript的项目。为TypeScript文件更改编译设置&#xff0c;File->Settings->toosl -> File Watchers->TypeScri…

Python爬虫入门01:在Chrome浏览器轻松抓包

文章目录 爬虫基本概念爬虫定义爬虫工作原理爬虫流程爬虫类型爬虫面临的挑战 使用Chrome浏览器抓包查看网页HTML代码查看HTTP请求请求头&#xff08;Request Header&#xff09;服务器响应抓包的意义 爬虫基本概念 爬虫定义 爬虫&#xff08;Web Crawler 或 Spider&#xff0…

Vulnhub靶机-Jangow 1.0.1

Vulnhub靶机-Jangow 1.0.1 修改为NAT模式 ?buscarecho <?php eval($_POST[cmd])?> >shell.php后面试了试很多网上的方法反弹shell但都不行

只用一个 HTML 元素可以写出多少形状?——平行四边形篇

您有没有想过一个问题&#xff0c;如果我们只用一个 div 元素&#xff0c;一共可以写出多少种形状呢&#xff1f; 暂停一下&#xff0c;思考三秒钟&#xff0c;默默记下自己的答案&#xff0c;看看自己想到的答案对不对。然后&#xff0c;我们就来一起盘点一下吧…… 今天的主…

java开发环境搭建基础之3----开发工具eclipse中Maven配置

一.背景 公司安排了带徒弟任务&#xff0c;写点基础的环境搭建这些吧。搭建基础开发环境&#xff0c;主要是jdk、eclipse、git、maven、mysql。后续再考虑编写jenkins、nexus、docker、1panel等CI/CD环境搭建。本次主要内容是eclipse中maven环境的配置。我的开发环境&#xff0…

React 学习——路由跳转(Link、useNavigate)、跳转时传递参数(问号传递、path中冒号拼接)

需要四个页面&#xff1a;项目入口index.js文件&#xff0c;router配置路由跳转文件&#xff0c;article组件页面&#xff0c;login组件页面 1、项目入口index.js文件 注意&#xff1a;要安装这个依赖 react-router-dom import React from react import { createRoot } fro…

TZDYM001矩阵系统源码 矩阵营销系统多平台多账号一站式管理

外面稀有的TZDYM001矩阵系统源码&#xff0c;矩阵营销系统多平台多账号一站式管理&#xff0c;一键发布作品。智能标题&#xff0c;关键词优化&#xff0c;排名查询&#xff0c;混剪生成原创视频&#xff0c;账号分组&#xff0c;意向客户自动采集&#xff0c;智能回复&#xf…

vue3使用递归组件渲染层级结构

先看看是不是你想要的&#xff1a; 当有层级去渲染的时候&#xff0c;嵌套的层级不明确&#xff0c;这时只能通过递归组件去渲染。 数据如下&#xff1a; 通过判断subCatalog这个字段的长度是否大于0来确定是否有下级。 上代码&#xff1a;(代码是使用uniapp开发的&#xff0…

简单洗牌算法

&#x1f389;欢迎大家收看&#xff0c;请多多支持&#x1f339; &#x1f970;关注小哇&#xff0c;和我一起成长&#x1f680;个人主页&#x1f680; ⭐目前主更 专栏Java ⭐数据结构 ⭐已更专栏有C语言、计算机网络⭐ 在学习了ArrayList之后&#xff0c;我们可以通过写一个洗…

iOS ------ 持久化

一&#xff0c;数据持久化的目的 1&#xff0c;快速展示&#xff0c;提升体验 已经加载过的数据&#xff0c;用户下次查看时&#xff0c;不需要再次从网络&#xff08;磁盘&#xff09;加载&#xff0c;直接展示给用户 2.节省用户流量 对于较大的资源数据进行缓存&#xff…

C++|设计模式(七)|⭐️观察者模式与发布/订阅模式,你分得清楚吗

本文内容来源于B站&#xff1a; 【「观察者模式」与「发布/订阅模式」&#xff0c;你分得清楚吗&#xff1f;】 文章目录 观察者模式&#xff08;Observer Pattern&#xff09;的代码优化观察者模式 与 发布订阅模式 他们是一样的吗&#xff1f;发布订阅模式总结 我们想象这样一…

批量下载 B 站 视频的工具 downkyi

批量下载 B 站 视频的工具 downkyi 亲测好用 图片&#xff1a; 下载地址&#xff1a; https://github.com/leiurayer/downkyi

MES系统在机床产业智能化的作用

MES系统&#xff08;Manufacturing Execution System&#xff0c;制造执行系统&#xff09;在机床产业智能化过程中发挥着至关重要的作用。以下是万界星空科技MES系统在机床产业智能化中的几个关键作用&#xff1a; 1. 实时数据采集与分析 数据采集&#xff1a;MES系统通过与生…

jenkins获取sonarqube质量门禁结果

前景 在使用 Jenkins 集成 SonarQube 时&#xff0c;获取质量门禁&#xff08;Quality Gate&#xff09;结果非常重要。SonarQube 的质量门禁是一种质量控制机制&#xff0c;用于评估代码质量是否符合预设的标准。以下是获取质量门禁结果的意义和作用&#xff1a; 评估代码质量…