EasyExcel之文件導出最佳實踐

文件導出

官方文檔:寫Excel | Easy Excel (alibaba.com)

引言

當使用 EasyExcel 進行 Excel 文件導出時,我最近在工作中遇到了一個需求。因此,我決定寫這篇文章來分享我的經驗和解決方案。如果你對這個話題感興趣,那么我希望這篇文章對你有所幫助。

本文的目標是介紹 EasyExcel 的基本概念、使用方法以及解決特定問題的技巧。通過使用 EasyExcel,我們可以提高文件導出的效率,簡化代碼,并實現更靈活的數據導出。

在閱讀完本文后,你將能夠了解 EasyExcel 的核心功能和常用操作,掌握如何根據實際需求進行配置和定制。此外,我還將分享一些實用的技巧和最佳實踐,幫助你更好地利用 EasyExcel 完成文件導出任務。

最后,我要再次感謝你的關注和支持。如果你覺得這篇文章對你有所幫助,請不要吝嗇點贊、收藏和關注我的其他文章或資源。這將是對我最大的鼓勵和支持!😘
文章內容會持續更新!!!

為何選擇 EasyExcel 而不是 POI?

選擇使用 EasyExcel 而不是 POI 的原因主要有以下幾點:

  1. EasyExcel 在盡可能節約內存的情況下支持讀寫大型 Excel 文件。具體來說,它通過一行一返回的方式解決了 POI 解析 Excel 非常耗費內存的問題。
  2. EasyExcel 是開源的,代碼放在 GitHub 上,如果遇到問題,可以隨時提出 issue。
  3. EasyExcel 社區活躍,網上的相關文檔也比較多,這對于使用者來說是一個很大的優勢。
  4. 雖然 POI 是目前使用最多的用來做 excel 解析的框架,但其 userModel 模式在處理大文件時存在明顯的缺陷,比如內存消耗大和有并發問題等。而 EasyExcel 則很好地解決了這些問題。
  5. EasyExcel 底層對象其實還是使用 poi 包的那一套,只是將 poi 包的一部分抽了出來,摒棄掉了大部分業務相關的屬性。

總的來說,EasyExcel 在處理大數據量的 Excel 文件導出方面,相比 POI 具有明顯的優勢,這也是為什么越來越多人選擇使用 EasyExcel 的原因。

簡單來說就是,因為 EasyExcel 性能更好。

EasyExcel 簡介

EasyExcel 是一個基于 Java 的開源庫,用于簡化和優化 Excel 文件的讀寫操作。它提供了一種簡單而高效的方式來處理大量數據的導入和導出,特別適用于大數據量的處理。

EasyExcel 具有以下特點:

  • 高性能:通過使用高效的數據模型和批量寫入技術,EasyExcel 能夠快速地處理大量數據,提高文件導出的效率。
  • 靈活性:EasyExcel 支持多種數據類型和格式,可以方便地導出各種類型的數據,包括文本、數字、日期等。
  • 簡潔的 API:EasyExcel 提供了簡潔易用的 API,使得開發者可以快速上手并實現文件導出功能。

Date 字段問題

報錯

{"code": "1","message": "導出文件失敗:java.lang.NoSuchMethodError: org.apache.poi.ss.usermodel.Cell.setCellValue(Ljava/time/LocalDateTime;)V"
}

這是因為要導出的列里面有 Date 類型,EasyExcel 識別不了

解決方法

1、編寫 Date 轉換器
public class DateConverter implements Converter<Date> {private static  final String PATTERN_YYYY_MM_DD = "yyyy-MM-dd";@Overridepublic Class<Date> supportJavaTypeKey() {return Date.class;}/*** easyExcel導出數據類型轉換* @param cellData* @param contentProperty* @param globalConfiguration* @return* @throws Exception*/@Overridepublic Date convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {String value = cellData.getStringValue();SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_YYYY_MM_DD);return sdf.parse(value);}/*** easyExcel導入Date數據類型轉換* @param context* @return* @throws Exception*/@Overridepublic WriteCellData<String> convertToExcelData(WriteConverterContext<Date> context) throws Exception {Date date = context.getValue();if (date == null) {return null;}SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_YYYY_MM_DD);return new WriteCellData<>(sdf.format(date));}}

然后,修改導出視圖類中的 Date 類型字段,加入 converter = DateConverterUtil.class 轉換屬性。

	@ExcelProperty(value = "最新維保時間", index = 3, converter = DateConverter.class)@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")private Date time;
2、用官方的注解

使用 EasyExcel 的 @DateTimeFormat 注解:

// 字段類型為String,否則注解可能會無效
@DateTimeFormat(value = "yyyy-MM-dd HH:mm:ss")
private String alarmTime;

對象轉換使用案例:

1、在需要使用時,轉換時間值

        // 1、獲取導出列表List<Alarm> list = alarmService.list();List<AlarmExportVO> list2 = list.stream().map(item -> {AlarmExportVO exportVO = new AlarmExportVO();exportVO.setEnterpriseTown(item.getEnterpriseTown());exportVO.setMarketSupervision(item.getMarketSupervision());exportVO.setDeviceName(item.getDeviceName());exportVO.setAlarmType(item.getAlarmType());exportVO.setAlarmLevel(item.getAlarmLevel());// item.getAlarmTime() 返回的是一個 Date 對象Date alarmTime = item.getAlarmTime(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");String formattedTime = sdf.format(alarmTime);exportVO.setAlarmTime(formattedTime);

