音视频同步版本【基于音频】

其实和基于外部时钟的原理操作基本上一模一样。只不过音频帧不需要去匹配现实时钟了,只有视频帧需要匹配现实时钟。而视频帧需要去匹配音频帧的时间,那么就需要给时钟设置一个补偿,因为现在是以音频帧为标准。假如现在现实时钟到了50pts,而音频帧到了80pts,那么10ms后出现的视频帧匹配的时钟pts应该就是90pts【现实时钟的60pts加上补偿的30pts(30=80-50,这里是在读取音频帧的时候设置的补偿)】

#include <iostream>
#include <windows.h>
#include<queue>
#include<chrono>
#include<ctime>
#ifdef __cplusplus  ///
extern "C"
{
// 包含ffmpeg头文件
#include "libavutil/avutil.h"
#include"libavformat/avformat.h"
#include"libswscale/swscale.h"
#include"libswresample/swresample.h"
// 包含SDL头文件
#include"SDL.h"
}
#endifusing namespace std;class AVSync{
public:AVSync() {}void init(){start_time = getNowMilliseconds();}// 获取当前时间的pts应该是多少了int getPts() {/* +上drift代表补偿偏差的时间,如果drift大于0,代表当前时间比音频时间慢了,所以实际上pts要更大才对如果小于0,则相反*/return getNowMilliseconds() - start_time + drift;}// 设置音频pts与现实时钟的偏差// pts_单位是秒void setClock(double pts_) {// 现实时钟的ptsint real_pts = getNowMilliseconds() - start_time;// 更新偏差值drift = pts_ * 1000 - real_pts;//音频比现实时钟快了多少}// 毫秒时间戳。【获取1970年到现在过去了多少微秒,例如:1672531199876】Uint64 getNowMilliseconds() {return getNowMicroseconds() / 1000;}// 微秒时间戳。【获取1970年到现在过去了多少微秒,例如:1672531199876543】Uint64 getNowMicroseconds() {using namespace std::chrono;//system_clock::time_point time_point_now = system_clock::now();system_clock::duration duration = time_point_now.time_since_epoch();return duration_cast<microseconds>(duration).count();}private:// 音视频播放启动的时间--毫秒时间戳Uint64 start_time = 0;// 音频pts与现实时钟的偏差【毫秒值,而不是时间戳】int drift = 0;
};// 线程停止运行标识,0为正在运行,1为停止
int thread_exit = 0;
// 当前帧音频PCM数据
static Uint8 *audio_pcm_g;
// 当前帧音频PCM数据的字节总大小长度
static Uint32 audio_len_g;
// 音视频帧队列
queue<AVFrame*> audio_frame_queue_;
queue<AVFrame*> video_frame_queue_;
// 将main方法中的变量提取到全局以供两个线程函数中使用
AVFormatContext *input_fmt_ctx = NULL;
int video_idx = -1;
int audio_idx = -1;
AVCodecContext *audio_codec_ctx;
// 音视频同步工具类
AVSync sync_;// 输出错误信息
void showError(int ret, const char *methodName = "method")
{if(ret == 0) {return ;}// 错误消息日志char err2str[256];// 将返回结果转化为字符串信息av_strerror(ret, err2str, sizeof(err2str));printf("%s failed, ret:%d, msg:%s\n", methodName, ret, err2str);
}// 填充PCM数据到SDL中
void fill_audio_pcm(void *udata, Uint8 *stream, int len) {// 清空上一帧的数据SDL_memset(stream, 0, len);// 如果外部线程【主线程读帧】还未读取到数据,那么无法填充PCM到SDL中进行播放if(audio_len_g == 0){return ;}// 本次回调结束最多只能取len字节的数据// 如果外部读取的帧小于len字节,那么直接填充外部读取到的所有数据即可// 如果外部读取的帧大于len字节,那么本次填充len字节的数据,等下次回调再填充 audio_len_g - len字节的数据// 【如果audio_len_g - len 还是大于了len字节,那么继续取len填充即可】len = len > audio_len_g ? audio_len_g : len;//填充PCM数据到SDL中SDL_MixAudio(stream, audio_pcm_g, len, SDL_MIX_MAXVOLUME/2);// SDL_MIX_MAXVOLUME/2 为音频大小,在0-128之间调整// 更新pcm内存指针指向位置,已经【又】使用了len个字节空间,那么下次需要从当前位置+len的位置开始使用audio_pcm_g += len;// 更新剩余字节大小数量,已经读取了len个字节大小的数据,那么下次还剩 audio_len_g - len 个字节大小的数据可以使用audio_len_g -= len;}// 视频播放线程
int play_video_thread(void *opaque) {// SDL// 初始化视频if(SDL_Init(SDL_INIT_VIDEO)) {return -1;}// 视频宽度int video_width_ = input_fmt_ctx->streams[video_idx]->codecpar->width;// 视频高度int video_height_ = input_fmt_ctx->streams[video_idx]->codecpar->height;// 创建窗口--显示器// 在这里设置显示出来的窗口的总大小SDL_Window *win_ = SDL_CreateWindow("苏花末测试窗口", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,video_width_, video_height_, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);if(!win_) {return -1;}// 渲染器,用于将纹理渲染到窗口上SDL_Renderer *renderer_ = SDL_CreateRenderer(win_, -1, 0);if(!renderer_) {return -1;}// 纹理,用于设置渲染图片数据SDL_Texture *texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_IYUV,  SDL_TEXTUREACCESS_STREAMING, video_width_, video_height_);if(!texture_) {return -1;}// Rect--页面显示区域SDL_Rect rect_;// 刷新事件队列【防止有缓存】SDL_PumpEvents();SDL_Event event;// 线程运行中while(thread_exit == 0){// 在没有事件的情况下才能刷新页面if(SDL_PollEvent(&event) != 0) {continue;}AVFrame *frame = video_frame_queue_.front();// 帧的相对时间过去了多久【单位:1/刻度 秒】double pts = frame->pts * av_q2d(input_fmt_ctx->streams[video_idx]->time_base);printf("video.pts: %f ; now.pts: %f\n", pts, sync_.getPts() / 1000.0);pts = pts * 1000;//转换毫秒// 当前帧如果实际上应该播放的时间超过了当前时间,则代表当前帧应该在未来播放,现在不能播放// 故在这里等待时间if(pts > sync_.getPts()){SDL_Delay(pts - sync_.getPts());//等待时间直到时间到了pts,那么才播放// 这里可以使用continue,或者也可以直接向下运行,加个continue方便盘逻辑continue;}// 将当前帧移除待播放队列,因为这一帧马上就播放了video_frame_queue_.pop();// 如果当前帧已经延迟了500ms了,那么丢弃该帧,直接播放下一帧if(pts < sync_.getPts() - 500){continue;}// SDL: output// 清空之前的页面SDL_RenderClear(renderer_);// 设置rect所占区域rect_.x = 0;rect_.y = 0;// 在这里设置rect区域的大小,如果这里和窗口总大小不一样,那么其他地方是黑屏显示// 故这里也体现了一个win可以设置多个rect,每个rect可以占据不同的位置rect_.w = video_width_;rect_.h = video_height_;// 通过YUV格式渲染图片SDL_UpdateYUVTexture(texture_, &rect_,frame->data[0], frame->linesize[0],frame->data[1], frame->linesize[1],frame->data[2], frame->linesize[2]);// 页面内容设置SDL_RenderCopy(renderer_, texture_, NULL, &rect_);// 显示新的页面SDL_RenderPresent(renderer_);// 释放内存av_frame_free(&frame);}return 0;
}// 音频播放线程
int play_audio_thread(void *opaque) {int ret = 0;// 初始化音频if(SDL_Init(SDL_INIT_AUDIO)) {return -1;}// 音频播放上下文,音频播放只能通过这个结构体进行操作// 创建 SwrContext 只能使用 swr_alloc() 函数SwrContext *swrContext = swr_alloc();if(!swrContext){cout << "初始化swrContext对象失败" << endl;return -1;}// 设置具体参数来创建 SwrContext对象/* channel布局:如立体声、5.1声道、单声道等* 采样格式:不同音频格式的采样格式不同,如AAC的采样格式是 AV_SAMPLE_FMT_FLTP,*      而MP3的采样格式是 AV_SAMPLE_FMT_S16P* 采样率:一秒钟采集多少次样本* */swrContext = swr_alloc_set_opts(NULL,   //是否需要继承一个存在的SwrContext的内容AV_CH_LAYOUT_STEREO, //输出的channel布局AV_SAMPLE_FMT_S16, //输出的采样格式44100, //输出的采样率av_get_default_channel_layout(audio_codec_ctx->channels), //输入的channel布局audio_codec_ctx->sample_fmt, //输入的采用格式audio_codec_ctx->sample_rate, //输入的采用率0,NULL);/* 为什么要重采样?* 是因为输入的音频可能是mp4格式的,但是我们的电脑只能播放avi格式的音频,*  所以需要转换数据,转换为确保我们的电脑一定能播放的格式。* */// 初始化重采样上下文ret = swr_init(swrContext);// 初始化重采样失败,那么音频无法播放if(ret < 0){cout << "初始化重采样上下文失败" << endl;return -1;}// SDL_AudioSpc 是音频播放参数的结构体// 期望能够实现的音频参数SDL_AudioSpec wanted_spec;wanted_spec.freq = 44100; //期望的采样率wanted_spec.format = AUDIO_S16SYS; //期望的采样格式wanted_spec.channels = 2; //期望的通道格式wanted_spec.silence = 0; //期望中静音大小的值wanted_spec.samples = 1024; //期望中一帧的数据大小,即样本数wanted_spec.callback = fill_audio_pcm;//播放音频时会开启一个线程,反复调用这个回调函数,用来给音频填充PCMwanted_spec.userdata = audio_codec_ctx; //回调函数中第一个参数的对象// 按照指定参数打开真实的物理设备ret = SDL_OpenAudio(&wanted_spec, NULL);if(ret < 0){cout << "打开音频设备失败" << endl;return -1;}// 开始播放音频SDL_PauseAudio(0);// 分配输出音频数据Uint8 *out_buffer = nullptr;// 线程运行中while(thread_exit == 0){// 如果队列为空,则等待帧if(audio_frame_queue_.empty()){SDL_Delay(1);continue;}AVFrame *frame = audio_frame_queue_.front();// 帧的相对时间过去了多久【单位:1/刻度 秒】double pts = frame->pts * av_q2d(input_fmt_ctx->streams[audio_idx]->time_base);// 更新时钟【音频为基准】sync_.setClock(pts);// 将当前帧移除待播放队列,因为这一帧马上就播放了audio_frame_queue_.pop();// 获取输入的样本数int in_samples = frame->nb_samples;// 目标样本数【想要输出的样本数】int dst_samples = av_rescale_rnd(in_samples, wanted_spec.freq, frame->sample_rate, AV_ROUND_UP);// 计算需要输出的样本数内存空间大小int out_buffer_size = av_samples_get_buffer_size(NULL, wanted_spec.channels,dst_samples, AV_SAMPLE_FMT_S16, 0);// 如果输出的音频数据未开辟过空间,那么开辟空间if(!out_buffer){// 输出数据的空间大小即为计算出来需要输出的样本数大小out_buffer = (Uint8 *)av_malloc(out_buffer_size);}// 返回每个通道需要输出的样本数,错误时返回负值int sample_count = swr_convert(swrContext, &out_buffer, dst_samples,(const Uint8 **)frame->data, in_samples);// frame->data 即为采样到的数据// 释放内存av_frame_free(&frame);// 获取不到样本数了,那么进行下一个包数据的读取if(sample_count < 0){break;}// 计算这一帧的字节数大小/长度int out_size = sample_count * wanted_spec.channels *av_get_bytes_per_sample(AV_SAMPLE_FMT_S16);// 如果回调函数中的字节数还未处理完,那么不能进行下一个音频帧的处理while(audio_len_g > 0)SDL_Delay(1);// 回调函数中的字节已经处理完了,那么可以填充下一个音频帧需要的数据了// 这一帧的字节长度audio_len_g = out_size;// 填充pcm数据audio_pcm_g = (Uint8 *)out_buffer;}return 0;
}// 将帧写入队列
void push_frame(queue<AVFrame*> &queue_, AVFrame *frame_) {AVFrame *frame = av_frame_alloc();av_frame_move_ref(frame, frame_);queue_.push(frame);
}#undef main
int main(int argc, char *argv[])
{SetConsoleOutputCP(CP_UTF8);if(argc < 2){cout << "请输入视频地址" << endl;return -1;}// 获取视频地址char *url = argv[1];// 方法调用结果int ret = 0;// FFmpeg// AVFormatContext 是音视频开发使用到最多的结构体,无论什么函数基本上都会用到它// AVFormatContext 只能通过 avformat_alloc_context() 创建空的对象input_fmt_ctx = avformat_alloc_context();// 加载视频内容到音视频格式上下文中ret = avformat_open_input(&input_fmt_ctx, url, NULL, NULL);// 输出日志showError(ret);// 查看流信息,可以不写,只是单纯拿返回值来做校验的ret = avformat_find_stream_info(input_fmt_ctx, NULL);// 输出日志showError(ret);// 输出视频信息,可以不写av_dump_format(input_fmt_ctx, 0, url, 0);// 查找指定流的idx,如果使用不到,可以不写; AVMEDIA_TYPE_VIDEO 代表视频流,AVMEDIA_TYPE_AUDIO代表音频流video_idx = av_find_best_stream(input_fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);audio_idx = av_find_best_stream(input_fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);printf("video_idx: %d , audio_idx: %d\n", video_idx, audio_idx);// AVCodecContext 是解码器上下文,需要对帧处理基本上都会用到它// AVCodecContext 只能通过 avcodec_alloc_context3(NULL) 创建空的对象// 视频解码器上下文AVCodecContext *video_codec_ctx = avcodec_alloc_context3(NULL);// 将音视频格式上下文中的参数加载到解码器上下文对象中ret = avcodec_parameters_to_context(video_codec_ctx, input_fmt_ctx->streams[video_idx]->codecpar);// 输出日志showError(ret);// 指定物理解码器;这里参数传的是codec_ctx->codec_id,实际上物理解码器有很多中,这里可以传不同的内容AVCodec *video_codec = avcodec_find_decoder(video_codec_ctx->codec_id);// 将物理解码器加载到解码器上下文中ret = avcodec_open2(video_codec_ctx, video_codec, NULL);// 音频解码器上下文audio_codec_ctx = avcodec_alloc_context3(NULL);// 将音视频格式上下文中的参数加载到解码器上下文对象中ret = avcodec_parameters_to_context(audio_codec_ctx, input_fmt_ctx->streams[audio_idx]->codecpar);// 输出日志showError(ret);// 指定物理解码器;这里参数传的是codec_ctx->codec_id,实际上物理解码器有很多中,这里可以传不同的内容AVCodec *audio_codec = avcodec_find_decoder(audio_codec_ctx->codec_id);// 将物理解码器加载到解码器上下文中ret = avcodec_open2(audio_codec_ctx, audio_codec, NULL);// 输出日志showError(ret);// 包,用来获取音视频格式上下文中的数据// AVPacket 只能通过 av_packet_alloc() 创建对象AVPacket pkt;// 开启播放线程SDL_CreateThread(play_video_thread, NULL, NULL);SDL_CreateThread(play_audio_thread, NULL, NULL);// 设置时钟sync_.init();// output and readFramewhile(1){// printf("video_queue.size: %d ; audio_queue.size: %d\n", video_frame_queue_.size(), audio_frame_queue_.size());// 防止读取内存过大if(video_frame_queue_.size() >= 100 || audio_frame_queue_.size() >= 100){SDL_Delay(1);continue;}// FFmpeg: readFrame// 获取该音视频格式上下文中的第一个包,并将从音视频格式上下文中移除// 则代表了每次调用都会获取到新的包,之前的包不会再在该音视频格式上下文中找到了ret = av_read_frame(input_fmt_ctx, &pkt);// 如果包数据读取完毕,则代表视频播放结束了if(ret < 0){cout << "play video finish" << endl;break;}// AVFrame 只能通过 av_frame_alloc() 创建对象AVFrame *frame = av_frame_alloc();// 音频帧if(pkt.stream_index == audio_idx){// 将包加载到解码器上下文中进行解码ret = avcodec_send_packet(audio_codec_ctx, &pkt);// 对应音频的包数据来说,一次包读取,可以获取到多个framewhile(1){// 读取解码后的包中的帧ret = avcodec_receive_frame(audio_codec_ctx, frame);// 如果 AVERROR(EAGAIN) == ret,则代表这个包无法获取到帧,需要再次加载下一个包配合解析帧// 如果所有的帧都读取完成了,那么开始读取下一个包if(ret == AVERROR(EAGAIN)){break;}// 将帧添加到队列中push_frame(audio_frame_queue_, frame);}}// 视频帧else if(pkt.stream_index == video_idx){// 将包加载到解码器上下文中进行解码ret = avcodec_send_packet(video_codec_ctx, &pkt);// 读取解码后的包中的帧ret = avcodec_receive_frame(video_codec_ctx, frame);// 如果 AVERROR(EAGAIN) == ret,则代表这个包无法获取到帧,需要再次加载下一个包配合解析帧if(AVERROR(EAGAIN) == ret){continue;}// 将帧添加到队列中push_frame(video_frame_queue_, frame);}// 释放内存av_packet_unref(&pkt);}// 加载帧完成了,现在需要等待所有帧播放完毕while(!video_frame_queue_.empty() || !audio_frame_queue_.empty()){printf("video_queue.size: %d ; audio_queue.size: %d --wait_over\n", video_frame_queue_.size(), audio_frame_queue_.size());SDL_Delay(10);}// 标记线程结束了thread_exit = 1;system("pause");return 0;
}

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

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

相关文章

Catalan数 C++解决

输入描述 输入一个正整数n。 输出描述 输出Catalan数的前n项。 用例输入 1 0 用例输出 1 1 用例输入 2 5 用例输出 2 1 1 2 5 14 42 #include<bits/stdc.h> using namespace std; int main() {int n;cin>>n;int dp[n1]{0};dp[0]dp[1]1;for(int m2;…

守护头顶安全——AI高空抛物监测,让悲剧不再重演

在城市的喧嚣中&#xff0c;我们享受着高楼林立带来的便捷与繁华&#xff0c;却往往忽视了那些隐藏在高空中的危险。近日&#xff0c;震惊全国的高空抛物死刑案件被最高院核准并执行。案件中被告人多次高空抛物的举动&#xff0c;夺去了无辜者的生命&#xff0c;也让自己付出了…

项目集群部署定时任务重复执行......怎么解决???

项目集群部署在不同服务器&#xff0c;导致定时任务重复执行 1、可以在部署时只让一个服务器上有定时任务模块&#xff0c;不过这样如果这台服务器宕机&#xff0c;就会导致整个定时任务崩溃 2、使用分布式锁&#xff0c;使用redis setNX命令加lua脚本在定时任务执行的时候只…

Go 语言中的 for range 循环教程

在 Go 语言中&#xff0c;for range 循环是一个方便的语法结构&#xff0c;用于遍历数组、切片、映射和字符串。本教程将通过示例代码来帮助理解如何在 Go 中使用 for range 循环。 package mainimport "fmt"func main() {// 遍历切片并计算和nums : []int{2, 3, 4}…

Kafka-代码示例

一、构建开发环境 File > New > Project 选择一个最简单的模板 项目和坐标命名 配置maven路径 添加maven依赖 <dependencies><!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients --><dependency><groupId>org.apache.kaf…

深度学习 基本函数01

np.dot 是 NumPy 库中的一个函数&#xff0c;用于计算两个数组的点积&#xff08;也称为内积或数量积&#xff09;。点积是两个向量的对应元素乘积之和。 np.random.normal 是 NumPy 库中的一个函数&#xff0c;用于生成符合正态分布&#xff08;也称为高斯分布&#xff09;的…

项目管理软件中这6个小技巧帮助项目经理同时管理多个项目

在网上看到一个数据&#xff0c;只有15%的项目经理一次只需要负责一个项目&#xff0c;其他的项目经理都需要同时负责多个项目&#xff0c;甚至有15%的项目经理一次需要负责10个以上的项目。 我在工作中&#xff0c;也只有很少很少的时间里&#xff0c;是一次性只负责一个项目…

目标检测——yolov5-3.1的环境搭建和运行

第一步&#xff1a;安装anaconda环境&#xff0c;并且配置好cuda&#xff0c;安装需要的基本包 查看对应cuda版本&#xff0c;后续下载cudatoolkit需要对应版本 nvcc -V 第二步&#xff1a;创建虚拟环境&#xff0c;激活环境&#xff0c;安装所需的包 conda create -n yolo…

Spark实现PageRank算法

详细步骤&#xff1a; 1、创建Spark sql 环境 2、读取数据 3、数据切分 &#xff08;分为page列&#xff0c;outLink列&#xff09;形成表 pageDF 4、新增pr一列 (给定初始值) 形成表 initPrDF 5、新增avgPr一列&#xff08;根据出链关系&#xff0c;求每个页面所分到…

Visual studio 下载安装

1&#xff0c;Visual stutdio 网址 下载 Visual Studio Tools - 免费安装 Windows、Mac、Linux 2&#xff0c;下划页面&#xff0c;点击 较早的下载 3&#xff0c;选择对应的版本进行下载

《深度学习》YOLO v1网络架构 、损失值、NMS极大值抑制

目录 一、Yolo系列v1 1、核心思想 2、示例 3、流程图解析 二、YOLO系列v1损失函数 1、位置误差 2、置信度误差 3、类别概率损失 三、NMS非极大值抑制 1、概念 2、步骤 四、YOLO v1优缺点 1、优点 1&#xff09;速度快 2&#xff09;端到端 3&#xff09;多尺度…

docker 可用镜像服务地址(2024.10.25亲测可用)

1.错误 Error response from daemon: Get “https://registry-1.docker.io/v2/” 原因&#xff1a;镜像服务器地址不可用。 2.可用地址 编辑daemon.json&#xff1a; vi /etc/docker/daemon.json内容修改如下&#xff1a; {"registry-mirrors": ["https://…

Python爬虫:从入门到精通

Python爬虫&#xff1a;从入门到精通 在数字时代&#xff0c;信息就如同水源&#xff0c;源源不绝。然而&#xff0c;当你想要从海量的信息中汲取有价值的“水”&#xff0c;你会发现这并不是一件容易的事。这就是为什么网络爬虫出现了。它们帮助我们在网络的海洋中航行&#…

光伏业务管理软件:提升企业管理效率的利器

一、优化业务流程 光伏业务管理软件能够对企业的各项业务流程进行全面梳理和优化。从项目前期的规划设计、设备采购&#xff0c;到项目建设中的施工管理、质量控制&#xff0c;再到项目后期的运维服务&#xff0c;软件都可以进行有效的跟踪和管理。 通过规范业务流程&#xf…

力扣第23题:合并K个升序链表

详解力扣第23题&#xff1a;合并K个升序链表 题目描述 给你一个链表数组&#xff0c;每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中&#xff0c;返回合并后的链表。 本题可以通过优先队列-最小堆来高效解决&#xff0c;因为我们需要频繁地找到当前K个链表中…

银行客户贷款行为数据挖掘与分析

#1024程序员节 | 征文# 在新时代下&#xff0c;消费者的需求结构、内容与方式发生巨大改变&#xff0c;企业要想获取更多竞争优势&#xff0c;需要借助大数据技术持续创新。本文分析了传统商业银行面临的挑战&#xff0c;并基于knn、逻辑回归、人工神经网络三种算法&#xff0…

2024 10.25 判断一个矩阵是否对称

主对角线对称 思路&#xff1a;a[i][j]!a[j][i] 第一行和第一列顺序比较&#xff0c;后面依次类推 #include <stdio.h>int main(){int n,m;scanf("%d",&n);int a[n][n];for(int i0;i<n;i){for(int j0;j<n;j)scanf("%d",&a[i][j]);}i…

Spring Boot框架下中小企业设备管理系统开发

1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及&#xff0c;互联网成为人们查找信息的重要场所&#xff0c;二十一世纪是信息的时代&#xff0c;所以信息的管理显得特别重要。因此&#xff0c;使用计算机来管理中小企业设备管理系统的相关信息成为必然。…

python的Django的render_to_string函数和render函数模板的使用

一、render_to_string render_to_string 是 Django 框架中的一个便捷函数&#xff0c;用于将模板渲染为字符串。 render_to_string(template_name.html, context, requestNone, usingNone) template_name.html&#xff1a;要渲染的模板文件的名称。context&#xff1a;传递给…

epub转为txt

使用Python通过ebooklib和BeautifulSoup等库将epub文件转换为txt文件。下 1.安装必要的依赖库&#xff1a; pip install ebooklib beautifulsoup4 lxmlimport ebooklib from ebooklib import epub from bs4 import BeautifulSoup import os# 读取epub文件并转换为txt文件 def …