FFmpeg的简单使用【Windows】--- 指定视频的时长

目录

功能描述

效果展示

代码实现

前端代码

后端代码

routers =》users.js

routers =》 index.js

app.js


功能描述

此案例是在上一个案例【FFmpeg的简单使用【Windows】--- 视频混剪+添加背景音乐-CSDN博客】的基础上的进一步完善,可以先去看上一个案例然后再看这一个,这些案例都是每次在上一个的基础上加一点功能。

在背景音乐区域先点击【选择文件】按钮上传生成视频的背景音乐素材;

然后在视频区域点击【选择文件】按钮选择要混剪的视频素材,最多可选择10个;

然后可以在文本框中输入你想要生成多长时间的视频,此处我给的默认值是 10s 即你要是不修改的话就是默认生成 10s 的视频;

最后点击【开始处理】按钮,此时会先将选择的视频素材上传到服务器,然后将视频按照指定的时间进行混剪并融合背景音乐。

效果展示

处理完毕的视频
上传的视频素材

代码实现

说明:

前端代码是使用vue编写的。

后端接口的代码是使用nodejs进行编写的。

前端代码

<template>
  <div id="app">
    <!-- 显示上传的音频 -->
    <div>
      <h2>上传的背景音乐</h2>
      <audio
        v-for="audio in uploadedaudios"
        :key="audio.src"
        :src="audio.src"
        controls
        style="width: 300px"
      ></audio>
    </div>

    <!-- 上传视频音频 -->
    <input type="file" @change="uploadaudio" accept="audio/*" />
    <hr />
    <!-- 显示上传的视频 -->
    <div>
      <h2>将要处理的视频</h2>
      <video
        v-for="video in uploadedVideos"
        :key="video.src"
        :src="video.src"
        controls
        style="width: 120px"
      ></video>
    </div>

    <!-- 上传视频按钮 -->
    <input type="file" @change="uploadVideo" multiple accept="video/*" />
    <hr />
    <h1>设置输出视频长度</h1>
    <input type="number" v-model="timer" class="inputStyle" />
    <hr />

    <!-- 显示处理后的视频 -->
    <div>
      <h2>已处理后的视频</h2>
      <video
        v-for="video in processedVideos"
        :key="video.src"
        :src="video.src"
        controls
        style="width: 120px"
      ></video>
    </div>

    <button @click="processVideos">开始处理</button>
  </div>
</template>

<script setup>
import axios from "axios";
import { ref } from "vue";

const uploadedaudios = ref([]);
const processedAudios = ref([]);
let audioIndex = 0;
const uploadaudio = async (e) => {
  const files = e.target.files;
  for (let i = 0; i < files.length; i++) {
    const file = files[i];
    const audioSrc = URL.createObjectURL(file);
    uploadedaudios.value = [{ id: audioIndex++, src: audioSrc, file }];
  }
  await processAudio();
};
// 上传音频
const processAudio = async () => {
  const formData = new FormData();
  for (const audio of uploadedaudios.value) {
    formData.append("audio", audio.file); // 使用实际的文件对象
  }
  try {
    const response = await axios.post(
      "http://localhost:3000/user/single/audio",
      formData,
      {
        headers: {
          "Content-Type": "multipart/form-data",
        },
      }
    );
    const processedVideoSrc = response.data.path;
    processedAudios.value = [
      {
        id: audioIndex++,
        src: processedVideoSrc,
      },
    ];
  } catch (error) {
    console.error("音频上传失败:", error);
  }
};

const uploadedVideos = ref([]);
const processedVideos = ref([]);
let videoIndex = 0;

const uploadVideo = async (e) => {
  const files = e.target.files;
  for (let i = 0; i < files.length; i++) {
    const file = files[i];
    const videoSrc = URL.createObjectURL(file);
    uploadedVideos.value.push({ id: videoIndex++, src: videoSrc, file });
  }
};
const timer = ref(10);

