文件上傳 分片上傳

分片上傳則是將一個大文件分割成多個小塊分別上傳,最后再由服務器合并成完整的文件。這種做法的好處是可以并行處理多個小文件,提高上傳效率;同時,如果某一部分上傳失敗,只需要重傳這一部分,不影響其他部分。

初步實現

后端代碼

/*** 分片上傳** @param file 上傳的文件* @param start 文件開始上傳的位置* @param fileName 文件名稱* @return  上傳結果*/
@PostMapping("/fragmentUpload")
@ResponseBody
public AjaxResult fragmentUpload(@RequestParam("file") MultipartFile file, @RequestParam("start") long start, @RequestParam("fileName") String fileName) {try {// 檢查上傳目錄是否存在,如果不存在則創建File directory = new File(uploadPath);if (!directory.exists()) {directory.mkdirs();}// 設置上傳文件的目標路徑File targetFile = new File(uploadPath +File.separator+ fileName);// 創建 RandomAccessFile 對象以便進行文件的隨機讀寫操作RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "rw");// 獲取 RandomAccessFile 對應的 FileChannelFileChannel channel = randomAccessFile.getChannel();// 設置文件通道的位置,即從哪里開始寫入文件內容channel.position(start);// 從 MultipartFile 對象的資源通道中讀取文件內容,并寫入到指定位置channel.transferFrom(file.getResource().readableChannel(), start, file.getSize());// 關閉文件通道和 RandomAccessFile 對象channel.close();randomAccessFile.close();// 返回上傳成功的響應return AjaxResult.success("上傳成功");} catch (Exception e) {// 捕獲異常并返回上傳失敗的響應return AjaxResult.error("上傳失敗");}
}/*** 檢測文件是否存在* 如果文件存在,則返回已經存在的文件大小。* 如果文件不存在,則返回 0,表示前端從頭開始上傳該文件。* @param filename* @return*/
@GetMapping("/checkFile")
@ResponseBody
public AjaxResult checkFile(@RequestParam("filename") String filename) {File file = new File(uploadPath+File.separator + filename);if (file.exists()) {return AjaxResult.success(file.length());} else {return AjaxResult.success(0L);}
}

前端

var prefix = ctx + "/kuroshiro/file-upload";// 每次上傳大小
const chunkSize = 1 * 1024 * 1024;/*** 開始上傳*/
function startUpload(type) {const fileInput = document.getElementById('fileInput');const file = fileInput.files[0];if (!file) {alert("請選擇文件");return;}if(type == 1){checkFile(filename).then(start => {uploadFile(file, start,Math.min(start + chunkSize, file.size));})}
}/*** 檢查是否上傳過* @param filename* @returns {Promise<unknown>}*/
function checkFile(filename) {return $fetch(prefix+`/checkFile?filename=${filename}`);
}/*** 開始分片上傳* @param file 文件* @param start 開始位置* @param end 結束位置*/
function uploadFile(file, start,end) {if(start < end){const chunk = file.slice(start, end);const formData = new FormData();formData.append('file', chunk);formData.append('start', start);formData.append('fileName', file.name);$fetch(prefix+'/fragmentUpload', {method: 'POST',body: formData}).then(response => {console.log(`分片 ${start} - ${end} 上傳成功`);// 遞歸調用uploadFile(file,end,Math.min(end + chunkSize, file.size))})}}function $fetch(url,requestInit){return new Promise((resolve, reject) => {fetch(url,requestInit).then(response => {if (!response.ok) {throw new Error('請求失敗');}return response.json();}).then(data => {if (data.code === 0) {resolve(data.data);} else {console.error(data.msg);reject(data.msg)}}).catch(error => {console.error(error);reject(error)});});}

以上雖然實現的分片上傳,但是它是某種意義上來說還是與整體上傳差不多,它是一段一段的上傳,某段上傳失敗后,后續的就不會再繼續上傳;不過比起整體上傳來說,它會保存之前上傳的內容,下一個上傳時,從之前上傳的位置接著上傳。不用整體上傳。下面進行優化。

優化

首先,之前的分片上傳,后端是直接寫入了一個文件中了,所以只能順序的上傳寫入,雖然可以保存上傳出錯之前的內容,但是整體上看來是速度也不行。
優化邏輯:把分片按順序單獨保存下來,等到所有分片都上傳成功后,把所有分片合并成文件。這樣上傳的時候就不用等著上一個上傳成功才上傳下一個了。

后端代碼

/**
* 分片上傳* @param file 文件* @param chunkIndex 分片下標*/
@PostMapping("/uploadChunk")
@ResponseBody
public AjaxResult uploadChunk(@RequestParam("file") MultipartFile file, @RequestParam("chunkIndex") int chunkIndex,@RequestParam("fileName") String fileName) {String uploadDirectory = chunkUploadPath+File.separator+fileName;File directory = new File(uploadDirectory);if (!directory.exists()||directory.isFile()) {directory.mkdirs();}String filePath = uploadDirectory + File.separator + fileName+ "_" + chunkIndex;try (OutputStream os = new FileOutputStream(filePath)) {os.write(file.getBytes());return AjaxResult.success("分片"+(chunkIndex+1)+"上傳成功");}catch (Exception e){// 保存失敗后如果文件建立了就刪除,下次上傳時重新保存,避免文件內容錯誤File chunkFile = new File(filePath);if(chunkFile.exists()) chunkFile.delete();e.printStackTrace();return AjaxResult.error("分片"+(chunkIndex+1)+"上傳失敗");}}/*** 檢測分片是否存在* 如果文件存在,則返回已經存在的分片下標集合。存在的就不上傳* 如果文件不存在,則返回空集合,表示前端從頭開始上傳該文件* @param fileName* @return*/
@GetMapping("/checkChunk")
@ResponseBody
public AjaxResult checkChunk(@RequestParam("fileName") String fileName) {String uploadDirectory = chunkUploadPath+File.separator+fileName;List<Integer> list = new ArrayList<>();File file = new File(uploadDirectory);// 文件目錄不存在if(!file.exists()||file.isFile()) return AjaxResult.success(list);File[] files = file.listFiles();// 文件目錄下沒有分片文件if(files == null) return AjaxResult.success(list);// 返回存在分片下標集合return AjaxResult.success(Arrays.stream(files).map(item->Integer.valueOf(item.getName().substring(item.getName().lastIndexOf("_")+1))).collect(Collectors.toList()));
}// 合并文件分片@PostMapping("/mergeChunks")@ResponseBodypublic AjaxResult mergeChunks(@RequestParam("fileName") String fileName, @RequestParam("totalChunks") int totalChunks) {String uploadDirectory = chunkUploadPath+File.separator+fileName;String mergedFilePath = uploadPath +File.separator+ fileName;try (OutputStream os = new FileOutputStream(mergedFilePath, true)) {for (int i = 0; i < totalChunks; i++) {Path chunkFilePath = Paths.get(uploadDirectory +File.separator+ fileName + "_" + i);Files.copy(chunkFilePath, os);Files.delete(chunkFilePath);}return AjaxResult.success();}catch (Exception e){e.printStackTrace();return AjaxResult.error(e.getMessage());}}

前端代碼

/*** 開始上傳*/
function startUpload(type) {const fileInput = document.getElementById('fileInput');const file = fileInput.files[0];if (!file) {alert("請選擇文件");return;}const filename = file.name;if(type == 1){checkFile(filename).then(start => {uploadFile(file, start,Math.min(start + chunkSize, file.size));})}if(type == 2){checkChunk(filename).then(arr => {uploadChunk(file, arr);})}
}/**
* 切割文件為多個分片
* @param file
* @returns {*[]}
*/
function sliceFile(file) {const chunks = [];let offset = 0;while (offset < file.size) {const chunk = file.slice(offset, offset + chunkSize);chunks.push(chunk);offset += chunkSize;}return chunks;
}
/**
* 檢查是否上傳過
* @param filename
* @returns {Promise<unknown>}
*/
function checkChunk(filename) {return $fetch(prefix+`/checkChunk?fileName=${filename}`);
}/**
* 開始分片上傳
* @param file 文件
* @param exists 存在的分片下標
*/
function uploadChunk(file,exists) {const chunkArr = sliceFile(file);Promise.all(chunkArr.map((chunk, index) => {if(!exists.includes(index)){const formData = new FormData();formData.append('file', chunk);formData.append('fileName', file.name);formData.append('chunkIndex', index);return $fetch(prefix+'/uploadChunk', {method: 'POST',body: formData});}})).then(uploadRes=> {// 合并分片const formData = new FormData();formData.append('fileName', file.name);formData.append('totalChunks', chunkArr.length);$fetch(prefix + '/mergeChunks', {method: 'POST',body:formData,}).then(mergeRes=>{console.log("合并成功")});});
}

以上優化后所有分片可以同時上傳,所有分片上傳都成功后進行合并。

最后是完整代碼

@Controller()
@RequestMapping("/kuroshiro/file-upload")
public class FileUploadController {private String prefix = "kuroshiro/fragmentUpload";// 文件保存目錄private final String uploadPath = RuoYiConfig.getUploadPath();// 分片保存目錄private final String chunkUploadPath = uploadPath+File.separator+"chunks";/*** demo* @return*/@GetMapping("/demo")public String demo() {return prefix+"/demo";}/*** 分片上傳** @param file 上傳的文件* @param start 文件開始上傳的位置* @param fileName 文件名稱* @return  上傳結果*/@PostMapping("/fragmentUpload")@ResponseBodypublic AjaxResult fragmentUpload(@RequestParam("file") MultipartFile file, @RequestParam("start") long start, @RequestParam("fileName") String fileName) {try {// 檢查上傳目錄是否存在,如果不存在則創建File directory = new File(uploadPath);if (!directory.exists()) {directory.mkdirs();}// 設置上傳文件的目標路徑File targetFile = new File(uploadPath +File.separator+ fileName);// 創建 RandomAccessFile 對象以便進行文件的隨機讀寫操作RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "rw");// 獲取 RandomAccessFile 對應的 FileChannelFileChannel channel = randomAccessFile.getChannel();// 設置文件通道的位置,即從哪里開始寫入文件內容channel.position(start);// 從 MultipartFile 對象的資源通道中讀取文件內容,并寫入到指定位置channel.transferFrom(file.getResource().readableChannel(), start, file.getSize());// 關閉文件通道和 RandomAccessFile 對象channel.close();randomAccessFile.close();// 返回上傳成功的響應return AjaxResult.success("上傳成功");} catch (Exception e) {// 捕獲異常并返回上傳失敗的響應return AjaxResult.error("上傳失敗");}}/*** 檢測文件是否存在* 如果文件存在,則返回已經存在的文件大小。* 如果文件不存在,則返回 0,表示前端從頭開始上傳該文件。* @param filename* @return*/@GetMapping("/checkFile")@ResponseBodypublic AjaxResult checkFile(@RequestParam("filename") String filename) {File file = new File(uploadPath+File.separator + filename);if (file.exists()) {return AjaxResult.success(file.length());} else {return AjaxResult.success(0L);}}/*** 分片上傳* @param file 文件* @param chunkIndex 分片下標*/@PostMapping("/uploadChunk")@ResponseBodypublic AjaxResult uploadChunk(@RequestParam("file") MultipartFile file, @RequestParam("chunkIndex") int chunkIndex,@RequestParam("fileName") String fileName) {String uploadDirectory = chunkUploadPath+File.separator+fileName;File directory = new File(uploadDirectory);if (!directory.exists()||directory.isFile()) {directory.mkdirs();}String filePath = uploadDirectory + File.separator + fileName+ "_" + chunkIndex;try (OutputStream os = new FileOutputStream(filePath)) {os.write(file.getBytes());return AjaxResult.success("分片"+(chunkIndex+1)+"上傳成功");}catch (Exception e){// 保存失敗后如果文件建立了就刪除,下次上傳時重新保存,避免文件內容錯誤File chunkFile = new File(filePath);if(chunkFile.exists()) chunkFile.delete();e.printStackTrace();return AjaxResult.error("分片"+(chunkIndex+1)+"上傳失敗");}}/*** 檢測分片是否存在* 如果文件存在,則返回已經存在的分片下標集合。存在的就不上傳* 如果文件不存在,則返回空集合,表示前端從頭開始上傳該文件* @param fileName* @return*/@GetMapping("/checkChunk")@ResponseBodypublic AjaxResult checkChunk(@RequestParam("fileName") String fileName) {String uploadDirectory = chunkUploadPath+File.separator+fileName;List<Integer> list = new ArrayList<>();File file = new File(uploadDirectory);// 文件目錄不存在if(!file.exists()||file.isFile()) return AjaxResult.success(list);File[] files = file.listFiles();// 文件目錄下沒有分片文件if(files == null) return AjaxResult.success(list);// 返回存在分片下標集合return AjaxResult.success(Arrays.stream(files).map(item->Integer.valueOf(item.getName().substring(item.getName().lastIndexOf("_")+1))).collect(Collectors.toList()));}// 合并文件分片@PostMapping("/mergeChunks")@ResponseBodypublic AjaxResult mergeChunks(@RequestParam("fileName") String fileName, @RequestParam("totalChunks") int totalChunks) {String uploadDirectory = chunkUploadPath+File.separator+fileName;String mergedFilePath = uploadPath +File.separator+ fileName;try (OutputStream os = new FileOutputStream(mergedFilePath, true)) {for (int i = 0; i < totalChunks; i++) {Path chunkFilePath = Paths.get(uploadDirectory +File.separator+ fileName + "_" + i);Files.copy(chunkFilePath, os);Files.delete(chunkFilePath);}File chunkDir = new File(uploadDirectory);if (chunkDir.exists()) chunkDir.delete();return AjaxResult.success();}catch (Exception e){e.printStackTrace();return AjaxResult.error(e.getMessage());}}}
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head><th:block th:include="include :: header('分片上傳')" />
</head>
<body class="gray-bg">
<div class="container-div" id="chunk-div"><div class="row"><div class="col-sm-12 search-collapse"><form id="formId"><div class="select-list"><ul><li><label>選擇文件:</label><input type="file" id="fileInput"/></li><li><a class="btn btn-primary btn-rounded btn-sm" @click="startUpload(1)"><i class="fa fa-upload"></i>&nbsp;開始上傳1</a><a class="btn btn-primary btn-rounded btn-sm" @click="startUpload(2)"><i class="fa fa-upload"></i>&nbsp;開始上傳2</a></li></ul></div></form></div><div class="col-sm-12" style="padding-left: 0;"><div class="ibox"><div class="ibox-content"><h3>上傳進度</h3><ul class="sortable-list connectList agile-list" v-if="uploadMsg && uploadMsg.length>0"><li v-for="item in uploadMsg" :class="item.status+'-element'">{{item.title}}</li></ul></div></div></div></div>
</div>
<th:block th:include="include :: footer" />
<script th:inline="javascript">var prefix = ctx + "/kuroshiro/file-upload";new Vue({el: '#chunk-div',data: {// 每次上傳大小chunkSize: 1 * 1024 * 1024,uploadMsg:[],},methods: {/*** 開始上傳*/startUpload: function(type){const fileInput = document.getElementById('fileInput');const file = fileInput.files[0];if (!file) {alert("請選擇文件");return;}const filename = file.name;this.uploadMsg = [];if(type == 1){this.checkFile(filename).then(start => {this.uploadFile(file, start,Math.min(start + this.chunkSize, file.size));},err => {this.uploadMsg.push({title:`文件檢測失敗失敗:${err}`,status:"danger"});})}if(type == 2){this.checkChunk(filename).then(arr => {this.uploadChunk(file, arr);},err => {this.uploadMsg.push({title:`文件檢測失敗失敗:${err}`,status:"danger"});})}},/*** 檢查是否上傳過* @param filename* @returns {Promise<unknown>}*/checkFile: function(filename) {return this.$fetch(prefix+`/checkFile?filename=${filename}`);},/*** 開始分片上傳* @param file 文件* @param start 開始位置* @param end 結束位置*/uploadFile: function(file, start,end) {if(start < end){const chunk = file.slice(start, end);const formData = new FormData();formData.append('file', chunk);formData.append('start', start);formData.append('fileName', file.name);this.$fetch(prefix+'/fragmentUpload', {method: 'POST',body: formData}).then(response => {this.uploadMsg.push({title:`分片 ${start} - ${end} 上傳成功`,status:"info"});// 遞歸調用this.uploadFile(file,end,Math.min(end + this.chunkSize, file.size))},err=>{this.uploadMsg.push({title:`分片 ${start} - ${end} 上傳失敗:${err}`,status:"danger"});})}else{this.uploadMsg.push({title:`文件已上傳`,status:"info"});}},/*** 切割文件為多個分片* @param file* @returns {*[]}*/sliceFile: function(file) {const chunks = [];let offset = 0;while (offset < file.size) {const chunk = file.slice(offset, offset + this.chunkSize);chunks.push(chunk);offset += this.chunkSize;}return chunks;},/*** 檢查是否上傳過* @param filename* @returns {Promise<unknown>}*/checkChunk: function(filename) {return this.$fetch(prefix+`/checkChunk?fileName=${filename}`);},/*** 開始分片上傳* @param file 文件* @param exists 存在的分片下標*/uploadChunk: function(file,exists) {const chunkArr = this.sliceFile(file);Promise.all(chunkArr.map((chunk, index) => {if(!exists.includes(index)){const formData = new FormData();formData.append('file', chunk);formData.append('fileName', file.name);formData.append('chunkIndex', index);return new Promise((resolve, reject) => {this.$fetch(prefix+'/uploadChunk', {method: 'POST',body: formData}).then(res => {resolve(res)this.uploadMsg.push({title:`分片 ${index+1} 上傳成功`,status:"info"});},err => {reject(err)this.uploadMsg.push({title:`分片 ${index+1} 上傳失敗:${err}`,status:"danger"});});})}})).then(uploadRes=> {// 合并分片const formData = new FormData();formData.append('fileName', file.name);formData.append('totalChunks', chunkArr.length);this.$fetch(prefix + '/mergeChunks', {method: 'POST',body:formData,}).then(mergeRes=>{this.uploadMsg.push({title:`合并成功`,status:"info"});},err => {this.uploadMsg.push({title:`合并失敗:${err}`,status:"danger"});});});},$fetch: function(url,requestInit){return new Promise((resolve, reject) => {fetch(url,requestInit).then(response => {if (!response.ok) {throw new Error('請求失敗');}return response.json();}).then(data => {if (data.code === 0) {resolve(data.data);} else {reject(data.msg)}}).catch(error => {reject(error)});});},}});</script>
</body>
</html>

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/66118.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/66118.shtml
英文地址,請注明出處:http://en.pswp.cn/web/66118.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

js解決 Number失精度問題

const updatePromises adinfo.rows.map(async item > {const cwf await uniCloud.httpclient.request("https://api.oceanengine.com/open_api/v3.0/project/list/", {method: GET,data: {advertiser_id: item.account_id},// 1. 指定text數據格式dataType: tex…

實力認證 | 海云安入選《信創安全產品及服務購買決策參考》

近日&#xff0c;國內知名安全調研機構GoUpSec發布了2024年中國網絡安全行業《信創安全產品及服務購買決策參考》&#xff0c;報告從產品特點、產品優勢、成功案例、安全策略等維度對各廠商信創安全產品及服務進行調研了解。 海云安憑借AI大模型技術在信創安全領域中的創新應用…

Picocli 命令行框架

官方文檔 https://picocli.info/ 官方提供的快速入門教程 https://picocli.info/quick-guide.html 使用 Picocli 創建命令行應用程序 Picocli 是一個用于構建 Java 命令行應用的強大框架&#xff0c;它簡化了參數解析和幫助消息生成的過程。 下面是如何使用 Picocli 構建簡單命…

windows系統“GameInputRedist.dll”文件丟失或錯誤導致游戲運行異常如何解決?windows系統DLL文件修復方法

GameInputRedist.dll是存放在windows系統中的一個重要dll文件&#xff0c;缺少它可能會造成部分游戲不能正常運行。當你的電腦彈出提示“無法找到GameInputRedist.dll”或“計算機缺少GameInputRedist.dll”等錯誤問題&#xff0c;請不用擔心&#xff0c;我們將深入解析DLL文件…

M4Pro安裝homebrew并基于homebrew安裝MySQL踩坑記錄

系統偏好設置允許安裝任何來源應用&#xff1a;sudo spctl --master-disable 清除提示已損壞軟件的安全隔離&#xff0c;重新安裝&#xff1a; xattr -cr 空格&#xff0b;App路徑 安裝homebrew&#xff1a; /opt/homebrew/Cellar 安裝包目錄 /opt/homebrew/etc 默認運行目…

tmux 中鼠標滾動異常:^[[A和^[[B是什么以及如何解決

tmux 中鼠標滾動異常問題及解決方案 在使用 tmux 時&#xff0c;有時我們會遇到一個現象&#xff1a;當嘗試使用鼠標滾輪滾動窗口內容時&#xff0c;終端中會出現一串類似 ^[[A^[[A 的字符。這讓人困惑&#xff0c;不知道鼠標滾動為什么不起作用&#xff0c;也不清楚這些字符究…

【Vue】mouted、created、computed區別

mouted、created、computed區別 前端vue重構 — computed、watch、組件通信等常用知識整理 created和mouted都是vue生命周期中的鉤子函數&#xff0c;通常用來做一些初始化的工作&#xff0c;比如發送http請求、對組件綁定自定義事件 created&#xff1a;實例創建完后立即調用…

前端如何設計一個回溯用戶操作的方案

同一個項目&#xff0c;為什么我本地無法復現&#xff0c;只有客戶的設備才復現&#xff1f; 如何獲取用戶的操作路徑呢&#xff1f; 兩種方案&#xff1a;埋點和rrweb 埋點就很簡單了&#xff0c;將所有可能操作的節點都進行預埋數據&#xff1b;但埋點簡單并不省心&#xff…

概率論考前一天

判斷是不是分布函數&#xff1a;單調不減&#xff0c;右連續&#xff0c;F負無窮為0&#xff0c; F正無窮為1 判斷是不是密度函數&#xff1a;非負性&#xff08;函數任意地方都大于0&#xff09;&#xff0c;規范&#xff1a;積分為1

2Hive表類型

2Hive表類型 1 Hive 數據類型2 Hive 內部表3 Hive 外部表4 Hive 分區表5 Hive 分桶表6 Hive 視圖 1 Hive 數據類型 Hive的基本數據類型有&#xff1a;TINYINT&#xff0c;SAMLLINT&#xff0c;INT&#xff0c;BIGINT&#xff0c;BOOLEAN&#xff0c;FLOAT&#xff0c;DOUBLE&a…

FPGA工程師成長四階段

朋友&#xff0c;你有入行三年、五年、十年的職業規劃嗎&#xff1f;你知道你所做的崗位未來該如何成長嗎&#xff1f; FPGA行業的發展近幾年是蓬勃發展&#xff0c;有越來越多的人才想要或已經踏進了FPGA行業的大門。很多同學在入行FPGA之前&#xff0c;都會抱著滿腹對職業發…

springCloudGateway+nacos自定義負載均衡-通過IP隔離開發環境

先說一下想法&#xff0c;小公司開發項目&#xff0c;參考若依框架使用的spring-cloud-starter-gateway和spring-cloud-starter-alibaba-nacos, 用到了nacos的配置中心和注冊中心&#xff0c;有多個模塊&#xff08;每個模塊都是一個服務&#xff09;。 想本地開發&#xff0c;…

深度解析 React 中 setState 的原理:同步與異步的交織

在 React 框架的核心機制里&#xff0c;setState是實現動態交互與數據驅動視圖更新的關鍵樞紐。深入理解setState的工作原理&#xff0c;尤其是其同步與異步的特性&#xff0c;對于編寫高效、穩定且可預測的 React 應用至關重要。 一、setState 的基礎認知 在 React 組件中&a…

向量數據庫如何助力Text2SQL處理高基數類別數據

01. 導語 Agent工作流和 LLMs &#xff08;大語言模型&#xff09;的出現&#xff0c;讓我們能夠以自然語言交互的模式執行復雜的SQL查詢&#xff0c;并徹底改變Text2SQL系統的運行方式。其典型代表是如何處理High-Cardinality Categorical Data &#xff08;高基數類別數據&am…

qBittorent訪問webui時提示unauthorized解決方法

現象描述 QNAP使用Container Station運行容器&#xff0c;使用Docker封裝qBittorrent時&#xff0c;訪問IP:PORT的方式后無法訪問到webui&#xff0c;而是提示unauthorized&#xff0c;如圖&#xff1a; 原因分析 此時通常是由于設備IP與qBittorrent的ip地址不在同一個網段導致…

工程水印相機結合圖紙,真實現場時間地點,如何使用水印相機,超簡單方法只教一次!

在工程管理領域&#xff0c;精準記錄現場信息至關重要。水印相機拍照功能&#xff0c;為工程人員提供了強大的現場信息記錄工具&#xff0c;助力工程管理和統計工程量&#xff0c;更可以將圖片分享到電腦、分享給同事&#xff0c;協同工作。 一、打開圖紙 打開手機版CAD快速看圖…

GO語言實現KMP算法

前言 本文結合朱戰立教授編著的《數據結構—使用c語言&#xff08;第五版&#xff09;》&#xff08;以下簡稱為《數據結構&#xff08;第五版&#xff09;朱站立》&#xff09;中4.4.2章節內容編寫&#xff0c;KMP的相關概念可參考此書4.4.2章節內容。原文中代碼是C語言&…

LeetCode 熱題 100_從前序與中序遍歷序列構造二叉樹(47_105_中等_C++)(二叉樹;遞歸)

LeetCode 熱題 100_從前序與中序遍歷序列構造二叉樹&#xff08;47_105&#xff09; 題目描述&#xff1a;輸入輸出樣例&#xff1a;題解&#xff1a;解題思路&#xff1a;思路一&#xff08;遞歸&#xff09;&#xff1a; 代碼實現代碼實現&#xff08;思路一&#xff08;遞歸…

1.2 ThreeJS能力演示——模型導入導出編輯

1、模型導入導出編輯能力 1&#xff09;支持導入基本類型模型 最常用&#xff0c;最適合作為web演示模型的是glb格式的&#xff0c;當前演示glb模型導入 // 1) 支持導入基本類型模型const loader new GLTFLoader();loader.load(./three.js-master/examples/models/gltf/Hors…

文檔智能:OCR+Rocketqa+layoutxlm <Rocketqa>

此次梳理Rocketqa&#xff0c;個人認為該篇文件講述的是段落搜索的改進點&#xff0c;關于其框架&#xff1a;粗檢索 重排序----&#xff08;dual-encoder architecture&#xff09;&#xff0c;講訴不多&#xff0c;那是另外的文章&#xff1b; 之前根據文檔智能功能&#x…