java实现局域网内视频投屏播放(二)爬虫
代码链接
视频播放原理
大多视频网站使用的是m3u8,m3u8其实不是一个真正的视频文件,而是一个视频播放列表(playlist)。它是一种文本文件,里面记录了一系列的视频片段(segment)的网络地址。这些视频片段通常是ts格式的,也就是传输流(transport stream)格式。ts格式的视频片段可以很快地在网络上传输和播放,而不需要等待整个文件下载完毕。这样就可以实现流媒体(streaming media)的效果,也就是边下边播。
m3u8是苹果公司提出的一种流媒体协议,叫做HTTP Live Streaming(HLS)。HLS的目的是为了解决在不同网络环境下,如何提供更好的视频观看体验的问题。
HLS的原理是把一个完整的视频切分成很多小的视频片段,并且为每个片段提供不同的码率(bitrate)和分辨率(resolution)的选项。这样,当用户观看视频时,可以根据自己的网络状况和设备性能,自动或者手动地选择合适的视频片段进行播放。这样就可以避免卡顿、缓冲、画质模糊等问题,提高用户满意度。
爬虫实现原理
所以对于爬虫来说,需要获取m3u8文件,再根据m3u8里面记录的ts链接列表去下载ts文件(视频片段)。将这些ts保存到本地磁盘中。
视频解析器接口
public interface VideoResolver {/*** 是否支持解析** @param url 地址* @return boolean*/boolean support(String url);/*** 获取m3u8文件** @param url 地址* @return Result*/M3U8BO getM3U8(String url);/*** 获取ts地址列表** @param m3u8BO m3u8内容* @return Result*/TsListBO getTsList(M3U8BO m3u8BO);/*** 获取解密ts的方法** @param head m3u8文件头* @param encKey 加密的key* @return UnaryOperator*/default UnaryOperator<byte[]> getDecodeTsFunction(String head, byte[] encKey) {return null;}
}
通用视频解析器抽象类
一般来说m3u8的链接保存在播放页面里面。可以通过查看源代码找到,只要写一个正则表达式就能取到链接,然后通过该链接获取m3u8内容和ts下载链接等内容,所以将这些公共的代码封装到一个一个抽象类中,并新增两个抽象方法,获取解析m3u8的正则模式和获取解析视频名的正则模式。
@Slf4j
public abstract class CommonVideoResolver implements VideoResolver {private static final Pattern tsContentPat = Pattern.compile("(#EXTINF.*?)\n(.*?\\.ts)");public static final Pattern encKeyPat = Pattern.compile("#EXT-X-KEY.*?URI=\"(.*?)\"[^\n]*");private static final Pattern m3u8ContentPat = Pattern.compile("([\\s\\S]*?)(#EXTINF[\\s\\S]*\\.ts)([\\s\\S]*)");/*** 获取解析m3u8的正则模式** @return Pattern*/protected abstract Pattern getM3U8UrlPat();/*** 获取解析视频名的正则模式** @return Pattern*/protected abstract Pattern getM3U8NamePat();@Overridepublic M3U8BO getM3U8(String url) {HttpRespBO respBO = HttpUtil.httpGet10(url);Assert.isTrue(respBO, "获取M3U8文件失败", () ->log.error("xcVideoService getM3U8 fail url:{}", url));String res = respBO.getUTF8Body();Matcher matcher = getM3U8UrlPat().matcher(res);Assert.isTrue(matcher.find(), "未获取到M3U8地址");String m3u8Url = matcher.group(1);HttpRespBO m3u8Resp = HttpUtil.httpGet10(m3u8Url);Assert.isTrue(m3u8Resp, "获取M3U8列表文件失败", () ->log.error("xcVideoService getM3U8 list fail m3u8Url:{},m3u8Resp:{}", m3u8Url, m3u8Resp));String m3u8Content = m3u8Resp.getUTF8Body();m3u8Content = HLSUtil.getMaxBandwidthM3U8(m3u8Url, m3u8Content);Matcher m3u8NameMatcher = getM3U8NamePat().matcher(res);String title = Optional.of(m3u8NameMatcher).filter(Matcher::find).map(m -> m.group(1)).orElse(null);String id = UUID.randomUUID().toString().replace("-", "");return new M3U8BO(id, title, m3u8Content, url, m3u8Url);}@Overridepublic TsListBO getTsList(M3U8BO m3u8BO) {String videoId = m3u8BO.getId();String m3u8Url = m3u8BO.getM3u8Url();String m3u8Content = m3u8BO.getContent();Matcher m3u8Matcher = m3u8ContentPat.matcher(m3u8Content);Assert.isTrue(m3u8Matcher.find(), "m3u8内容解析失败");String head = m3u8Matcher.group(1);StringBuilder newHead = new StringBuilder(head);TsEncBO tsEncBO = buildTsEnc(videoId, newHead, m3u8Url);List<TsBO> tsList = new ArrayList<>();String domain = NetUtil.resolveRootUrl(m3u8Url);Matcher tsMatcher = tsContentPat.matcher(m3u8Matcher.group(2));while (tsMatcher.find()) {String url = tsMatcher.group(2);tsList.add(new TsBO(composeUrl(domain, url), tsMatcher.group(1)));}TsListBO tsListBO = new TsListBO();tsListBO.setTsList(tsList);tsListBO.setTsEncBO(tsEncBO);tsListBO.setHead(newHead.toString());tsListBO.setEnd(m3u8Matcher.group(3));return tsListBO;}private String composeUrl(String domain, String url) {if (url.startsWith("http")) {return url;}return url.startsWith("/") ? domain + url : domain + "/" + url;}private TsEncBO buildTsEnc(String videoId, StringBuilder newHead, String m3u8Url) {TsEncBO tsEncBO = null;String head = newHead.toString();Matcher encKeyMatcher = encKeyPat.matcher(head);if (encKeyMatcher.find()) {String originEncKeyUrl = encKeyMatcher.group(1);String encKeyUrl;if (originEncKeyUrl.startsWith("http")) {encKeyUrl = originEncKeyUrl;} else if (originEncKeyUrl.startsWith("/")) {encKeyUrl = NetUtil.resolveRootUrl(m3u8Url) + originEncKeyUrl;} else {encKeyUrl = m3u8Url.substring(0, m3u8Url.lastIndexOf("/") + 1) + originEncKeyUrl;}HttpRespBO encKeyResp = HttpUtil.httpGet10(encKeyUrl);Assert.isTrue(encKeyResp, "获取ts文件密钥失败", () ->log.error("getEncKey fail encKeyUrl:{},encKeyResp:{}", encKeyUrl, encKeyResp));newHead.setLength(0);newHead.append(head.replace(originEncKeyUrl, "/video/enc/key/" + videoId));byte[] encKey = encKeyResp.getBody();tsEncBO = new TsEncBO();tsEncBO.setEncKey(encKey);tsEncBO.setEncKeyUrl(encKeyUrl);tsEncBO.setOriginEncKeyUrl(originEncKeyUrl);}return tsEncBO;}
}
如果想新增一个网站的解析,只需要继承这个抽象类,提供两个正则表达式即可。当然对于前后端分离的网站,就不能使用这个抽象类了,需要自己根据m3u8的接口新增一个公用的抽象类(等遇到了再实现),获取m3u8和ts的内容了(实现VideoResolver接口)
@Slf4j
@Service("ccVideoResolver")
public class CCVideoResolver extends CommonVideoResolver {private static final Pattern m3u8NamePat = Pattern.compile("<title>(.*?)</title>");private static final Pattern m3u8UrlPat = Pattern.compile("player_data=\\{.*?\"url\":\"(.*?)\"");@Overridepublic boolean support(String url) {return url != null && url.contains("www.nxyjjt.com");}@Overrideprotected Pattern getM3U8UrlPat() {return m3u8UrlPat;}@Overrideprotected Pattern getM3U8NamePat() {return m3u8NamePat;}
}
ts文件解密
有的ts文件是加密的,播放的时候需要根据m3u8上的加密方式和加密的key去解密ts文件,如m3u8文件中有一行为 #EXT-X-KEY:METHOD=AES-128,URI="key.key",IV=0x864267cc19f34ec1066e016e0da856ee。对于这种情况我们有两种处理方案
- 将ts文件解密后再保存到本地,这种保存到本地的ts文件可以直接播放,将这些ts文件直接拼接成一个大的ts文件后,就是完整的视频。并且需要将要提供的m3u8文件中的 #EXT-X-KEY:METHOD=xxx 这一行删掉,代表这个视频没有进行过加密。
- 不解密直接保存,这种ts文件不能直接播放。并将加密的key也保存下来,需要将要提供的m3u8文件中的 #EXT-X-KEY:METHOD=xxx 这一行URI="xxx“中的地址改为自己服务获取密钥的地址,让投屏设置在投屏时候根据这个链接获取密钥,在播放过程中解密ts文件
第一种方式下载相对慢一些但是播放很快,因为下载需要解密,播放无需解密。第二种方式则相反。注意这个慢是相对的,因为在局域网内投屏,最耗时的步骤已经解决,无论哪一种方式都不会卡
可以看到VideoResolver接口中提供了一个getDecodeTsFunction方法,返回一个解密方法UnaryOperator,默认是返回null即不解密,如果想解密的话,可以根据m3u8里的解密方式重写getDecodeTsFunction方法
@Slf4j
@Service("xcVideoResolver")
public class XCVideoResolver extends CommonVideoResolver {private static final Pattern m3u8UrlPat = Pattern.compile("\"url\":\"(.*?)\"");private static final Pattern m3u8NamePat = Pattern.compile("<title>(.*?)</title>");private static final Pattern encKeyPat = Pattern.compile("#EXT-X-KEY:METHOD=(.*?),.*?IV=(.*?)\n");@Overridepublic boolean support(String url) {return url != null && (url.contains("www.huidongxie.com") || url.contains("www.wszwz.net"));}@Overrideprotected Pattern getM3U8UrlPat() {return m3u8UrlPat;}@Overrideprotected Pattern getM3U8NamePat() {return m3u8NamePat;}@Overridepublic UnaryOperator<byte[]> getDecodeTsFunction(String head, byte[] encKey) {Matcher encKeyMatcher = encKeyPat.matcher(head);if (encKeyMatcher.find()) {String method = encKeyMatcher.group(1);return method.contains("aes") || method.contains("AES") ?encByte -> AESUtil.decode(encByte, encKey, "0000000000000000") : null;}return null;}
}
通用视频下载执行器
当获取完ts的下载列表后,就需要将ts下载下来并且保存到本地。整个过程如下
- 如果ts被加密但是没有解密方法,就将密钥文件保存到本地
- 将ts地址列表依次提交到线程池中执行下载任务
- ts下载完成后,如果有解密方法就执行解密,然后保存到本地,并且将下载ts的信息放到SynchronousQueue中
- 主线程创建一个文件info.txt,写入视频名、来源、进度,始终打开着该文件,然后从SynchronousQueue不断的获取下载完成的ts信息,并更新info.txt中的进度,直到100%
- 最后将本地m3u8文件(定义了如何从本地服务获取ts文件)保存到本地(video.base.path配置中执行的路径,可以直接修改application.yml或者执行jar包时候指定属性java -jar -Dvideo.base.path=D:/xx xxx.jar )
解密的视频:
不解密的视频:
@Slf4j
@Service("commonVideoActuator")
public class CommonVideoActuator implements VideoActuator {@Value("${video.base.path}")private String videoBasePath;@Resource(name = "downloadTSPool")private ExecutorService downloadTSPool;public static final Pattern encKeyPat = Pattern.compile("#EXT-X-KEY.*?URI=\"(.*?)\"[^\n]*");@Overridepublic Result<String> downloadAndSaveTS(String url, VideoResolver videoResolver) {try {M3U8BO m3u8BO = videoResolver.getM3U8(url);TsListBO tsListBO = videoResolver.getTsList(m3u8BO);List<TsBO> tsList = tsListBO.getTsList();Assert.isNotEmpty(tsList, "ts地址列表为空");String fileId = m3u8BO.getId();String basePath = videoBasePath + "/" + fileId;Assert.isTrue(FileUtil.deleteFolder(basePath), basePath + "删除失败");Files.createDirectories(Paths.get(basePath + "/ts"));String m3u8Content = m3u8BO.getContent();Path originM3U8Path = Paths.get(basePath + "/origin.m3u8");Files.write(originM3U8Path, m3u8Content.getBytes(), StandardOpenOption.CREATE_NEW);UnaryOperator<byte[]> decodeTsFunction = getDecodeTsFunction(basePath, tsListBO, videoResolver);SynchronousQueue<LocalTsBO> synchronousQueue = new SynchronousQueue<>();batchSubmitTsTask(basePath, tsList, decodeTsFunction, synchronousQueue);List<LocalTsBO> successTsList = getFutureAndSaveInfo(basePath, m3u8BO, tsList.size(), synchronousQueue);Path localM3U8Path = Paths.get(basePath + "/local.m3u8");String newM3U8Content = buildLocalM3U8Content(tsListBO, fileId, successTsList);Files.write(localM3U8Path, newM3U8Content.getBytes(), StandardOpenOption.CREATE_NEW);return Result.success(fileId);} catch (ViewException e) {throw e;} catch (Exception e) {log.error("downloadAndSaveTS fail url:{}", url, e);return Result.fail("下载保存视频发生错误");}}private UnaryOperator<byte[]> getDecodeTsFunction(String basePath, TsListBO tsListBO, VideoResolver videoResolver) throws IOException {TsEncBO tsEncBO = tsListBO.getTsEncBO();if (tsEncBO != null) {String head = tsListBO.getHead();byte[] encKey = tsEncBO.getEncKey();UnaryOperator<byte[]> decodeTsFunction = videoResolver.getDecodeTsFunction(head, encKey);if (decodeTsFunction != null) {tsListBO.setHead(head.replaceAll(encKeyPat.toString(), ""));return decodeTsFunction;} else {Path encKeyPath = Paths.get(basePath + "/enc.key");Files.write(encKeyPath, encKey, StandardOpenOption.CREATE_NEW);}}return UnaryOperator.identity();}private void batchSubmitTsTask(String basePath, List<TsBO> tsList, UnaryOperator<byte[]> decodeFun, SynchronousQueue<LocalTsBO> synchronousQueue) {IntStream.range(0, tsList.size()).forEach(i -> {TsBO tsBO = tsList.get(i);String localTsName = i + ".ts";LocalTsBO localTsBO = new LocalTsBO();localTsBO.setIndex(i);localTsBO.setTsUrl(tsBO.getUrl());localTsBO.setExtInf(tsBO.getExtInf());localTsBO.setLocalTsName(localTsName);localTsBO.setLocalTsPath(basePath + "/ts/" + localTsName);downloadTSPool.submit(() -> doDownloadAndSaveTS(localTsBO, decodeFun, synchronousQueue));});}private List<LocalTsBO> getFutureAndSaveInfo(String basePath, M3U8BO m3u8BO, int allSize, SynchronousQueue<LocalTsBO> synchronousQueue) throws IOException {try (RandomAccessFile rf = new RandomAccessFile(basePath + "/info.txt", "rw")) {String head = "文件名:" + m3u8BO.getName() + "\n";head += "来源:" + m3u8BO.getSourceUrl() + "\n";head += "进度:";rf.write(head.getBytes(StandardCharsets.UTF_8));int preRateByteNum = 0;StringBuilder failTsContent = new StringBuilder();List<LocalTsBO> successTsList = new ArrayList<>(allSize);for (int i = 0; i < allSize; i++) {LocalTsBO localTsBO = synchronousQueue.take();rf.seek(rf.getFilePointer() - preRateByteNum);String rate = String.format("%.2f", (i + 1) * 100.0 / allSize) + "%";byte[] rateByte = rate.getBytes(StandardCharsets.UTF_8);rf.write(rateByte);preRateByteNum = rateByte.length;HandlerUtil.branchHandler(localTsBO.isTaskSuccess(), () -> successTsList.add(localTsBO), () ->failTsContent.append(localTsBO.getLocalTsName()).append("->").append(localTsBO.getTsUrl()).append("\n"));}rf.write(("\n异常ts文件:\n" + failTsContent).getBytes(StandardCharsets.UTF_8));successTsList.sort(Comparator.comparing(LocalTsBO::getIndex));return successTsList;} catch (InterruptedException e) {Thread.currentThread().interrupt();log.error("getFutureAndSaveInfo fail m3u8BO:{}", m3u8BO, e);throw new IllegalStateException("线程中断,下载任务停止");}}private String buildLocalM3U8Content(TsListBO tsListBO, String fileId, List<LocalTsBO> successLocalTs) {StringBuilder newM3U8Content = new StringBuilder(tsListBO.getHead());successLocalTs.forEach(localTsBO -> {String extInf = localTsBO.getExtInf();String localTsName = localTsBO.getLocalTsName();newM3U8Content.append(extInf).append("\n").append("/video/ts/").append(fileId).append("/").append(localTsName).append("\n");});newM3U8Content.append(tsListBO.getEnd());return newM3U8Content.toString();}private void doDownloadAndSaveTS(LocalTsBO localTsBO, UnaryOperator<byte[]> decodeFunction, SynchronousQueue<LocalTsBO> synchronousQueue) {String tsUrl = localTsBO.getTsUrl();String localTsPath = localTsBO.getLocalTsPath();try {HttpRespBO respBO = HttpUtil.httpGet10(tsUrl);if (respBO == null) {log.error("downloadTS fail localTsBO:{}", localTsBO);return;}try (RandomAccessFile ts = new RandomAccessFile(localTsPath, "rw")) {ts.write(decodeFunction.apply(respBO.getBody()));localTsBO.setTaskSuccess(true);}} catch (IOException e) {log.error("save ts fail,localTsBO:{}", localTsBO, e);} finally {putSynchronousQueue(synchronousQueue, localTsBO);}}private void putSynchronousQueue(SynchronousQueue<LocalTsBO> synchronousQueue, LocalTsBO localTsBO) {try {synchronousQueue.put(localTsBO);} catch (InterruptedException e) {Thread.currentThread().interrupt();log.error("doDownloadAndSaveTS put synchronousQueue fail,localTsBO:{}", localTsBO, e);}}@Dataprivate static class LocalTsBO {/*** ts的位置*/private int index;/*** ts的地址*/private String tsUrl;/*** ts时长*/private String extInf;/*** 本地ts名称*/private String localTsName;/*** 本地ts路径*/private String localTsPath;/*** 任务是否成功*/private boolean taskSuccess;}
}
视频播放能力
最后提供本地视频的播放能力,也就是提供三个http接口
- 根据视频id获取本地m3u8文件
- 根据视频id和ts文件名获取本地ts字节数组(这个接口就是上面m3u8里面的ts链接)
- 根据视频id获取本地密钥数组(这个接口就是上面m3u8里面的密钥链接)
至此随便找一个能播放m3u8的软件就可以播放我们本地的视频了,如Safari浏览器、QuickTime Player等,直接在Safari浏览器或者播放器中输入m3u8文件的网址,就可以开始观看视频了
@Slf4j
@RestController
@RequestMapping("/video")
public class VideoController {@Autowiredprivate VideoService videoService;@GetMapping(value = "/m3u8/{videoId}", produces = "application/vnd.apple.mpegurl")public byte[] getM3U8(@PathVariable String videoId) {return returnFileTemplate(videoId + "/local.m3u8");}@GetMapping(value = "/ts/{videoId}/{tsName}", produces = "video/mp2t")public byte[] getTs(@PathVariable String videoId, @PathVariable String tsName) {return returnFileTemplate(videoId + "/ts/" + tsName);}@GetMapping(value = "/enc/key/{videoId}", produces = "application/octet-stream")public byte[] getEncKey(@PathVariable String videoId) {return returnFileTemplate(videoId + "/enc.key");}
}
private byte[] returnFileTemplate(String relativePath) {Result<byte[]> result = videoService.getFileByte(relativePath);return Optional.of(result).filter(Result::isSuccess).map(Result::getData).orElseGet(() -> JSON.toJSONBytes(result));}@Overridepublic Result<byte[]> getFileByte(String relativePath) {String filePath = videoBasePath + "/" + relativePath;try (RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "r")) {byte[] buffer = new byte[(int) randomAccessFile.length()];randomAccessFile.read(buffer);return Result.success(buffer);} catch (Exception e) {log.error("getTs fail,filePath:{}", filePath, e);return Result.fail("获取文件失败");}}
本质上就是先从本地服务里获取m3u8文件,根据里面定义的ts链接去本地服务获取ts数据,如果加密了,再获取密钥进行解密
相关文章:
java实现局域网内视频投屏播放(二)爬虫
代码链接 视频播放原理 大多视频网站使用的是m3u8,m3u8其实不是一个真正的视频文件,而是一个视频播放列表(playlist)。它是一种文本文件,里面记录了一系列的视频片段(segment)的网络地址。这些…...
a标签的target属性
<a> 标签的 target 属性规定在何处打开链接文档。 最常用的两个值是: _self : 在当前窗口打开被链接文档 _blank:在新窗口打开被链接文档 就是常见浏览网页打开链接的方式...
无mac在线申请hbuilderx打包ios证书的方法
hbuilderx是一个跨平台的开发工具,可以开发android和ios的app应用。打包hbuilderx应用需要hbuilderx打包证书。但是很多使用hbuilderx开发的程序员,并没有mac电脑,而申请ios的证书,hbuilderx官网的教程却是需要mac电脑的ÿ…...
[css] flex wrap 九宫格布局
<div class"box"><ul class"box-inner"><li>九宫格1</li><li>九宫格2</li><li>九宫格3</li><li>九宫格4</li><li>九宫格5</li><li>九宫格6</li><li>九宫格7&l…...
云上丝绸之路| 云轴科技ZStack成功实践精选(西北)
古有“丝绸之路” 今有丝绸之路经济带 丝路焕发新生,数智助力经济 云轴科技ZStack用“云”护航千行百业 沿丝绸之路,领略西北数字化。 古丝绸之路起点-陕西 集历史与现代交融,不仅拥有悠久的历史文化积淀,而且现代化、数字化发…...
Java8 IfPresent 与 forEach 的组合操作
一、需求背景 Java8的Optional接口是我们经常使用的一个接口,尤其是对对象进行判空的时候,需要经常使用到IfPresent()。 但是,如果是对List进行判空、循环的话,就稍显繁杂了,因为几乎每次对List进行操作的时候&a…...
WebGL+Three.js入门与实战——给画布换颜色、绘制一个点、三维坐标系
个人简介 👀个人主页: 前端杂货铺 🙋♂️学习方向: 主攻前端方向,正逐渐往全干发展 📃个人状态: 研发工程师,现效力于中国工业软件事业 🚀人生格言: 积跬步…...
SystemServer 进程启动过程
首语 SystemServer进程主要用于启动系统服务,诸如AMS、WMS、PMS都是由它来创建的。在系统的名称为"system_server",Android核心服务都是它启动,它是非常重要。 Zygote处理SystemServer进程 在 Zygote启动过程 文章中分析我们知道…...
Java EE 多线程之 JUC
文章目录 1. Callable 接口2. ReentrantLock3. 信号量4. CountDownLatch JUC这里就是指(java.util.concurrent) concurrent 就是并发的意思 这个包里的内容,主要就是一些多线程相关的组件 1. Callable 接口 Callable 也是一种创建线程的方式…...
Unity光照模型实践
光照作为3D渲染中最重要的部分之一,如何去模拟真实环境的光照是重要的研究内容,但是现实环境光照过于复杂,有很多经典好用的光照模型去近似真实光照。 根据基础的Phong模型 最终某个点的结果为 环境光Ambient 漫反射光Diffuse 高光Specula…...
从0创建并部署一个网页到服务器
创建一个页面 1 下载node.js 下载VScode 2 在Windows下找一个路径新建一个文件夹 例如:D:\study_project\PersonalWeb 3 VSCodee中打开文件夹 4 Windows下 管理员身份打开命令提示符,执行npm install -g vue/cli 5 VSCode下打开终端,执…...
Ubuntu 22.04 安装 OCI CLI
Ubuntu 22.04 安装 OCI CLI 安装命令 安装命令 wget https://codeload.github.com/oracle/oci-cli/zip/master -O oci-cli.zip pip install oci-cli.zip完结!...
K8S的安装工具
kubectl Kubernetes 命令行工具 kubectl, 让你可以对 Kubernetes 集群运行命令。 你可以使用 kubectl 来部署应用、监测和管理集群资源以及查看日志。 有关更多信息,包括 kubectl 操作的完整列表,请参见 kubectl参考文件。 kubectl 可安装在…...
vue中哪些数组的方法可以做到响应式
Vue2 中为什么直接通过数组的索引修改元素是不会触发视图更新 vue2 为什么不直接监听数组 Vue2 对于数组提供了一些变异方法 重写数组方法源码分析 定义拦截器将拦截器挂载到数组上面收集依赖 扩展:理解Vue2如何解决数组和对象的响应式问题 对复杂对象的处理 复杂对…...
软考科目如何选择?
软考科目繁多,让许多学弟学妹感到困惑,不知道该选择哪个科目。以下是一些建议,可以根据个人实际需求选择备考的科目。 1、初级是可选的 软考初级非常简单,适合刚刚入门学习的朋友报考。对于一些有基础的朋友,建议直接…...
羊大师解读,血压波动
羊大师解读,血压波动 血压是身体健康的一个重要指标,但有时候我们会发现血压存在着波动的情况。血压波动的原因有很多,包括生活方式、遗传因素、药物影响等等。本文小编羊大师将为大家详细介绍血压波动的原因,以及预防和管理血压…...
关于充值!购买的流量卡第一次在哪充值?这个问题你想过吗?
手机套餐太贵、物联卡体验又不好,而官网申请的流量卡又都是定向流量,所以,运营商推出的只能线上申请的大流量卡一时之间便成了大家关注的焦点。 在流量卡的使用过程中,申请是免费的,快递是免费的,但…...
HTML基础标签
但实际上无论声明为中文还是英文都可以写,中文/英文 主要是浏览器在进行调用翻译功能的时候,会按照声明的语言来进行翻译。 标签语义: 标签的属性一般都是在第一个标签中定义该标签效果所拥有的属性。 即标签的作用是什么 <>标签功能…...
人大金仓引领医疗行业新标准
近日,由中国信息产业商会团体标准委员会主办,人大金仓与国家电子计算机质量检验检测中心(北京尊冠科技有限公司)联合承办的《基于医疗应用的国产关系型数据库能力评价规范》团体标准研讨会顺利召开。 “ 各大知名医院专家云集 深入…...
【UML】NO.1 UML简介
目录 一、什么是UML 二、UML和软件工程 三、UML的诞生 四、UML的基本构成 从今天开始,开一个新的话题,把UML梳理一遍。 一、什么是UML UML(Unified Modeling Language,UML)是一个通用的可视化建模语言标准,用于对…...
【Idea】SpringBoot项目中,jar包引用冲突异常的排查 / SM2算法中使用bcprov-jdk15to18的报错冲突问题
问题描述以及解决方法: 项目中使用了bcprov-jdk15to18 pom依赖,但是发现代码中引入的版本不正确。 追溯代码发现版本引入的是bcprov-jdk15on,而不是bcprov-jdk15to18,但是我找了半天pom依赖也没有发现有引入bcprov-jdk15on依赖。…...
MISRA C++ 2023:C和C++测试解决方案实现静态分析
自动化软件测试解决方案的全球领导者Parasoft今天宣布,随着Parasoft C/Ctest 2023.2即将发布,全面支持MISRA C 2023。Parasoft针对C和C软件开发的完全集成测试解决方案计划于2023年12月发布,可以帮助团队实现自动化静态分析和编码标准合规性&…...
半导体:Gem/Secs基本协议库的开发(4)
继续接上篇 《半导体:Gem/Secs基本协议库的开发(3)》,本篇我们分享的比较简单,windows系统下tcp和串口通讯。这也是我们协议开发比较重要的一部分,不过我们在此把它封装程一个单独的通讯库,毕竟…...
解锁知识的新大门:自建知识付费小程序的技术指南
在数字化时代,知识付费小程序的崛起为创作者和学习者提供了全新的学习和分享方式。本文将以“知识付费小程序源码”为关键词,从技术角度出发,为你展示如何搭建一个独具特色的知识付费平台。 步骤1:选择适用的知识付费小程序源码…...
Java8实战 - 行为参数化传递代码
背景: 根据《java8实战》把第二章简单概括一下。 在软件工程中,一个最重要的问题是,用户的需求会一直变化,如何应对不断变化的需求,并且把工作量降到最低是需要考虑的,而行为参数化就是一个处理频繁变更需…...
jmeter,取“临时重定向的登录接口”响应头中的cookie
1、线程组--创建线程组; 2、线程组--添加--取样器--HTTP请求; 3、Http请求--添加--后置处理器--正则表达式提取器; 4、线程组--添加--监听器--查看结果树; 5、线程组--添加--取样器--调试取样器。 首先理解 自动重定向 与跟随…...
流程控制之条件判断
目录 流程控制之条件判断 2.1.if语句语法 2.1.1单分支结构 2.1.2双分支结构 2.1.3多分支结构 2.2.案例 例一: 例2: 例3: 例4: 例5: 例6: 例7: 例8: 例9: 2.3.case多条件判断 2.3.1.格式 2.3.2.执行过程 例10: 流程控制之条件判断 2.1.if语句语法 2.1.1单分…...
2 - Electron 核心概念
Electron 核心概念 主进程 通过Node.js、Electron提供的API与系统底层打交道启动项目时运行的 main.js 脚本就是我们说的主进程。在主进程运行的脚本可以以创建 Web 页面的形式展示 GUI。主进程只有一个 渲染进程 每个 Electron 的页面都在运行着自己的进程,这样…...
Cmake找不到mysql.h和libmysqlclient.so
查看mysql.h和libmysqlclient.so的路径 eikeik-Virtual-Machine:~/桌面/dbpool/bin$ locate mysql.h /usr/include/mysql/mysql.h eikeik-Virtual-Machine:~/桌面/dbpool/bin$ locate libmysqlclient.so /usr/lib/x86_64-linux-gnu/libmysqlclient.so /usr/lib/x86_64-linux-g…...
图论——二分图
图论——二分图 二分图通俗解释 有一个图,将顶点分成两类,边只存在不同类顶点之间,同类顶点之间设有边。称图 G 为二部图,或称二分图,也称欧图。 性质 二分图不含有奇数环图中没有奇数环,一定可以转换为二…...
南昌网站快速排名提升/网络平台建站
PasswordStrength是AtlasControlToolkit在最新版本里面提供的一个检测密码强度的Extender,它支持两种文本提示和进度条提示两种方式。主要内容1.PasswordStrength介绍2.完整示例一.PasswordStrength介绍PasswordStrength是AtlasCo…...
flash网站的优点和缺点/深圳优化公司
python脚本为敏捷开发脚本,在zabbix监控也起到重要作用,以下是使用python脚本发送告警邮件配置方法。 脚本如下: #!/usr/bin/python #coding:utf-8 import smtplib from email.mime.text import MIMEText import sys mail_host = smtp.126.com mail_user = username mail…...
wordpress 归档函数/西安网络推广营销公司
MYSQL 内部模块 [Connection Pool] (授权、线程复用、连接限制、内存检测等) >[SQL Interface] (DML、DDL、Views等) [Parser] (Query Translation、Object privilege) [Optimizer] (Access Paths、 统计分析) [Caches & Buffers] >[Pluggable Storage Engines] 复…...
赣州网站建设如何/自助建站工具
通常,我们用到数据库会有很多种,这里就不做讨论了,我们只来说说如何用room来存储一些复杂数据结构。首先看此文章的都假设你已经看过了room的简单用法,如果没有看过,那你可能需要先去看看了。假设,我们从后…...
商城网站建设合同/简述seo
作者Qiu800820,源码SuperLike,仿今日头条点赞喷射表情动画,主要用于点赞防抖事件的交互提示,这里只提供交互View 防抖功能可以根据Rx自行实现。使用compile com.sum.slike:library:0.2.1参数含义自定义属性名字参数含义eruption_e…...
钓鱼网站的危害/自助建站模板
《传奇世界》最新卡品\元宝刷法。几块钱而已。要刷的快刷,估计帖子一出很快就更新维护BUG了。最新公布刷传世元宝方法:在传奇世界的冲值页面下,有一个叫a8156759的POPTANG资料处理中心的数据文件,它是用来记录冲过的冲值卡的,当然它是没有办法…...