const processVideos = async () => {
  const formData = new FormData();
  formData.append("audioPath", processedAudios.value[0].src);
  formData.append("timer", timer.value);
  for (const video of uploadedVideos.value) {
    formData.append("videos", video.file); // 使用实际的文件对象
  }
  try {
    const response = await axios.post(
      "http://localhost:3000/user/process",
      formData,
      {
        headers: {
          "Content-Type": "multipart/form-data",
        },
      }
    );
    const processedVideoSrc = response.data.path;
    processedVideos.value.push({
      id: videoIndex++,
      src: "http://localhost:3000/" + processedVideoSrc,
    });
  } catch (error) {
    console.error("视频处理失败:", error);
  }
};
</script>
<style lang="scss" scoped>
.inputStyle {
  padding-left: 20px;
  font-size: 20px;
  line-height: 2;
  border-radius: 20px;
  border: 1px solid #ccc;
}
</style>

后端代码

说明:

此案例的核心就是针对于视频的输出长度的问题。
我在接口中书写的视频混剪的逻辑是每个视频中抽取的素材都是等长的,这就涉及到一个问题,将时间平均(segmentLength)到每个素材上的时候,有可能素材视频的长度(length)要小于avaTime,这样的话就会导致从这样的素材中随机抽取视频片段的时候有问题。

我的解决方案是这样的:

首先对视频片段进行初始化的抽取,如果segmentLength>length的时候,就将整个视频作为抽取的片段传入,如果segmentLength<length的时候再进行从该素材中随机抽取指定的视频片段。

当初始化完毕之后发现初始化分配之后的视频长度(totalLength)<设置的输出视频长度(timer),则通过不断从剩余的视频素材中随机选择片段来填补剩余的时间,直到总长度达到目标长度为止。每次循环都会计算剩余需要填补的时间,并从随机选择的视频素材中截取一段合适的长度。

routers =》users.js
var express = require('express');
var router = express.Router();
const multer = require('multer');
const ffmpeg = require('fluent-ffmpeg');
const path = require('path');
const { spawn } = require('child_process')
// 视频
const upload = multer({
  dest: 'public/uploads/',
  storage: multer.diskStorage({
    destination: function (req, file, cb) {
      cb(null, 'public/uploads'); // 文件保存的目录
    },
    filename: function (req, file, cb) {
      // 提取原始文件的扩展名
      const ext = path.extname(file.originalname).toLowerCase(); // 获取文件扩展名,并转换为小写
      // 生成唯一文件名,并加上扩展名
      const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
      const fileName = uniqueSuffix + ext; // 新文件名
      cb(null, fileName); // 文件名
    }
  })
});
// 音频
const uploadVoice = multer({
  dest: 'public/uploadVoice/',
  storage: multer.diskStorage({
    destination: function (req, file, cb) {
      cb(null, 'public/uploadVoice'); // 文件保存的目录
    },
    filename: function (req, file, cb) {
      // 提取原始文件的扩展名
      const ext = path.extname(file.originalname).toLowerCase(); // 获取文件扩展名,并转换为小写
      // 生成唯一文件名,并加上扩展名
      const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
      const fileName = uniqueSuffix + ext; // 新文件名
      cb(null, fileName); // 文件名
    }
  })
});

const fs = require('fs');

// 处理文件上传
router.post('/upload', upload.single('video'), (req, res) => {
  const videoPath = req.file.path;
  const originalName = req.file.originalname;
  const filePath = path.join('uploads', originalName);
  fs.rename(videoPath, filePath, (err) => {
    if (err) {
      console.error(err);
      return res.status(500).send("Failed to move file.");
    }
    res.json({ message: 'File uploaded successfully.', path: filePath });
  });
});

// 处理单个视频文件
router.post('/single/process', upload.single('video'), (req, res) => {
  console.log(req.file)
  const videoPath = req.file.path;
  // 使用filename进行拼接是为了防止视频被覆盖
  const outputPath = `public/processed/reversed_${req.file.filename}`;

  ffmpeg()
    .input(videoPath)
    .outputOptions([
      '-vf reverse'// 反转视频帧顺序
    ])
    .output(outputPath)
    .on('end', () => {
      res.json({ message: 'Video processed successfully.', path: outputPath.replace('public', '') });
    })
    .on('error', (err) => {
      console.log(err)
      res.status(500).json({ error: 'An error occurred while processing the video.' });
    })
    .run();
});


