Java中csv文件讀寫超詳細分析
一、txt、csv、tsv文件
txt、csv、tsv都屬于文本文件
文件類型 | 英文全稱 | 名稱 | 分隔符 | 描述 |
---|---|---|---|---|
txt | text | 文本類型 | 沒有明確要求 | 可以有分隔符,也可以沒有 |
csv | Comma-separated values | 逗號分隔值類型 | 半角逗號:',' | csv是txt的特殊類型 |
tsv | Tab-separated values | 制表符分隔值 | 制表符:'\t' | tsv是txt的特殊類型 |
csv又有叫做Char-separated values(字符分隔值類型),通過字符值進行分隔。
但因為半角逗號在數據中出現的的可能性比較大,所以經常會使用文本包裝符來標識逗號為數據中的一部分,或者直接使用其它特殊符號作為分隔符。
二、csv文件規(guī)范
- 每一行記錄位于一個單獨的行上,用回車換行符CRLF(\r\n)分割。
- 文件中的最后一行記錄可以有結尾回車換行符,也可以沒有。
- 第一行可以存在一個可選的標題頭,格式和普通記錄行的格式一樣。標題頭要包含文件記錄字段對應的名稱,應該有和記錄字段一樣的數量。
- 在標題頭行和普通行每行記錄中,會存在一個或多個由半角逗號(,)分隔的字段。整個文件中每行應包含相同數量的字段,空格也是字段的一部分,不應被忽略。每一行記錄最后一個字段后不能跟逗號。(通常用逗號分隔,也有其他字符分隔的CSV,需事先約定)
- 每個字段可用也可不用半角雙引號(")(文本包裝符)括起來(如Microsoft的Excel就根本不用雙引號)。如果字段沒有用引號括起來,那么該字段內部不能出現雙引號字符。
- 字段中若包含回車換行符、雙引號或者逗號,該字段需要用雙引號括起來。
- 如果用雙引號括字段,那么出現在字段內的雙引號前必須再加一個雙引號進行轉義。
三、csv使用場景
csv文件經常用于導出大批量數據(csv比excel更輕量級,更適合大批量數據)。
csv與excel對比:
- csv只能用于存儲純文本內容,excel不僅支持純文本內容還支持二進制數據
- csv可以看做是excel的輕量級簡單版實現,excel比csv更加強大
- csv文件可以被excel軟件直接打開,csv文件一般用于表格數據的傳輸
四、Java中的csv類庫
java中的csv的類庫主要有以下幾類:
- javacsv:javacsv在2014-12-10就不維護了
- opencsv:opencsv是apache的項目,至今仍在維護
1. javacsv
2. opencsv
opencsv是一個用Java來分析和生成csv文件的框架。通常用來bean的寫入csv文件和從csv文件讀出bean,并支持注解的方式。
maven依賴:
<dependency> <groupId>com.opencsv</groupId> <artifactId>opencsv</artifactId> <version>5.6</version> </dependency>
寫入器
名稱 | 描述 |
---|---|
CSVWriter | 簡單的CSV寫入器 |
CSVParserWriter | 通過CSVParser解析數據的寫入器 |
StatefulBeanToCsv | 直接將bean寫入CSV的寫入器 |
讀取器
名稱 | 描述 |
---|---|
CSVReader | 簡單的CSV讀取器 |
CsvToBean | CSV讀取為bean的讀取器 |
CSVReaderHeaderAware |
解析器
名稱 | 描述 |
---|---|
CSVParser | 簡單的CSV解析器 |
RFC4180Parser | 基于RFC4180規(guī)范的解析器 |
注解
注解 | 描述 | 主要屬性 |
---|---|---|
@CsvBindByName | 按表頭名稱綁定 | required:必須字段,默認為false.該字段為空拋異常 column:對象列標題名稱 |
@CsvBindByPosition | 按位置綁定 | required:必須字段,默認為false.該字段為空拋異常 position:位置索引 |
@CsvCustomBindByName | 與CsvBindByName相同,但必須提供自己的數據轉換類 | required:必須字段,默認為false.該字段為空拋異常 column:對象列標題名稱 converter:轉換器 |
@CsvCustomBindByPosition | 與CsvBindByPosition相同,但必須提供自己的數據轉換類 | required:必須字段,默認為false.該字段為空拋異常 column:對象列標題名稱 converter:轉換器 |
@CsvBindAndJoinByName | 應用于MultiValuedMap集合類型的bean字段,通過標題名稱綁定 | required:必須字段,默認為false.該字段為空拋異常 column:對象列標題名稱 converter:轉換器 mapType:集合類型 elementTyp:元素類型 |
@CsvBindAndJoinByPosition | 應用于MultiValuedMap集合類型的bean字段,通過位置索引綁定 | required:必須字段,默認為false.該字段為空拋異常 position:位置索引 converter:轉換器 mapType:集合類型 elementTyp:元素類型 |
@CsvBindAndSplitByName | 應用于Collection集合類型的bean字段,通過標題名稱綁定 | required:必須字段,默認為false.該字段為空拋異常 column:對象列標題名稱 converter:轉換器 mapType:集合類型 elementTyp:元素類型 splitOn: |
@CsvBindAndSplitByPosition | 應用于Collection集合類型的bean字段,通過位置索引綁定 | required:必須字段,默認為false.該字段為空拋異常 position:位置索引 converter:轉換器 mapType:集合類型 elementTyp:元素類型 splitOn: |
@CsvDate | 應用于日期/時間類型的bean字段,與上面相關的綁定注解結合使用 | value:日期格式,例如:yyyy-MM-dd |
@CsvNumber | 應用于數字類型的bean字段,與上面相關的綁定注解結合使用 | value:數字格式,例如:000.### |
映射策略
名稱 | 描述 | 重要方法 | 方法描述 |
---|---|---|---|
ColumnPositionMappingStrategy | 列位置映射策略,用于沒有頭文件(標題行)的文件 | setColumnMapping(String… columnMapping | 設置要映射的列名集合,集合下標即為列寫入順序 |
HeaderColumnNameMappingStrategy | 標題列名稱映射策略, | setColumnOrderOnWrite(Comparator writeOrder) | 通過比較器,設置列寫入順序 |
HeaderColumnNameTranslateMappingStrategy | 標題列名稱翻譯映射策略 bean的屬性名可以與csv列頭不一樣,通過指定map來映射。 | setColumnMapping(Map<String, String> columnMapping) | 設置標題名與列名的映射 |
FuzzyMappingStrategy |
① ColumnPositionMappingStrategy
使用該映射策略需要csv文件沒有標題行。該策略通過設置列的下標位置來指定列的順序,有兩種方式來設置列的下標:
- 通過CsvBindByPosition、CsvCustomBindByPosition、CsvBindAndJoinByPosition、CsvBindAndSplitByPosition注解來設置列的下標
- 通過setColumnMapping(String… columnMapping)方法來設置列的下標
② HeaderColumnNameMappingStrategy
該映射策略用于有標題行的csv文件。該策略通過指定比較器來指定列的順序:
- 通過setColumnOrderOnWrite(Comparator writeOrder)指定比較器
關于標題列的名稱:
- 默認使用bean的字段名稱大寫作為標題列的名稱
- 如果使用CsvBindByName、CsvCustomBindByName、CsvBindAndJoinByName、CsvBindAndSplitByName注解的column屬性指定列名稱,則使用該值,否則使用bean的字段名稱大寫作為標題列的名稱
③ HeaderColumnNameTranslateMappingStrategy
該映射策略用于有標題行的csv文件。該策略通過映射Map來指定標題列名與bean的屬性名映射關系。
映射Map的key=標題列名,value=bean的屬性名。
需要注意:
- 該映射策略只適用于讀取csv文件時,指定標題列名與bean的屬性名的映射關系
- 該映射策略不適用于寫入csv文件時,指定bean的屬性名與標題列名的映射關系(不要誤解)
過濾器
名稱 | 描述 |
---|---|
CsvToBeanFilter | 讀取時根據過濾規(guī)則過濾掉一些行 |
主要方法:boolean allowLine(String[] line)
- 入參中的line表示一行數據的集合
- 返回值為false的這行數據被將被過濾掉
構建器
名稱 | 描述 |
---|---|
CSVWriterBuilder | CSV寫入構建器,構建CSVWriter或CSVParserWriter |
StatefulBeanToCsvBuilder | 對象寫入CSV構建器,構建StatefulBeanToCsv |
CSVReaderBuilder | CSV讀取構建器,構建CSVReader |
CsvToBeanBuilder | CSV讀取對象構建器,構建CsvToBean |
CSVReaderHeaderAwareBuilder | 構建CSVReaderHeaderAware |
CSVParserBuilder | CSV解析器構造器,構建CSVParser |
RFC4180ParserBuilder | RFC4180解析器構造器,構建RFC4180Parser |
寫入方式
User類:
@Data @NoArgsConstructor @AllArgsConstructor public class User { public String userId; public String userName; public String sex; }
User1類:
@Data @NoArgsConstructor @AllArgsConstructor public class User1 { @CsvBindByPosition(position = 0) public String userId; @CsvBindByPosition(position = 1) public String userName; @CsvBindByPosition(position = 2) public String sex; }
User2類:
@Data @NoArgsConstructor @AllArgsConstructor public class User2 { @CsvBindByName(column = "用戶ID") public String userId; @CsvBindByName(column = "用戶名") public String userName; @CsvBindByName(column = "性別") public String sex; }
① 簡單的寫入
CSVWriter的主要參數:
- Writer writer:指定需要寫入的源文件
- char separator:分隔符(默認逗號)
- char quotechar:文本邊界符(默認雙引號)
- 如果數據中包含分隔符,需要使用文本邊界符包裹數據。通常用雙引號、單引號或斜杠作為文本邊界符
- char escapechar:轉義字符(默認雙引號)
- String lineend:行分隔符(默認為\n)
使用方法:
/** * 簡單的寫入 * @throws Exception */ private static void csvWriter() throws Exception { // 寫入位置 String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); String fileName = classpath+"test/demo.csv"; // 標題行 String[] titleRow = {"用戶ID", "用戶名", "性別"}; // 數據行 ArrayList<String[]> dataRows = new ArrayList<>(); String[] dataRow1 = {"1", "張三", "男"}; String[] dataRow2 = {"2", "李四", "男"}; String[] dataRow3 = {"3", "翠花", "女"}; dataRows.add(dataRow1); dataRows.add(dataRow2); dataRows.add(dataRow3); OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(fileName), Charset.forName("UTF-8")); // 1. 通過new CSVWriter對象的方式直接創(chuàng)建CSVWriter對象 // CSVWriter csvWriter = new CSVWriter(writer); // 2. 通過CSVWriterBuilder構造器構建CSVWriter對象 CSVWriter csvWriter = (CSVWriter) new CSVWriterBuilder(writer) .build(); // 寫入標題行 csvWriter.writeNext(titleRow, false); // 寫入數據行 csvWriter.writeAll(dataRows, false); csvWriter.close(); }
demo.csv內容:
用戶ID,用戶名,性別
1,張三,男
2,李四,男
3,翠花,女
② 基于位置映射的寫入
使用方法:
/** * 基于位置映射的寫入 * @throws Exception */ private static void beanToCsvByPosition() throws Exception { String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); String fileName = classpath+"test/demo.csv"; List<User> list = new ArrayList<>(); list.add(new User("1", "張三", "男")); list.add(new User("2", "李四", "男")); list.add(new User("3", "翠花", "女")); OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(fileName), Charset.forName("UTF-8")); ColumnPositionMappingStrategy<User> strategy = new ColumnPositionMappingStrategy(); // 未指定的列不寫入 String[] columns = new String[] { "userId", "userName", "sex"}; strategy.setColumnMapping(columns); strategy.setType(User.class); // 如果需要標題行,可這樣寫入 // CSVWriter csvWriter = (CSVWriter) new CSVWriterBuilder(writer) // .build(); // String[] titleRow = {"用戶ID", "用戶名", "性別"}; // csvWriter.writeNext(titleRow, false); StatefulBeanToCsv<User> statefulBeanToCsv = new StatefulBeanToCsvBuilder<User>(writer) .withMappingStrategy(strategy) .withApplyQuotesToAll(false) .build(); statefulBeanToCsv.write(list); writer.close(); }
demo.csv內容:
1,張三,男
2,李四,男
3,翠花,女
③ 基于CsvBindByPosition注解映射的寫入
使用方法:
/** * 基于CsvBindByPosition注解映射的寫入 * @throws Exception */ private static void beanToCsvByPositionAnnotation() throws Exception { String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); String fileName = classpath+"test/demo.csv"; List<User1> list = new ArrayList<>(); list.add(new User1("1", "張三", "男")); list.add(new User1("2", "李四", "男")); list.add(new User1("3", "翠花", "女")); // 未使用@CsvBindByPosition注解的列不寫入 OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(fileName), Charset.forName("UTF-8")); // 如果需要標題行,可這樣寫入 // CSVWriter csvWriter = (CSVWriter) new CSVWriterBuilder(writer) // .build(); // String[] titleRow = {"用戶ID", "用戶名", "性別"}; // csvWriter.writeNext(titleRow, false); StatefulBeanToCsv<User1> statefulBeanToCsv = new StatefulBeanToCsvBuilder<User1>(writer) .withApplyQuotesToAll(false) .build(); statefulBeanToCsv.write(list); writer.close(); }
demo.csv內容:
1,張三,男
2,李四,男
3,翠花,女
④ 基于列名映射的寫入
使用方法:
/** * 基于列名映射的寫入 * @throws Exception */ private static void beanToCsvByName() throws Exception { String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); String fileName = classpath+"test/demo.csv"; List<User> list = new ArrayList<>(); list.add(new User("1", "張三", "男")); list.add(new User("2", "李四", "男")); list.add(new User("3", "翠花", "女")); OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(fileName), Charset.forName("UTF-8")); // 可通過比較器指定列的順序 // 標題行的列名默認為bean的字段名大寫 HeaderColumnNameMappingStrategy<User> strategy = new HeaderColumnNameMappingStrategy<>(); HashMap<String, Integer> columnOrderMap = new HashMap<>(); columnOrderMap.put("USERID", 1); columnOrderMap.put("SEX", 10); columnOrderMap.put("USERNAME", 100); strategy.setColumnOrderOnWrite(Comparator.comparingInt(column -> (columnOrderMap.getOrDefault(column, 0)))); strategy.setType(User.class); StatefulBeanToCsv<User> statefulBeanToCsv = new StatefulBeanToCsvBuilder<User>(writer) .withMappingStrategy(strategy) .withApplyQuotesToAll(false) .build(); statefulBeanToCsv.write(list); writer.close(); }
demo.csv內容:
用戶ID,用戶名,性別
1,張三,男
2,李四,男
3,翠花,女
⑤ 基于CsvBindByName注解映射的寫入
使用方法:
/** * 基于CsvBindByName注解映射的寫入 * @throws Exception */ private static void beanToCsvByNameAnnotation() throws Exception { String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); String fileName = classpath+"test/demo.csv"; List<User2> list = new ArrayList<>(); list.add(new User2("1", "張三", "男")); list.add(new User2("2", "李四", "男")); list.add(new User2("3", "翠花", "女")); OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(fileName), Charset.forName("UTF-8")); // 可通過比較器指定列的順序 // 通過CsvBindByName注解的column屬性,指定標題行的列名 HeaderColumnNameMappingStrategy<User2> strategy = new HeaderColumnNameMappingStrategy<>(); // 注意這里的key是指的標題行的列名 HashMap<String, Integer> columnOrderMap = new HashMap<>(); columnOrderMap.put("用戶ID", 1); columnOrderMap.put("用戶名", 10); columnOrderMap.put("性別", 100); strategy.setColumnOrderOnWrite(Comparator.comparingInt(column -> (columnOrderMap.getOrDefault(column, 0)))); strategy.setType(User2.class); StatefulBeanToCsv<User2> statefulBeanToCsv = new StatefulBeanToCsvBuilder<User2>(writer) .withMappingStrategy(strategy) .withApplyQuotesToAll(false) .build(); statefulBeanToCsv.write(list); writer.close(); }
demo.csv內容:
用戶ID,用戶名,性別
1,張三,男
2,李四,男
3,翠花,女
讀取方式
通過簡單的寫入寫入的數據
① 簡單的讀取
使用方法:
/** * 簡單的讀取 * @throws Exception */ private static void csvReader() throws Exception { String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); String fileName = classpath+"test/demo.csv"; InputStreamReader reader = new InputStreamReader(new FileInputStream(fileName), Charset.forName("UTF-8")); CSVReader csvReader = new CSVReaderBuilder(reader).build(); List<String[]> list = csvReader.readAll(); for (String[] strings : list) { System.out.println(JSON.toJSONString(strings)); } csvReader.close(); }
控制臺日志:
["用戶ID","用戶名","性別"]
["1","張三","男"]
["2","李四","男"]
["3","翠花","女"]
② 基于位置映射的讀取
通過基于位置映射的寫入寫入的數據
使用方法:
/** * 基于位置映射的讀取 * @throws Exception */ private static void csvToBeanByPosition() throws Exception { String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); String fileName = classpath+"test/demo.csv"; InputStreamReader reader = new InputStreamReader(new FileInputStream(fileName), Charset.forName("UTF-8")); // 不需要標題行,列的順序通過列位置映射指定 ColumnPositionMappingStrategy<User> strategy = new ColumnPositionMappingStrategy(); String[] columns = new String[] { "userId", "userName", "sex"}; strategy.setColumnMapping(columns); strategy.setType(User.class); CsvToBean<User> csvToBean = new CsvToBeanBuilder<User>(reader) .withMappingStrategy(strategy) .build(); List<User> list = csvToBean.parse(); for (User user : list) { System.out.println(JSON.toJSONString(user)); } reader.close(); }
控制臺日志:
{"sex":"男","userId":"1","userName":"張三"}
{"sex":"男","userId":"2","userName":"李四"}
{"sex":"女","userId":"3","userName":"翠花"}
③ 基于CsvBindByPosition注解映射的讀取
通過基于CsvBindByPosition注解映射的寫入寫入的數據
使用方法:
/** * 基于CsvBindByPosition注解映射的讀取 * @throws Exception */ private static void csvToBeanByPositionAnnotation() throws Exception { String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); String fileName = classpath+"test/demo.csv"; InputStreamReader reader = new InputStreamReader(new FileInputStream(fileName), Charset.forName("UTF-8")); // 不需要標題行,列的順序通過CsvBindByPosition注解的position屬性指定 CsvToBean<User1> csvToBean = new CsvToBeanBuilder<User1>(reader) .withType(User1.class) .build(); List<User1> list = csvToBean.parse(); for (User1 user : list) { System.out.println(JSON.toJSONString(user)); } reader.close(); }
控制臺日志:
{"sex":"男","userId":"1","userName":"張三"}
{"sex":"男","userId":"2","userName":"李四"}
{"sex":"女","userId":"3","userName":"翠花"}
④ 基于列名映射的讀取
通過基于列名映射的寫入寫入的數據
使用方法:
/** * 基于列名映射的讀取 * @throws Exception */ private static void csvToBeanByName() throws Exception { String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); String fileName = classpath+"test/demo.csv"; InputStreamReader reader = new InputStreamReader(new FileInputStream(fileName), Charset.forName("UTF-8")); // bean的字段名稱大寫為標題列名 CsvToBean<User> csvToBean = new CsvToBeanBuilder<User>(reader) .withType(User.class) .build(); List<User> list = csvToBean.parse(); for (User user : list) { System.out.println(JSON.toJSONString(user)); } reader.close(); }
控制臺日志:
{"sex":"男","userId":"1","userName":"張三"}
{"sex":"男","userId":"2","userName":"李四"}
{"sex":"女","userId":"3","userName":"翠花"}
⑤ 基于CsvBindByName注解映射的讀取
通過基于CsvBindByName注解映射的寫入寫入的數據
使用方法:
private static void csvToBeanByNameAnnotation() throws Exception { String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); String fileName = classpath+"test/demo.csv"; InputStreamReader reader = new InputStreamReader(new FileInputStream(fileName), Charset.forName("UTF-8")); // CsvBindByName注解的column屬性為標題列名 CsvToBean<User2> csvToBean = new CsvToBeanBuilder<User2>(reader) .withType(User2.class) .build(); List<User2> list = csvToBean.parse(); for (User2 user : list) { System.out.println(JSON.toJSONString(user)); } reader.close(); }
控制臺日志:
{"sex":"男","userId":"1","userName":"張三"}
{"sex":"男","userId":"2","userName":"李四"}
{"sex":"女","userId":"3","userName":"翠花"}
⑥ 基于列名轉換映射的讀取
通過基于CsvBindByName注解映射的讀取寫入的數據
使用方法:
public class MyCsvToBeanFilter implements CsvToBeanFilter { @Override public boolean allowLine(String[] line) { // 過濾掉用戶名為李四的行 if("李四".equals(line[1])){ return false; } return true; } }
/** * 基于列名轉換映射的讀取 * @throws Exception */ private static void csvToBeanByColumnNameTranslateMapping() throws Exception { String classpath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); String fileName = classpath+"test/demo.csv"; InputStreamReader reader = new InputStreamReader(new FileInputStream(fileName), Charset.forName("UTF-8")); // 指定標題列名和bean列名映射關系 HeaderColumnNameTranslateMappingStrategy<User> strategy = new HeaderColumnNameTranslateMappingStrategy<>(); // key:標題列名,value:bean的屬性名 HashMap<String, String> columnMappingMap = new HashMap<>(); columnMappingMap.put("用戶ID", "userId"); columnMappingMap.put("性別", "sex"); columnMappingMap.put("用戶名", "userName"); strategy.setColumnMapping(columnMappingMap); strategy.setType(User.class); CsvToBean<User> csvToBean = new CsvToBeanBuilder<User>(reader) .withMappingStrategy(strategy) .withFilter(new MyCsvToBeanFilter()) .withIgnoreField(User2.class, User2.class.getField("userId"))// 忽略userId屬性 .build(); List<User> list = csvToBean.parse(); for (User user : list) { System.out.println(JSON.toJSONString(user)); } reader.close(); }
控制臺日志:
{"sex":"男","userName":"張三"} {"sex":"女","userName":"翠花"}
3. commons-csv
4. hutool CsvUtil(擴展)
總結
到此這篇關于Java中csv文件讀寫超詳細分析的文章就介紹到這了,更多相關Java csv文件讀寫內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringBoot對Filter過濾器中的異常進行全局處理方案詳解
這篇文章主要介紹了SpringBoot對Filter過濾器中的異常進行全局處理,在SpringBoot中我們通過 @ControllerAdvice 注解和 @ExceptionHandler注解注冊了全局異常處理器,需要的朋友可以參考下2023-09-09Java中getSuperclass()方法的使用與原理解讀
文章介紹了Java中的getSuperclass()方法,該方法用于獲取一個類的直接父類,通過理解其使用方式、工作原理以及實際應用場景,可以更好地利用反射機制處理類的繼承關系,實現動態(tài)類型檢查、類加載以及序列化等功能2025-01-01SpringBoot整合mybatis使用Druid做連接池的方式
這篇文章主要介紹了SpringBoot整合mybatis使用Druid做連接池的方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-08-08SpringBoot如何監(jiān)聽redis?Key變化事件案例詳解
項目中需要監(jiān)聽redis的一些事件比如鍵刪除,修改,過期等,下面這篇文章主要給大家介紹了關于SpringBoot如何監(jiān)聽redis?Key變化事件的相關資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-08-08