2、在實體類中進行修改,重寫 get 方法

    public String getAlarmTime() {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");return sdf.format(alarmTime);}

自定義字典映射轉換器

反例:

直接用 @Resource 注入 bean 使用,會報錯,不被 Spring IoC 管理。

		List<DictDTO> dictList = documentMapper.getDict(CERTIFICATE);

報錯:

注入的 bean,查數據庫的時候,報 空指針異常,導致轉換數據的時候第一條字典映射轉換就失敗

原因猜測:

  • 在 EasyExcel 中,轉換器通常是默認通過構造函數 new 出來的,而不是由 Spring 容器管理的 Bean
  • 因此,在轉換器中,如果你需要使用 Spring 容器中的其他 Bean,你需要手動獲取這些 Bean,而不是通過 Spring 的依賴注入。

💡 Spring 管理的 bean 通常是由 Spring 容器負責創建、配置和管理的。當你使用 new 運算符直接實例化一個對象時,這個對象不會由 Spring 容器來管理,因此 Spring 不會介入該對象的生命周期和依賴注入。

解決方法

通過 Spring 容器提供的方法來獲取已經由 Spring 管理的 bean。

		DocumentMapper bean = SpringUtil.getBean(DocumentMapper.class);List<DictDTO> dictList = bean.getDict(CERTIFICATE);

參考:

EasyExcel 使用Converter 轉換注入時報nullPoint異常_converter null入參_地平線上的新曙光的博客-CSDN博客

完整代碼:

public class DocumentDictConverter implements Converter<String> {@Overridepublic Class<?> supportJavaTypeKey() {return String.class;}@Overridepublic CellDataTypeEnum supportExcelTypeKey() {return CellDataTypeEnum.STRING;}@Overridepublic String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {return cellData.getStringValue();}/*** 這里是寫的時候會調用** @return*/@Overridepublic WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {// 獲取字典列表DocumentMapper bean = SpringUtil.getBean(DocumentMapper.class);List<DictDTO> dictList = bean.getDict(CERTIFICATE);HashMap<String, String> dictMap = new HashMap<>(16);for (DictDTO dictDTO : dictList) {dictMap.put(dictDTO.getDictValue(), dictDTO.getDictName());}// 根據字典映射進行值轉換String convertedValue = dictMap.get(context.getValue());if (convertedValue != null) {return new WriteCellData<>(convertedValue);} else {return new WriteCellData<>(context.getValue());}}}

性能問題

每次做字段值映射轉換的時候都需要查數據庫,太影響性能了。

  1. 考慮引入緩存,達到只需查詢一次數據即可。(使用 Spring 框架的緩存注解 @Cacheable
  2. 將字典數據在服務啟動時加載到內存中,并在轉換器中直接使用內存中的字典數據而不是每次都查詢數據庫。
  3. 聲明靜態變量解決。(推薦)

解決方案

一、使用 Spring 框架的緩存注解

參考筆者這篇文章:Cacheable注解小記 | DreamRain

使用說明:

  • 該方法使用 @Cacheable("dictionaryCache") 注解來定義緩存區域為 “dictionaryCache”,并且可以根據 dictionaryType 參數來查詢字典數據。
  • 當方法第一次被調用時,數據將被查詢并放入緩存中,以后的調用將直接從緩存中獲取數據。
  • 緩存找不到時會報錯,轉換失敗
二、將字典數據在服務啟動時加載到內存中
  1. 創建一個單例的字典數據加載類,該類在應用啟動時加載字典數據到內存中。你可以使用 @PostConstruct 注解來標記一個初始化方法,該方法在 Spring 容器加載完所有 bean 后執行。
@Service
public class DictionaryDataService {private Map<String, String> certificateDict = new HashMap<>();@Autowiredprivate DocumentExpiredMapper documentExpiredMapper;// 這個方法會在 bean 初始化時被自動調用@PostConstructpublic void init() {// 在 bean 初始化時執行一些初始化操作List<DictDTO> dictList = documentExpiredMapper.getDict(CERTIFICATE);for (DictDTO dictDTO : dictList) {certificateDict.put(dictDTO.getDictValue(), dictDTO.getDictName());}}public String getCertificateDictValue(String key) {return certificateDict.get(key);}
}

2、修改轉換器類,使用內存中的字典數據進行轉換:

    @Overridepublic WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {DictionaryDataService bean = SpringUtil.getBean(DictionaryDataService.class);String convertedValue = bean.getCertificateDictValue(context.getValue());if (convertedValue != null) {return new WriteCellData<>(convertedValue);} else {return new WriteCellData<>(context.getValue());}}

通過這種方式,字典數據在應用啟動時加載到內存中,以后的值轉換操作都會使用內存中的數據,避免了重復查詢數據庫的性能開銷。這是一種常見的性能優化方法。

三、聲明靜態變量解決(推薦)
  • 在每次調用導出接口時都查一次數據庫,并只需查詢一次。
  • 從而減輕了數據值映射轉換時查詢數據庫的壓力,并且確保數據為最新數據,還無需考慮刪除緩存問題。
@Service
public class DictionaryService {// 聲明靜態變量存儲字典數據public static HashMap<String, HashMap<String, String>> hashMap = new HashMap<>();@Resourceprivate DocumentExpiredMapper documentExpiredMapper;/*** 將字典數據存入靜態變量* @param dictionaryType* @return*/public HashMap<String, String> getDict(String dictionaryType) {if (hashMap.containsKey(dictionaryType)){return hashMap.get(dictionaryType);}List<DictDTO> dictList = documentExpiredMapper.getDict(dictionaryType);HashMap<String, String> dictMap = new HashMap<>(dictList.size());for (DictDTO dictDTO : dictList) {dictMap.put(dictDTO.getDictValue(), dictDTO.getDictName());}hashMap.put(dictionaryType, dictMap);return dictMap;}}

前兩者優劣分析

第一種方法(DictionaryService 使用緩存):

優點:

  1. 使用了 @Cacheable 注解,Spring 會自動處理緩存相關邏輯,包括緩存的清除、存儲、失效等,減輕了你的工作負擔。
  2. 緩存數據在運行時動態從數據庫中獲取,因此數據保持最新,不需要手動更新。
  3. 可以靈活地在其他地方使用 DictionaryService 服務,而不需要關心緩存細節。

缺點:

  1. 需要依賴 Spring 緩存機制,可能需要較多配置和依賴,不如手動控制靈活。
  2. 當有多個不同字典類型需要緩存時,可能需要創建多個不同的緩存,增加了管理復雜度。

第二種方法(DictionaryDataService 使用內存緩存):

優點:

  1. 簡單明了,不依賴 Spring 緩存機制,適用于小規模應用或特定場景。
  2. 在應用啟動時加載字典數據到內存中,查詢字典數據的速度非常快,適用于頻繁查詢字典數據的場景。

缺點:

  1. 手動加載字典數據到內存,如果數據庫中的數據發生變化,需要手動同步內存數據,容易出現數據不一致的問題。
  2. 不支持自動過期和失效處理,需要自己編寫邏輯來處理緩存的更新和失效。
  3. 在大規模應用中,如果內存占用較多,可能會影響應用性能。

場景分析:

  • 如果你的應用要求字典數據保持實時性,能夠自動過期和更新,使用第一種方法更為合適。
  • 如果應用規模較小、字典數據變化不頻繁,或者希望簡化配置,第二種方法也是一個不錯的選擇。
  • 理論上來說,第一種用的更為廣泛

@PostConstruct 注解

@PostConstruct 是 Java EE(Enterprise Edition)的注解之一,它標識在類實例化后,但在類投入使用之前要執行的方法。通常在使用 Spring 框架或其他依賴注入框架時,@PostConstruct 注解用于在 bean 的初始化過程中執行一些額外的初始化操作。以下是關于 @PostConstruct 注解的一些重要信息:

  1. 生命周期回調方法@PostConstruct 用于定義在 bean 的生命周期中何時應該執行的初始化方法。它提供了一個方便的方式來執行一些準備工作,如數據加載、資源初始化等。
  2. 執行時機@PostConstruct 注解的方法會在 Spring 容器創建 bean 實例后,依賴注入之前執行。這意味著它是在 bean 的構造函數之后,依賴注入之前執行的,用于初始化 bean 的各種屬性。
  3. 方法簽名:被 @PostConstruct 注解的方法沒有參數。方法名可以隨意命名,但通常為 initinitializepostConstruct 等。
  4. 依賴注入和容器管理@PostConstruct 注解通常與依賴注入和容器管理框架(如 Spring、Java EE 容器等)一起使用。容器會在執行構造函數和依賴注入后,自動調用被 @PostConstruct 注解的初始化方法。
  5. 異常處理:如果 @PostConstruct 注解的方法拋出異常,容器會將異常捕獲并處理,通常會導致 bean 創建失敗。這可以用于在初始化階段檢測配置錯誤或其他問題。
  6. 多次調用@PostConstruct 注解的方法只會被調用一次,即使 bean 在容器中被多次注入也是如此。
  7. 典型用途@PostConstruct 常用于執行一些需要在 bean 初始化時進行的操作,例如數據庫連接的建立、資源初始化、數據加載等。

內存與緩存

以下是 “加載到內存” 和 “放入緩存” 的相關概念。

  1. 加載到內存

    • 加載到內存通常指將數據、資源或對象從持久存儲(如硬盤或數據庫)加載到【計算機的內存】中,以便在應用程序中使用。
    • 這是一個通用操作,常見于應用程序的啟動過程或在需要訪問數據時。
  2. 放入緩存

    • 放入緩存是一種特定的加載到內存操作,它指的是將數據或計算結果存儲在一個【臨時存儲區域】中,通常在內存中,以提高后續訪問的性能。
    • 緩存通常包括緩存鍵(用于檢索數據)和緩存值(實際數據或計算結果)。
  3. 內存加載的情況

    • 內存加載可能是一次性的,例如在應用程序啟動時加載配置文件。(一次性初始化)

    • 內存加載也可能是動態的,例如從數據庫中加載實時數據或通過用戶請求加載。

      • 如果數據的變化頻率較低,可以使用定時刷新
      • 如果數據變化頻繁,懶加載異步加載可能更合適。
      • 緩存加載器適用于需要復雜邏輯來獲取數據的情況。
  4. 緩存的情況

    • 緩存是一種性能優化技術,通過將頻繁訪問的數據存儲在內存中,以減少重復訪問持久存儲的開銷。
    • 緩存通常采用一定策略,例如緩存過期時間或根據內存大小來管理緩存。

綜上所述:

  • 加載到內存是一種廣泛的操作,它可以用于不同的用途,
  • 而緩存是一種內存加載的具體應用,它的主要目的是提高數據訪問的性能。緩存通常包括一些管理策略,以確保緩存數據的有效性和一致性。

在這里插入圖片描述

使用案例

參考官方文檔案例:web中的寫并且失敗的時候返回json

	@Overridepublic void exportDocument(DocumentDTO documentDTO, HttpServletResponse response) throws IOException {// 1、獲取證件臨期告警列表List<DocumentVO> documentList = getDocumentList(documentDTO);// 2、 Excel文件標題String title = "Excel文件標題";// 3、告警類型值替換List<DocumentExportVO> exportVOList = BeanUtil.copy(documentList, DocumentExportVO.class);// 4、EasyExcel導出try {// 設置內容類型response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");// 設置字符編碼response.setCharacterEncoding("utf-8");// 這里URLEncoder.encode可以防止中文亂碼 當然和easy excel沒有關系String fileName = URLEncoder.encode(title, "UTF-8").replaceAll("\\+", "%20");// 設置響應頭response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");// 這里需要設置不關閉流EasyExcel.write(response.getOutputStream(), DocumentExportVO.class).autoCloseStream(Boolean.FALSE).sheet("模板").doWrite(exportVOList);} catch (Exception e) {// 重置responseresponse.reset();String errorMessage = "導出文件失敗:" + e.getMessage();returnResult(response, errorMessage);// e.printStackTrace();}}

代碼規范問題:

// 將錯誤信息全部打印在控制臺
e.printStackTrace();
  • 全部錯誤信息打印出來,有助于排查 轉換器類 的問題(不會打印到日志文件中,但會一直刷控制臺)
  • 但是一般生產情況不能打印出來,因為可能會引發事故

導出圖片并壓縮

代碼倉庫:excel-demo

要壓縮圖片,您可以使用 Java 中的圖像處理庫,例如 ImageIO 或 Thumbnails 庫。

下面是使用 Thumbnails 庫壓縮圖片的示例講述。

1、添加依賴

首先,確保您已將 Thumbnails 庫添加到您的項目依賴項中。在 Maven 項目中,您可以在 pom.xml 文件中添加以下依賴項:

<dependency><groupId>net.coobird</groupId><artifactId>thumbnailator</artifactId><version>0.4.14</version>
</dependency>

2、使用 Thumbnails 庫內部方法

  1. 使用 File.createTempFile() 方法創建一個臨時文件,然后通過 toFile() 方法將壓縮后的圖片保存到臨時文件中。最后,將臨時文件的路徑設置到 ExcelExportVO 對象的 file 屬性中。

    臨時文件的生命周期由操作系統管理,通常在程序退出后會自動刪除。(可以設置手動刪除)

  2. 使用 Thumbnails.of() 方法加載原始圖片,然后通過 scale() 方法設置壓縮比例。

            try {// 1.1 壓縮圖片并保存到臨時文件File compressedFile = File.createTempFile("compressed_image", ".jpg");// 1.2 壓縮圖片Thumbnails.of(new URL(item.getFile())).scale(0.5) // 設置壓縮比例.toFile(compressedFile);// 1.3 將臨時文件路徑設置到ExcelExportVO對象中exportVO.setFile(compressedFile.toURI().toURL());} catch (MalformedURLException e) {throw new RuntimeException(e);} catch (IOException e) {throw new RuntimeException("壓縮圖片失敗!!!", e);}

壓縮比例講解

選擇壓縮比例的大小取決于您的需求和偏好,以及圖像的具體情況。

  • 較高的壓縮比例會導致圖像更大程度地被壓縮,文件大小更小,但可能會損失一些圖像細節和質量。
  • 較低的壓縮比例可以保留更多的圖像細節和質量,但文件大小會相對較大。

總結

  • 一般來說,如果您更關注圖像的質量和細節保留,可以選擇較低的壓縮比例,如 0.8。這樣可以在一定程度上減小文件大小,同時保持圖像的視覺質量。
  • 如果您更關注文件大小的減小,可以選擇較高的壓縮比例,如 0.5,以獲得更小的文件大小,但可能會犧牲一些圖像細節和質量。
  • 壓縮比例值越小,文件大小就越小。

3、完整代碼示例

手動刪除資源的寫法

這種壓縮的效果會更好,但是需要手動刪除資源。

    @Testvoid export() {// 1、獲取導出列表 List<TblExcel> list = excelService.list();List<ExcelExportVO> list2 = list.stream().map(item -> {ExcelExportVO exportVO = new ExcelExportVO();exportVO.setName(item.getName());try {// 1.1 壓縮圖片并保存到臨時文件File compressedFile = File.createTempFile("compressed_image", ".jpg");// 1.2 壓縮圖片Thumbnails.of(new URL(item.getFile())).scale(0.5) // 設置壓縮比例.toFile(compressedFile);// 1.3 將臨時文件路徑設置到ExcelExportVO對象中exportVO.setFile(compressedFile.toURI().toURL());} catch (MalformedURLException e) {throw new RuntimeException(e);} catch (IOException e) {throw new RuntimeException("壓縮圖片失敗!!!", e);}return exportVO;}).collect(Collectors.toList());// 2、導出EasyExcel.write("D:\\TblExcel.xls").sheet("模板").head(ExcelExportVO.class).doWrite(list2);// 3、使用完畢后手動刪除臨時文件for (ExcelExportVO exportVO : list2) {try {File compressedFile = new File(exportVO.getFile().toURI());if (!compressedFile.delete()) {// 刪除操作失敗,記錄日志或進行其他錯誤處理log.error("刪除臨時文件失敗: " + compressedFile.getAbsolutePath());}} catch (Exception e) {// 處理刪除臨時文件的異常e.printStackTrace();}}}
自動刪除資源的寫法
  • 在使用 try-with-resources 來壓縮圖像并保存到臨時文件后,該臨時文件會在 try-with-resources 塊結束時自動關閉。因此,我們無需另外手動操作刪除臨時文件。
  • 但是這種方法壓縮的效果沒有上面那種方法好。

代碼如下:

    /*** Thumbnails 壓縮圖片導出 -- 使用 try-with-resources 自動關閉資源版* 壓縮效果較差*/@Testvoid export11() {// 1、獲取導出列表List<TblExcel> list = excelService.list();List<ExcelExportVO> list2 = list.stream().map(item -> {ExcelExportVO exportVO = new ExcelExportVO();exportVO.setName(item.getName());try {// 壓縮圖像并保存到臨時文件File compressedFile;try (OutputStream outputStream = new FileOutputStream(compressedFile = File.createTempFile("compressed_image", ".jpg"))) {Thumbnails.of(new URL(item.getFile())).scale(0.5) // 設置壓縮比例.toOutputStream(outputStream);}// 將臨時文件路徑設置到ExcelExportVO對象中exportVO.setFile(compressedFile.toURI().toURL());} catch (IOException e) {throw new RuntimeException("壓縮圖片失敗!!!", e);}return exportVO;}).collect(Collectors.toList());// 2、導出EasyExcel.write("D:\\TblExcel.xls").sheet("模板").head(ExcelExportVO.class).doWrite(list2);}
原因分析
  1. 在第一個方法中,我使用了 Thumbnails.of(new URL(item.getFile())).scale(0.5).toFile(compressedFile); 這一行代碼來壓縮圖片。這個方法將壓縮后的圖片直接保存到了文件系統中,然后在 ExcelExportVO 對象中設置的是臨時文件的路徑。

  2. 而在第二個方法中,我使用了 Thumbnails.of(new URL(item.getFile())).scale(0.5).toOutputStream(outputStream); 這一行代碼來壓縮圖片。這個方法將壓縮后的圖片數據寫入到了一個輸出流(OutputStream)中,而沒有將其直接保存到文件系統。這意味著,雖然壓縮后的圖片數據被存儲在了內存中的字節數組中,但并沒有實際地創建一個新的臨時文件。因此,在第二個方法中,ExcelExportVO 對象中的文件路徑實際上指向的是一個尚未存在的臨時文件。

當 EasyExcel 將這些對象寫入到 Excel 文件時,它會嘗試打開每個 ExcelExportVO 對象中的文件路徑。在第一個方法中,因為臨時文件已經存在,所以 EasyExcel 可以成功地打開并讀取這些文件。但在第二個方法中,由于臨時文件并未實際創建,所以 EasyExcel 無法打開這些文件。

因此,盡管兩個方法都實現了壓縮圖片的功能,但由于第二個方法沒有將壓縮后的圖片數據實際保存到文件系統中,所以在導出的 Excel 文件中可能不會包含這些圖片數據,從而導致導出的文件更小。

總結
  1. 第一種,需要手寫代碼刪除臨時文件,但是壓縮效果
  2. 第二種,不需要手寫代碼刪除臨時文件,但是壓縮效果較差

圖片壓縮效果說明

測試數據:100 條記錄,每條記錄包含一張【圖片URL】。

  1. 壓縮前:Excel 文件大小為 67.8M
  2. 壓縮后
    • (手動刪除資源版)Excel 文件大小為 2.68M
    • (自動關閉資源版)Excel 文件大小為 23.7M
  3. 選擇壓縮比例為 0.8 時,文件大小為 6.01M

指定字段導出

代碼如下:

        // 根據用戶傳入字段 假設我們只要導出 fileSet<String> includeColumnFiledNames = new HashSet<String>();includeColumnFiledNames.add("file");// 2、導出EasyExcel.write("D:\\TblExcel.xls").sheet("模板").head(ExcelExportVO.class).includeColumnFieldNames(includeColumnFiledNames)// .includeColumnIndexes(Collections.singleton(1)).doWrite(list2);

具體說明:

  1. includeColumnFieldNames 是根據字段名指定導出列(建議導出視圖 VO 不要指定 index 屬性,否則會有導出會有空列

  2. includeColumnIndexes 是根據索引指定導出列,即使實體類中沒有指定 index 屬性一樣可以使用

    • List<Integer> columnList = Arrays.asList(0, 1, 2, 3, 4);
      

自動列寬

使用官方自帶的處理器:

                // 自動列寬.registerWriteHandler((new LongestMatchColumnWidthStyleStrategy()))

具體說明:

  • 可以自己根據官方的父類繼承,重寫處理器來使用;
  • 也可結合【自動列寬處理器 + 實體類注解】來一起使用。

自定義自適應列寬處理策略:

  • 使用動態表頭時,官方的自動列寬策略不夠好用,所以重寫了一下方法
  • 以下內容是指定第四列的列寬,其余列自定義
public class CustomColumnWidthStyleStrategy extends AbstractColumnWidthStyleStrategy {public CustomColumnWidthStyleStrategy() {}@Overrideprotected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {Sheet sheet = writeSheetHolder.getSheet();sheet.setColumnWidth(3, 8000);}}

合并單元格導出

1、自定義Excel合并表格策略

  • 需要實現 CellWriteHandler,Cell 是列,ROW 是行
  • 這里針對的是合并處理
public class CustomMergeStrategy implements CellWriteHandler {/*** 合并列索引。*/private final List<Integer> mergeColumnIndexes;/*** 構造函數。** @param mergeColumnIndexes 合并列索引集合。*/public CustomMergeStrategy(List<Integer> mergeColumnIndexes) {this.mergeColumnIndexes = mergeColumnIndexes;}@Overridepublic void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {// 校驗:如果當前是表頭,則不處理。if (isHead) {return;}// 校驗:如果當前是第一行,則不處理。if (relativeRowIndex == 0) {return;}// 校驗:如果當前列索引不在合并列索引列表中,則不處理。Integer columnIndex = cellDataList.get(0).getColumnIndex();if (!this.mergeColumnIndexes.contains(columnIndex)) {return;}// 獲取:當前表格、當前行下標、上一行下標、上一行對象、上一列對象。Sheet sheet = cell.getSheet();int rowIndexCurrent = cell.getRowIndex();int rowIndexPrev = rowIndexCurrent - 1;Row rowPrev = sheet.getRow(rowIndexPrev);Cell cellPrev = rowPrev.getCell(cell.getColumnIndex());// 獲取:當前單元格值、上一單元格值。Object cellValueCurrent = cell.getCellTypeEnum() == CellType.STRING ? cell.getStringCellValue() : cell.getNumericCellValue();Object cellValuePrev = cellPrev.getCellTypeEnum() == CellType.STRING ? cellPrev.getStringCellValue() : cellPrev.getNumericCellValue();// 校驗:如果當前單元格值與上一單元格值不相等,則不處理。if (!cellValueCurrent.equals(cellValuePrev)) {return;}List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();boolean merged = false;for (int i = 0; i < mergedRegions.size(); i++) {CellRangeAddress cellRangeAddress = mergedRegions.get(i);if (cellRangeAddress.isInRange(rowIndexPrev, cell.getColumnIndex())) {// 移除合并單元格。sheet.removeMergedRegion(i);// 設置合并單元格的結束行。cellRangeAddress.setLastRow(rowIndexCurrent);// 重新添加合并單元格。sheet.addMergedRegion(cellRangeAddress);merged = true;break;}}if (!merged) {CellRangeAddress cellRangeAddress = new CellRangeAddress(rowIndexPrev, rowIndexCurrent, cell.getColumnIndex(), cell.getColumnIndex());sheet.addMergedRegion(cellRangeAddress);}}}

2、實際使用

		  // ......// 導出EasyExcel.write(response.getOutputStream()).head(headList)// 自動列寬.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())// .registerWriteHandler(new CustomMergeStrategy(Arrays.asList(1, 2))).registerWriteHandler(new CustomMergeStrategy(Collections.singletonList(1))).autoCloseStream(Boolean.FALSE).sheet("模板").doWrite(resultList);// ......// 獲取表格頭列表private List<List<String>> getHeadList(SettleDTO settleDTO) {List<List<String>> headList = new ArrayList<>();Date startDate = DateUtil.parse(settleDTO.getStartDay(), "yyyy-MM-dd");Date endDate = DateUtil.parse(settleDTO.getEndDay(), "yyyy-MM-dd");SimpleDateFormat sdf = new SimpleDateFormat("MM月dd日");String startDay = sdf.format(startDate);String endDay = sdf.format(endDate);String day = startDay + "-" + endDay;ArrayList<String> headColumn1 = new ArrayList<>();headColumn1.add("第一列");headList.add(headColumn1);ArrayList<String> headColumn2 = new ArrayList<>();headColumn2.add("第二列");headList.add(headColumn2);ArrayList<String> headColumn3 = new ArrayList<>();headColumn3.add(day);headColumn3.add("第三列");headList.add(headColumn3);return headList;}

代碼解析

拓展性說明:

  • 做了一個拓展性處理,可根據列索引來指定需要執行合并的列。

以下是 afterCellDispose 方法的各參數的含義:

  1. WriteSheetHolder writeSheetHolder:當前正在寫入的 Sheet 的持有者。它包含有關當前 Sheet 的信息,例如 Sheet 的索引、當前行的索引以及其他相關詳細信息。

  2. WriteTableHolder writeTableHolder:當前正在寫入的表格的持有者。它包含有關當前表格的信息,例如表格的名稱、起始行的索引以及其他相關詳細信息。

  3. List<WriteCellData<?>> cellDataList:這是一個 WriteCellData 對象的列表,表示要寫入當前單元格的數據。如果單元格跨越多個列或行,該列表可能包含多個 WriteCellData 對象。

  4. Cell cell:表示當前正在處理的單元格。它提供有關單元格位置、樣式和其他屬性的信息。

  5. Head head:表示當前單元格的頭部(標題)。它包含有關頭部的信息,如字段名、類類型以及其他相關詳細信息。

    • 如果傳入的不是一個 class 文件(實體類)時,head 可能會為 null,例如上面示例(.head(headList)),傳入的是個列表集合,此時 head 為 null

  6. Integer relativeRowIndex:表示當前行在當前 Sheet 中的相對索引。一開始就是表頭之下的第一行。

  7. Boolean isHead:這是一個布爾標志,指示當前單元格是否為表頭單元格。如果 isHeadtrue,則表示當前單元格是表頭單元格;否則,它是數據單元格。

總的來說,這些參數提供了有關當前 Excel 寫入過程狀態的上下文信息。它們允許您基于正在處理的 Sheet、表格、單元格和頭部,以及當前行在 Sheet 中的位置執行自定義邏輯。

打包成壓縮包導出

壓縮包導出有以下兩種情況:

  1. 指定本地路徑導出
  2. 寫入響應流導出

指定路徑導出

一、可以使用 hutool 的 ZipUtil 工具類

    /*** 壓縮包導出 -- hutool*/@Testvoid export6() throws IOException {List<TblExcel> list = new ArrayList<>();list.add(new TblExcel("張三", "abc"));List<ByteArrayInputStream> ins = new ArrayList<>();// 導出第一個ExcelByteArrayOutputStream out1 = new ByteArrayOutputStream();EasyExcel.write(out1, TblExcel.class).sheet("第一個").doWrite(list);ins.add(new ByteArrayInputStream(out1.toByteArray()));// 導出第二個ExcelByteArrayOutputStream out2 = new ByteArrayOutputStream();EasyExcel.write(out2, TblExcel.class).sheet("第二個").doWrite(list);ins.add(new ByteArrayInputStream(out2.toByteArray()));// 將多個 InputStream 壓縮到一個 zip 文件File zipFile = new File("C:\\Users\\喬\\Desktop\\noModelWrite.zip");String[] fileNames = {"1.xlsx", "2.xlsx"};InputStream[] inputStreams = ins.toArray(new InputStream[0]);cn.hutool.core.util.ZipUtil.zip(zipFile, fileNames, inputStreams);}

二、手寫一個 ZipUtil 工具類

public class ZipUtil {/*** 默認編碼,使用平臺相關編碼*/private static final Charset DEFAULT_CHARSET = Charset.defaultCharset();/*** 將文件流壓縮到目標流中** @param out       目標流,壓縮完成自動關閉* @param fileNames 流數據在壓縮文件中的路徑或文件名* @param ins       要壓縮的源,添加完成后自動關閉流*/public static void zip(OutputStream out, List<String> fileNames, List<InputStream> ins) throws IOException {zip(out, fileNames.toArray(new String[0]), ins.toArray(new InputStream[0]));}/*** 將文件流壓縮到目標流中** @param out       目標流,壓縮完成自動關閉* @param fileNames 流數據在壓縮文件中的路徑或文件名* @param ins       要壓縮的源,添加完成后自動關閉流*/public static void zip(File out, List<String> fileNames, List<InputStream> ins) throws IOException {FileOutputStream outputStream = new FileOutputStream(out);zip(outputStream, fileNames.toArray(new String[0]), ins.toArray(new InputStream[0]));outputStream.flush();}/*** 將文件流壓縮到目標流中** @param out       目標流,壓縮完成自動關閉* @param fileNames 流數據在壓縮文件中的路徑或文件名* @param ins       要壓縮的源,添加完成后自動關閉流*/public static void zip(OutputStream out, String[] fileNames, InputStream[] ins) throws IOException {ZipOutputStream zipOutputStream = null;try {zipOutputStream = getZipOutputStream(out, DEFAULT_CHARSET);zip(zipOutputStream, fileNames, ins);} catch (IOException e) {throw new IOException("壓縮包導出失敗!", e);} finally {IOUtils.closeQuietly(zipOutputStream);}}/*** 將文件流壓縮到目標流中** @param zipOutputStream 目標流,壓縮完成不關閉* @param fileNames       流數據在壓縮文件中的路徑或文件名* @param ins             要壓縮的源,添加完成后自動關閉流* @throws IOException IO異常*/public static void zip(ZipOutputStream zipOutputStream, String[] fileNames, InputStream[] ins) throws IOException {if (ArrayUtils.isEmpty(fileNames) || ArrayUtils.isEmpty(ins)) {throw new IllegalArgumentException("文件名不能為空!");}if (fileNames.length != ins.length) {throw new IllegalArgumentException("文件名長度與輸入流長度不一致!");}for (int i = 0; i < fileNames.length; i++) {add(ins[i], fileNames[i], zipOutputStream);}}/*** 添加文件流到壓縮包,添加后關閉流** @param in       需要壓縮的輸入流,使用完后自動關閉* @param fileName 壓縮的路徑* @param out      壓縮文件存儲對象* @throws IOException IO異常*/private static void add(InputStream in, String fileName, ZipOutputStream out) throws IOException {if (null == in) {return;}try {out.putNextEntry(new ZipEntry(fileName));IOUtils.copy(in, out);} catch (IOException e) {throw new IOException(e);} finally {IOUtils.closeQuietly(in);closeEntry(out);}}/*** 獲得 {@link ZipOutputStream}** @param out     壓縮文件流* @param charset 編碼* @return {@link ZipOutputStream}*/private static ZipOutputStream getZipOutputStream(OutputStream out, Charset charset) {if (out instanceof ZipOutputStream) {return (ZipOutputStream) out;}return new ZipOutputStream(out, DEFAULT_CHARSET);}/*** 關閉當前Entry,繼續下一個Entry** @param out ZipOutputStream*/private static void closeEntry(ZipOutputStream out) {try {out.closeEntry();} catch (IOException e) {// ignore}}
}

測試代碼如下:

    @Testvoid export5() throws IOException {List<TblExcel> list = new ArrayList<>();list.add(new TblExcel("張三", "abc"));List<InputStream> ins = new ArrayList<>();OutputStream out1 = new ByteArrayOutputStream();OutputStream out2 = new ByteArrayOutputStream();// 2、導出EasyExcel.write(out1).sheet("第一個").head(ExcelExportVO.class).doWrite(list2);ins.add(outputStream2InputStream(out1)); // 寫法可參考上一個 hutool 的示例EasyExcel.write(out2).sheet("第二個").head(ExcelExportVO.class).doWrite(list2);ins.add(outputStream2InputStream(out2));File zipFile = new File("C:\\Users\\喬\\Desktop\\noModelWrite.zip");// 壓縮包內流的文件名List<String> paths = Arrays.asList("1.xlsx", "2.xlsx");ZipUtil.zip(zipFile, paths, ins); // 工具類使用}/*** 輸出流轉輸入流;數據量過大請使用其他方法** @param out* @return*/private ByteArrayInputStream outputStream2InputStream(OutputStream out) {Objects.requireNonNull(out);ByteArrayOutputStream bos;bos = (ByteArrayOutputStream) out;return new ByteArrayInputStream(bos.toByteArray());}

寫入響應流導出

    @Overridepublic void exportSettleZip(TestDTO dto, HttpServletResponse response) throws IOException {// 1、參數校驗if (StringUtils.isEmpty(dto.getStartDay()) || StringUtils.isEmpty(dto.getEndDay())) {throw new IllegalArgumentException("日期不能為空!!");}// 2、獲取所有入駐企業的行業類型List<String> filedTypes = FiledTypeEnum.getValues();// 3、所有Excel導出并壓縮ByteArrayOutputStream zipStream = new ByteArrayOutputStream();try (ZipOutputStream zipOut = new ZipOutputStream(zipStream)) { // zipOutfor (String filedType : filedTypes) {// 獲取當前行業類型的入駐企業數據List<TestVO> entSettleList = enterpriseMapper.getEntSettleList(dto, filedType);// 創建一個字節流,用于存儲當前行業類型的 Excel 數據ByteArrayOutputStream excelStream = new ByteArrayOutputStream();// 使用 EasyExcel 導出 Excel 數據EasyExcel.write(excelStream).head(getHeadList(dto)) // 獲取表頭.registerWriteHandler(new CustomColumnWidthStyleStrategy()) // 自適應列寬策略.registerWriteHandler(new CustomMergeStrategy(Collections.singletonList(1))) // 單元格合并策略.sheet(filedType + "模板") // 設置 Excel 表格名.doWrite(entSettleList); // 寫入 Excel 數據// 將 Excel 寫入 ZipOutputStream -- 這三行代碼用于將一個 Excel 文件的數據寫入到 ZIP 文件中ZipEntry zipEntry = new ZipEntry(filedType + "信息導出.xlsx"); // 表示 ZIP 文件中的一個文件名稱zipOut.putNextEntry(zipEntry); // 將剛剛創建的 ZipEntry 對象添加到 ZipOutputStream 中,表示開始寫入 ZIP 文件的一個新文件。zipOut.write(excelStream.toByteArray()); // 將之前在內存中生成的 Excel 文件數據寫入到 ZIP 文件中的當前條目// 關閉當前 ZipEntryzipOut.closeEntry();// 關閉當前 Excel 字節流excelStream.close();}}// 4、將壓縮包寫入響應流String start = dto.getStartDay().replaceAll("-", "");String end = dto.getEndDay().replaceAll("-", "");String zipName = "模板數據" + start + "-" + end + ".zip";zipName = URLEncoder.encode(zipName, "UTF-8");try {response.setContentType("application/zip");response.setHeader("Content-Disposition", "attachment;filename=" + zipName);response.getOutputStream().write(zipStream.toByteArray()); // 重點關注} finally {// 關閉 ByteArrayOutputStreamzipStream.close();}}

代碼用途

  • 這段代碼實例,實現了根據不同的行業類型導出對應的 Excel 文件,并將這些 Excel 文件壓縮成一個 ZIP 文件。
  • 主要用于在 Web 環境下導出 Excel 數據并進行壓縮,方便用戶一次性下載多個行業類型的數據。

以下是對代碼的詳細解釋

  1. **參數校驗:**檢查傳入的日期參數是否為空,若為空則拋出異常。
  2. **獲取所有的行業類型:**通過 FiledTypeEnum.getValues() 獲取所有的行業類型。(此方法在枚舉類里面定義)
  3. **所有 Excel 導出并壓縮:**使用 ZipOutputStream 創建一個 ZIP 文件,然后遍歷所有行業類型,為每個行業類型生成對應的 Excel 文件,并將其寫入 ZIP 文件中。
    • 在每個行業類型的循環中,獲取當前行業類型的入駐企業數據。
    • 創建一個 ByteArrayOutputStream 用于存儲當前行業類型的 Excel 數據。
    • 使用 EasyExcel 導出 Excel 數據,包括設置表頭、列寬策略和單元格合并策略。
    • 將當前行業類型的 Excel 數據寫入 ZIP 文件,并關閉當前 ZipEntry。
    • 關閉當前 Excel 字節流。
  4. **將壓縮包寫入響應流:**將生成的 ZIP 文件寫入響應流,實現下載功能。設置響應頭的文件名,并使用 URLEncoder.encode 處理中文文件名。最后,關閉 ByteArrayOutputStream

以下是一些關于流的概念說明

流(Stream)是用于在程序之間傳輸數據的抽象。流可以是輸入流(Input Stream),用于從某個源讀取數據,也可以是輸出流(Output Stream),用于將數據寫入某個目標。

現在來解釋一下這段代碼中各個流的用法:

  1. ByteArrayOutputStream: 這是一個字節數組輸出流,它會在內存中創建一個字節數組緩沖區,所有寫入到這個流的數據都會被保存在這個緩沖區中。在這段代碼中,用于將每個行業類型的 Excel 數據保存在內存中。
    • 為什么需要它: 因為我們需要在內存中生成 Excel 數據,而不是將其寫入到硬盤。EasyExcel.write 方法的參數是一個輸出流,而 ByteArrayOutputStream 就是一個方便在內存中存儲字節數據的流。
    • 為什么需要關閉: 關閉流是為了釋放占用的系統資源。在這里,通過 ByteArrayOutputStreamclose 方法,確保所有關聯的資源被釋放,尤其是關閉底層的字節數組。
  2. ZipOutputStream: 這是一個用于寫入 ZIP 文件的輸出流。ZIP 文件是一種存檔文件,可以包含多個文件或目錄,通過壓縮來減小文件大小。
    • 為什么需要它: 在這段代碼中,我們希望將每個行業類型的 Excel 數據寫入一個 ZIP 文件中,以便用戶可以一次性下載多個文件。
    • 為什么需要關閉: 關閉 ZipOutputStream 將確保 ZIP 文件的完整性。在這里,通過 zipOut.closeEntry() 來關閉當前 ZIP 文件的條目(即一個文件),并準備開始下一個 ZIP 條目。
  3. ZipEntry: 在 ZIP 文件中,每個文件或目錄都對應一個條目,這個條目就是 ZipEntry。在這段代碼中,用于表示 ZIP 文件中的每個 Excel 文件。
    • 為什么需要它: 我們希望 ZIP 文件中有多個文件,每個文件對應一個行業類型的 Excel 數據。ZipEntry 就是用于表示 ZIP 文件中的每個文件。
    • 為什么需要關閉:ZipOutputStream 中,每次調用 putNextEntry 方法都會創建一個新的 ZipEntry,表示一個新的文件。通過 zipOut.closeEntry() 來關閉當前 ZIP 條目,以確保下一次寫入時不會影響到前一個 ZIP 條目。

總體來說,這些流的使用是為了在【內存】中生成多個 Excel 文件,并將這些文件寫入一個 ZIP 文件中,最終提供給用戶進行下載。關閉流是為了釋放資源,確保數據完整性。

學習參考

  • 使用easyExcel導入導出Date類型的轉換問題 (mfbz.cn)

  • 代碼規范:禁用e.printStackTrace()打印異常_e.printstacktrace()禁用-CSDN博客

  • 代碼規范之e.printStackTrace()-CSDN博客

  • 【溫情提醒】工作中要少用e.printStackTrace()的致命原因之一_printtrace問題-CSDN博客

  • 視頻:Easy Excel 13:導出圖片內容

  • 使用 easyExcel 生成多個 excel 并打包成zip壓縮包-CSDN博客

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

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

相關文章

c語言插入排序算法(詳解)

插入排序是一種簡單直觀的排序算法&#xff0c;其主要思想是將一個待排序的元素插入到已經排好序的部分的合適位置。 插入排序的原理如下&#xff1a; 將序列分為兩部分&#xff1a;已排序部分和未排序部分。初始時&#xff0c;已排序部分只包含第一個元素&#xff0c;未排序…

php 接入 百度編輯器

按照github上的操作下載百度編輯器的包后&#xff0c;根據文檔上的步驟操作&#xff08;可能會遇到報錯&#xff09;&#xff1a; 1、git clone 倉庫 2、npm install 安裝依賴&#xff08;如果沒有安裝 grunt , 請先在全局安裝 grunt&#xff09; 我的是報了下面的錯&#…

Leetcode 17 電話號碼的字母組合

理解題意&#xff1a; 給定一個僅包含數字 2-9 的字符串&#xff0c;返回所有它能表示的字母組合 本質上&#xff1a;數字代表著一個字母集合 數字的個數決定了遞歸的深度&#xff0c;即樹的深度 數字代表的字母組合決定了當前樹的寬度。 1.暴力回溯 這里沒有什么剪枝…

387.字符串中的第一個唯一字符 —> `size()`

解答&#xff1a; int firstUniqChar(string s) {int size s.size();// char count[26] { 0 };// error.1int count[26] { 0 };// for (int i 0; i < s.size() - 1; i) // error.2for (int i 0; i < size; i){count[s[i] - a] 1;}for (int i 0; i < size; i){…

Android 幸運轉盤實現邏輯

一、前言 幸運轉盤在很多app中都有&#xff0c;也有很多現實的例子&#xff0c;不過這個難度并不是如何讓轉盤轉起來&#xff0c;真正的難度是如何統一個方向轉動&#xff0c;且轉到指定的目標區域&#xff08;中獎概率從來不是隨機的&#xff09;&#xff0c;當然還不能太假&…

AI全棧大模型工程師(二十二)什么是模型訓練

文章目錄 ?? 這節課會帶給你還是寫在前面Fine-Tuning 有什么用:先看一個例子我有很多問題一、什么是:二、什么是模型2.1、通俗(不嚴謹)的說、模型是一個函數:2.2、一個最簡單的神經網絡三、什么是模型訓練3.1、模型訓練本質上是一個求解最優化問題的過程3.2、怎么求解3.…

人類的耳朵:聽覺的動態范圍

作者&#xff1a;聽覺健康 聽覺的動態范圍即可用的聽力范圍。在坐標系中&#xff0c;它可以表示為以聽閾和最大舒適級為界形成的區域&#xff0c;其坐標軸分別為頻率和聲壓級&#xff08;刺激持續時間在某種程度上對其產生影響&#xff09;。是什么因素決定了人類聽力的極限&am…

隨機森林回歸模型,SHAP庫可視化

隨機森林回歸模型 創建一個隨機森林回歸模型&#xff0c;訓練模型&#xff0c;然后使用SHAP庫解釋模型的預測結果&#xff0c;并將結果可視化。 具體步驟如下&#xff1a; 首先&#xff0c;代碼導入了所需的庫&#xff0c;包括matplotlib、shap、numpy和sklearn.ensemble。ma…

Compilation failureFailure executing javac, but could not parse the error

記一次maven編譯錯誤導致的打包失敗問題。錯誤如下 Compilation failure Failure executing javac, but could not parse the error: javac: Ч ? : ? ? : javac <options> <source files> -help г ? ? 排查路徑如下&#xff1a; 1&#xff…

[原創] FPGA的JTAG燒錄不穩定或燒錄失敗原因分析

一、電路故障背景 打板回來常會出現燒錄不良&#xff0c;調試是一個技術活&#xff0c;如果燒錄不過關&#xff0c;一切白搭。 二、常見JTAG故障原因如下&#xff1a; 1、ESD防護器件焊接不良&#xff1b; 電路板給生產部分焊接&#xff0c;發現元器件虛焊&#xff0c;特別是…

【MySQL】MySQL的varchar字段最大長度是65535?

在MySQL建表sql里,我們經常會有定義字符串類型的需求。 CREATE TABLE `user` ( `name` varchar(100) NOT NULL DEFAULT COMMENT 名字) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ; 比方說user表里的名字,就是個字符串。MySQL里有兩個類型比較適合這個場景。 char和varchar。…

我嘗試用 AI 來做數據分析,結果差強人意!

大家好&#xff0c;我是木川 工作中經常會需要分析數據 1、統計分析&#xff0c;計算某項指標的均值、分位數、標準差等 2、相關性分析&#xff0c;比如分析銷售額與顧客年齡、顧客性別、促銷活動等的相關性 3、可視化分析&#xff0c;比如繪制柱狀圖、折線圖、散點圖等 有了 A…

幾種排序的實現

直接插入排序 直接插入排序是一種簡單的插入排序法&#xff0c;其基本思想是&#xff1a; 把待排序的記錄按其關鍵碼值的大小逐個插入到一個已經排好序的有序序列中&#xff0c;直到所有的記錄插入完為止&#xff0c;得到一個新的有序序列 。 實際中我們玩撲克牌時&#xff…

交付《啤酒游戲經營決策沙盤》的項目

感謝首富客戶連續兩年的邀請&#xff0c;交付《啤酒游戲經營決策沙盤》的項目&#xff0c;下周一JSTO首席學習官Luna想讓我分享下系統思考與投資理財&#xff0c;想到曾經看過的一本書《深度思維》&#xff0c;看到一些結構來預判未來。不僅僅可以應用在企業經營和組織發展上&a…

Uncaught SyntaxError: Unexpected end of input (at manage.html:1:21) 的一個解

關于Uncaught SyntaxError: Unexpected end of input (at manage.html:1:21)的一個解 問題復現 <button onclick"deleteItem(${order.id},hire,"Orders")" >delete</button>報錯 原因 函數參數的雙引號和外面的雙引號混淆了&#xff0c;改成…

【vuex】

vuex 1 理解vuex1.1 vuex是什么1.2 什么時候使用vuex1.3 vuex工作原理圖1.4 搭建vuex環境1.5 求和案例1.5.1 vue方式1.5.2 vuex方式 2 vuex核心概念和API2.1 getters配置項2.2 四個map方法的使用2.2.1 mapState方法2.2.2 mapGetters方法2.2.3 mapActions方法2.2.4 mapMutations…

買賣股票的最佳時機算法(leetcode第121題)

題目描述&#xff1a; 給定一個數組 prices &#xff0c;它的第 i 個元素 prices[i] 表示一支給定股票第 i 天的價格。你只能選擇 某一天 買入這只股票&#xff0c;并選擇在 未來的某一個不同的日子 賣出該股票。設計一個算法來計算你所能獲取的最大利潤。返回你可以從這筆交易…

“HALCON error #2454:HALCON handle was already cleared in operator set_draw“

分析&#xff1a;錯誤提示是窗口句柄已經被刪除&#xff0c;這是因為前邊的一句 HOperatorSet.CloseWindow(hWindowControl1.HalconWindow); 關掉了窗口&#xff0c;屏蔽或刪除即可。

UDS診斷 10服務的肯定響應碼后面跟著一串數據的含義,以及診斷報文格式定義介紹

一、首先看一下10服務的請求報文和肯定響應報文格式 a.診斷儀發送的請求報文格式 b.ECU回復的肯定響應報文格式 c.肯定響應報文中參數定義 二、例程數據解析 a.例程數據 0.000000 1 725 Tx d 8 02 10 03 00 00 00 00 00 0.000806 1 7A5 Rx d 8 06 50 03 00 32 01 F4 CC …

Brushed DC mtr--PIC

PIC use brushed DC mtr fundmental. Low-Cost Bidirectional Brushed DC Motor Control Using the PIC16F684 DC mtr & encoder