// 处理多个视频文件上传
router.post('/process', upload.array('videos', 10), (req, res) => {
  // 要添加的背景音频
  const audioPath = path.join(path.dirname(__filename).replace('routes', 'public'), req.body.audioPath)
  //要生成多长时间的视频
  const { timer } = req.body

  // 格式化上传的音频文件的路径
  const videoPaths = req.files.map(file => path.join(path.dirname(__filename).replace('routes', 'public/uploads'), file.filename));

  // 输出文件路径
  const outputPath = path.join('public/processed', 'merged_video.mp4');
  // 要合并的视频片段文件
  const concatFilePath = path.resolve('public', 'concat.txt').replace(/\\/g, '/');//绝对路径

  // 创建 processed 目录(如果不存在)
  if (!fs.existsSync("public/processed")) {
    fs.mkdirSync("public/processed");
  }

  // 计算每个视频的长度
  const videoLengths = videoPaths.map(videoPath => {
    return new Promise((resolve, reject) => {
      ffmpeg.ffprobe(videoPath, (err, metadata) => {
        if (err) {
          reject(err);
        } else {
          resolve(parseFloat(metadata.format.duration));
        }
      });
    });
  });

  // 等待所有视频长度计算完成
  Promise.all(videoLengths).then(lengths => {
    console.log('lengths', lengths)
    // 构建 concat.txt 文件内容
    let concatFileContent = '';
    // 定义一个函数来随机选择视频片段
    function getRandomSegment(videoPath, length, segmentLength) {
      // 如果该素材的长度小于截取的长度,则直接返回整个视频素材
      if (segmentLength >= length) {
        return {
          videoPath,
          startTime: 0,
          endTime: length
        };
      }
      const startTime = Math.floor(Math.random() * (length - segmentLength));
      return {
        videoPath,
        startTime,
        endTime: startTime + segmentLength
      };
    }

    // 随机选择视频片段
    const segments = [];
    let totalLength = 0;

    // 初始分配
    for (let i = 0; i < lengths.length; i++) {
      const videoPath = videoPaths[i];
      const length = lengths[i];
      const segmentLength = Math.min(timer / lengths.length, length);
      const segment = getRandomSegment(videoPath, length, segmentLength);
      segments.push(segment);
      totalLength += (segment.endTime - segment.startTime);
    }
    console.log("初始化分配之后的视频长度", totalLength)

    /* 
      这段代码的主要作用是在初始分配后,如果总长度 totalLength 小于目标长度 targetLength,则通过不断从剩余的视频素材中随机选择片段来填补剩余的时间,直到总长度达到目标长度为止。每次循环都会计算剩余需要填补的时间,并从随机选择的视频素材中截取一段合适的长度。
    */
    // 如果总长度小于目标长度,则从剩余素材中继续选取随机片段
    while (totalLength < timer) {
      // 计算还需要多少时间才能达到目标长度
      const remainingTime = timer - totalLength;
      // 从素材路径数组中随机选择一个视频素材的索引
      const videoIndex = Math.floor(Math.random() * videoPaths.length);
      // 根据随机选择的索引,获取对应的视频路径和长度
      const videoPath = videoPaths[videoIndex];
      const length = lengths[videoIndex];

      // 确定本次需要截取的长度
      // 这个长度不能超过剩余需要填补的时间,也不能超过素材本身的长度,因此选取两者之中的最小值
      const segmentLength = Math.min(remainingTime, length);

      // 生成新的视频片段
      const segment = getRandomSegment(videoPath, length, segmentLength);
      // 将新生成的视频片段对象添加到片段数组中
      segments.push(segment);
      // 更新总长度
      totalLength += (segment.endTime - segment.startTime);
    }

    // 打乱视频片段的顺序
    function shuffleArray(array) {
      for (let i = array.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]];
      }
      return array;
    }

    shuffleArray(segments);

    // 构建 concat.txt 文件内容
    segments.forEach(segment => {
      concatFileContent += `file '${segment.videoPath.replace(/\\/g, '/')}'\n`;
      concatFileContent += `inpoint ${segment.startTime}\n`;
      concatFileContent += `outpoint ${segment.endTime}\n`;
    });

    fs.writeFileSync(concatFilePath, concatFileContent, 'utf8');

    // 获取视频总时长
    const totalVideoDuration = segments.reduce((acc, segment) => acc + (segment.endTime - segment.startTime), 0);
    console.log("最终要输出的视频总长度为", totalVideoDuration)

    // 获取音频文件的长度
    const getAudioDuration = (filePath) => {
      return new Promise((resolve, reject) => {
        const ffprobe = spawn('ffprobe', [
          '-v', 'error',
          '-show_entries', 'format=duration',
          '-of', 'default=noprint_wrappers=1:nokey=1',
          filePath
        ]);

        let duration = '';

        ffprobe.stdout.on('data', (data) => {
          duration += data.toString();
        });

        ffprobe.stderr.on('data', (data) => {
          console.error(`ffprobe stderr: ${data}`);
          reject(new Error(`Failed to get audio duration`));
        });

        ffprobe.on('close', (code) => {
          if (code !== 0) {
            reject(new Error(`FFprobe process exited with code ${code}`));
          } else {
            resolve(parseFloat(duration.trim()));
          }
        });
      });
    };
    getAudioDuration(audioPath).then(audioDuration => {
      // 计算音频循环次数
      const loopCount = Math.floor(totalVideoDuration / audioDuration);

      // 使用 ffmpeg 合并多个视频
      ffmpeg()
        .input(audioPath) // 添加音频文件作为输入
        .inputOptions([
          `-stream_loop ${loopCount}`, // 设置音频循环次数
        ])
        .input(concatFilePath)
        .inputOptions([
          '-f concat',
          '-safe 0'
        ])
        .output(outputPath)
        .outputOptions([
          '-y', // 覆盖已存在的输出文件
          '-c:v libx264', // 视频编码器
          '-preset veryfast', // 编码速度
          '-crf 23', // 视频质量控制
          '-map 0:a', // 选择第一个输入(即音频文件)的音频流
          '-map 1:v', // 选择所有输入文件的视频流(如果有)
          '-c:a aac', // 音频编码器
          '-b:a 128k', // 音频比特率
          '-t', totalVideoDuration.toFixed(2), // 设置输出文件的总时长为视频的时长
        ])
        .on('end', () => {
          const processedVideoSrc = `/processed/merged_video.mp4`;
          console.log(`Processed video saved at: ${outputPath}`);
          res.json({ message: 'Videos processed and merged successfully.', path: processedVideoSrc });
        })
        .on('error', (err) => {
          console.error(`Error processing videos: ${err}`);
          console.error('FFmpeg stderr:', err.stderr);
          res.status(500).json({ error: 'An error occurred while processing the videos.' });
        })
        .run();
    }).catch(err => {
      console.error(`Error getting audio duration: ${err}`);
      res.status(500).json({ error: 'An error occurred while processing the videos.' });
    });
  }).catch(err => {
    console.error(`Error calculating video lengths: ${err}`);
    res.status(500).json({ error: 'An error occurred while processing the videos.' });
  });

  // 写入 concat.txt 文件
  const concatFileContent = videoPaths.map(p => `file '${p.replace(/\\/g, '/')}'`).join('\n');
  fs.writeFileSync(concatFilePath, concatFileContent, 'utf8');
});

// 处理单个音频文件
router.post('/single/audio', uploadVoice.single('audio'), (req, res) => {
  const audioPath = req.file.path;
  console.log(req.file)
  res.send({
    msg: 'ok',
    path: audioPath.replace('public', '').replace(/\\/g, '/')
  })
})
module.exports = router;
routers =》 index.js
var express = require('express');
var router = express.Router();

router.use('/user', require('./users'));

module.exports = router;
app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// 使用cors解决跨域问题
app.use(require('cors')());

app.use('/', indexRouter);

// catch 404 and forward to error handler
app.use(function (req, res, next) {
  next(createError(404));
});

// error handler
app.use(function (err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++核心编程和桌面应用开发 第十一天(静态转换 动态转换 常量转换 重新解释转换)

目录 1.静态类型转换 1.1语法 1.2用法 2.动态类型转换 2.1语法 2.2用法 3.常量类型转换 3.1语法 3.2用法 4.重新解释转换 4.1语法 1.静态类型转换 1.1语法 static_cast<目标转换类型>(待转换变量) 1.2用法 可用于基本数据类型之间的转换。比如int和char之…

2.线段求交

1.线段求交 给定由平面上 n 条闭线段构成的一个集合 S&#xff0c;报告出 S 中各线段之间的所有交点。 我们所希望得到的算法&#xff0c;其运行时间不仅取决于输入中线段的数目&#xff0c;还取决于&#xff08;实际的&#xff09;交点数目。这样的算法&#xff0c;被称为“输…

网络爬虫-数美滑块验证码

仅供研究学习使用。 今天带来的是数美滑块验证码的逆向 目标站 --> 传送门 解决此类验证码 首先要解决滑动距离的判定 无论是使用selenium还是使用协议的方式来破解 都绕不开滑动距离的识别 滑动距离可以参考以前我博客上的方式&#xff0c;或者找一找开源的一些算法&am…

Collection 单列集合 List Set

集合概念 集合是一种特殊类 ,这些类可以存储任意类对象,并且长度可变, 这些集合类都位于java.util中,使用的话必须导包 按照存储结构可以分为两大类 单列集合 Collection 双列集合 Map 两种 区别如下 Collection 单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两…

Android:记录一个打包发布版的release包以后闪退的问题

个人感觉其实release闪退的问题挺难排查的&#xff0c;因为release包运行起来as捕获不到相应的应用程序进程&#xff0c;从而不易查看到日志&#xff0c;也是我玩得不溜&#xff0c;大家有不同的方法可以评论区探讨&#xff0c;我也定期回复一些评论一起讨论。以下是我遇到的情…

Scrapy | 使用Scrapy进行数据建模和请求

scrapy数据建模与请求 数据建模1.1 为什么建模1.2 如何建模1.3如何使用模板类1.4 开发流程总结 目标&#xff1a; 1.应用在scrapy项目中进行建模 2.应用构造Request对象&#xff0c;并发送请求 3.应用利用meta参数在不同的解析函数中传递数据 数据建模 | 通常在做项目的过程中…

标准IO练习及思维导图

1、完成标准io的单字符、字符串、格式化、模块化实现两个文件的拷贝&#xff1b; #include <myhead.h> typedef struct sockaddr_in addr_in_t; typedef struct sockaddr addr_t; typedef struct sockaddr_un addr_un_t; int main(int argc, const char *argv[]) {FILE*…

Kafka-设计思想-2

一、消息传递语义 现在我们对生产者和消费者的工作方式有了一些了解&#xff0c;让我们讨论一下Kafka在生产者和消费者之间提供的语义保证。 1、最多发送一次&#xff1a;会造成数据丢失 2、至少发送一次&#xff1a;会造成数据重复消费 3、只发送一次&#xff1a;我们想要的效…

docker 部署 vscode 远程开发环境(Go,Java)

1. 前言&#xff1a; 构建一个远程开发环境&#xff0c;一般来说开个linux云服务器是最好的&#xff0c;但是这里使用 docker 来搭建&#xff0c;docker 意味着更省资源&#xff0c;可以直接在一个 linux 主机上去设置 准备 一个安装了 docker 的主机&#xff0c;最好是linux&…

几何完备的3D分子生成/优化扩散模型 GCDM-SBDD - 评测

GCDM 是一个新的 3D 分子生成扩散模型&#xff0c;与之前的 EDM 相比&#xff0c;GCDM 优化了其中的图神神经网络部分&#xff0c;使用手性敏感的 SE3 等变神经网络 GCPNET 代替了 EDM 中的 EGNN&#xff0c;让节点间消息传递、聚合根据手性不同而进行。本文对 GCDM-SBDD&#…

制造企业数字化转型顶层规划案例(55页满分PPT)

基于集团的战略和运营特点&#xff0c;数字化转型应如何考虑&#xff1f; 在集团的战略和运营特点基础上进行数字化转型&#xff0c;需要实现业务多元化&#xff0c;整合资源和流程&#xff0c;推动国际化拓展&#xff0c;实施差异化战略&#xff0c;并通过数据驱动决策&#…

WPF开发一个语音转文字输入软件(一)

本文探索的Demo地址: https://gitee.com/lishuangquan1987/try_win32 https://github.com/lishuangquan1987/try_win32 后续会把他当做一个开源项目来维护 需求 开发一个软件&#xff0c;能够让用户说话来进行文字输入。具体如下&#xff1a; 像腾讯电脑管家那样的悬浮球悬浮…

Py之pygetwindow:pygetwindow的简介、安装和使用方法、案例应用之详细攻略

Py之pygetwindow&#xff1a;pygetwindow的简介、安装和使用方法、案例应用之详细攻略 目录 pygetwindow的简介 pygetwindow的安装和使用方法 pygetwindow的案例应用 1、使用了Windows系统打开了记事本应用程序&#xff0c;其窗口标题为“无标题 - 记事本” 2、Window对象…

STM32学习笔记---RTC

目录 一、什么是RTC 二、如何配置RTC 1、标准实时时钟部分(万年历部分) 1.1 时钟源分类 1.2 RTC时钟源的选择 1.3 精密校正 1.4 异步7位预分频器 1.5 粗略校正 1.6 同步15位分频 1.7 日历寄存器 1.8 RTC的初始化与配置 1.9 程序设计 2、闹钟部分 2.1 闹钟的初始化…

Python酷库之旅-第三方库Pandas(155)

目录 一、用法精讲 706、pandas.DatetimeTZDtype类 706-1、语法 706-2、参数 706-3、功能 706-4、返回值 706-5、说明 706-6、用法 706-6-1、数据准备 706-6-2、代码示例 706-6-3、结果输出 707、pandas.Timedelta.asm8属性 707-1、语法 707-2、参数 707-3、功能…

信息学CCF CSP-J/S 2024常见问题汇总,低年级考生重点关注

摘要 随着2024年CSP-J/S初赛的临近&#xff0c;各省报名要求细则陆续公布。为了帮助广大考生和家长准确了解各省政策&#xff0c;自主选拔在线团队特为汇总整理全国各省CSP-J/S2024认证相关问题&#xff0c;希望可以帮助各位考生更好的备考&#xff01; CCF CSP-J/S 2024 认证…

Android平台RTSP|RTMP播放器PK:VLC for Android还是SmartPlayer?

好多开发者&#xff0c;希望在Android端低延迟的播放RTMP或RTSP流&#xff0c;本文就目前市面上主流2个直播播放框架&#xff0c;做个简单的对比。 VLC for Android VLC for Android 是一款功能强大的多媒体播放器&#xff0c;具有以下特点和功能&#xff1a; 广泛的格式支持…

PDF-XChange PRO v10.4.2.390 x64 已授权中文特别版

PDF-XChange PRO是一款功能强大的PDF编辑和查看软件&#xff0c;PDF-XChange PRO 一个多合一的PDF解决方案。这是Tracker Software的三个最佳应用程序的套件&#xff1a;PDF-XChange Editor Plus&#xff0c;PDF-Tools和PDF-XChange Standard。使用 PDF-XChange Editor Plus&am…

vector的深入剖析与底层逻辑

前言&#xff1a; 上篇我们谈到vector的概念&#xff0c;使用&#xff0c;以及相关接口的具体应用&#xff0c;本文将对vector进行深入剖析&#xff0c;为读者分享其底层逻辑&#xff0c;讲解其核心细节。 上篇链接&#xff1a; 初始vector——数组的高级产物-CSDN博客 一.…

CDGA|数据治理:如何让传统行业实现数据智能

在当今这个数字化时代&#xff0c;数据已成为推动各行各业转型升级的关键力量。对于传统行业而言&#xff0c;如何从海量、复杂的数据中挖掘价值&#xff0c;实现“数据智能”&#xff0c;成为了提升竞争力、优化运营效率、创新业务模式的重要途径。本文将探讨数据治理如何助力…