1 前言
最近在忙專案上線的事情,那麼上線前要確保系統流程或者基礎檔案的正確性,往往會做幾次資料的準確性驗證。具體的說就是把客戶的一個月甚至兩個月的不同公司下的各種訂單資料、庫存資料,匯入到系統中,看每個公司的訂單資料、所消耗的庫存數量、流程能不能正確的跑下去等方面進行對比來驗證。
奈何系統的匯入做的真是拉跨,匯入幾十條慢的,更何況要匯入的都是上萬、幾十萬的資料,這就需要我們的產品、測試一點點拆分 Excel,來匯入,消耗大量的時間,所以就寫了點程式碼,輔助他們匯入。
2 具體開發
匯入的 Excel 都是明細級別的,並且訂單號都是統一放入到每個 Excel 的訂單備註這一列,所以分兩步走:
(1)拆分:按訂單號進行拆分,把他們拆分到各個 Excel 中(這裡我用的 POI)
(2)匯入:然後批次將拆分好的 Excel 進行匯入(透過執行緒池來併發匯入,併發數量取決於最大執行緒數,我目前暫時設定的 16)
這是 Excel 資料,大概長這個樣子:
我們來看第一步拆分,我這裡就直接貼簡化程式碼了哈,註釋也寫的比較清晰,我就不囉嗦了哈:
package com.example.demo; import com.google.common.collect.Maps; import org.apache.commons.compress.utils.Lists; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.xssf.usermodel.XSSFCell; import org.apache.poi.xssf.usermodel.XSSFRow; import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; import java.util.stream.Collectors; /** * @author: kuku * @description */ public class Demo { /** * 資料檔案絕對路徑 */ private static String dataFileDir = "D:\\data\\匯入模版.xlsx"; /** * 拆分檔案存放的位置 */ private static String outDir = "D:\\data\\data\\"; public static void main(String[] args) throws Exception { // 資料檔案、以及流 File dataFile = new File(dataFileDir); FileInputStream fileInputStream = new FileInputStream(dataFile); // 讀到資料檔案 XSSFWorkbook workbook = new XSSFWorkbook(fileInputStream); XSSFSheet sheet = workbook.getSheetAt(0); // !!!下邊四個引數需要根據每個資料檔案改寫 // 內容行列的邊界,比如列的邊界在第40列 行的邊界在12400行 // 因為 Excel 不標準,透過 POI 可以獲取 sheet 的 lastRow 但是不準都,所以直接自己定義 int columns = 40; int rows = 12400; // 分組的在第幾列,拆分的依據 int groupColumn = 6; // 資料從第幾行開始讀取 int dataRow = 2; // 存放資料 List<Map<Integer, Object>> dataList = Lists.newArrayList(); // 遍歷讀取 for (int i = 2; i < rows; i++) { // 讀取一行 XSSFRow row = sheet.getRow(i); // 存放一行的資料,用 map key = 第幾列 val = 資料值 Map<Integer, Object> dataMap = Maps.newLinkedHashMap(); for (int j = 0; j < columns; j++) { // 遍歷該行的每一列 XSSFCell cell = row.getCell(j); // 不為空的再讀取 if (Objects.nonNull(cell)) { // 因為日期用 poi 讀取的是整數所以這裡特殊處理下 if (j == 0) { Date date = cell.getDateCellValue(); DateFormat formater = new SimpleDateFormat("yyyy/M/d"); dataMap.put(j, formater.format(date)); } else { // 這裡一些小數呀、下拉選擇框的獲取方式需要特別注意下,要根據 poi 的型別來呼叫不同的方法 // 否則小數精度、訂單號變為科學記數等會出錯,下拉的拿的值也不對,要注意 String value = getValue(cell); dataMap.put(j, value); } } } // 列印該行資料,看看精度、日期什麼的都對不對 System.out.println(String.format("%s==%s", dataMap.get(groupColumn), dataMap)); dataList.add(dataMap); } // 分組 Map<String, List<Map<Integer, Object>>> orderMap = dataList.stream().collect(Collectors.groupingBy(i -> String.valueOf(i.get(groupColumn)))); // 分組後,然後拆分到不同的 Excel 我這裡是順序的拆 // 因為拆分完互不影響了就,可以考慮將上邊的分組的 map 用一個執行緒安全的 map, 然後併發的進行拆,提升下速度 Set<String> orderNos = orderMap.keySet(); for (String orderNo : orderNos) { // 每個訂單的資料 list List<Map<Integer, Object>> data = orderMap.get(orderNo); // 每一個都寫入到一個新的excel 中 XSSFWorkbook newWorkbook = new XSSFWorkbook(); XSSFSheet resource = newWorkbook.createSheet("資源模板"); for (int i = 0; i < data.size(); i++) { Map<Integer, Object> dataMap = data.get(i);
// + dataRow 從第幾行開始寫 XSSFRow row = resource.createRow(i + dataRow); for (int j = 0; j < columns; j++) { Object val = dataMap.get(j); if (Objects.nonNull(val)) { XSSFCell cell = row.createCell(j); cell.setCellValue((val.toString())); } } } newWorkbook.write(new FileOutputStream(outDir + orderNo + ".xlsx")); } } /** * 獲取不同 cell 的值 * @param cell * @return */ private static String getValue(Cell cell) { String value = null; switch (cell.getCellType()) { case STRING: value = formatStringValue(cell); break; case NUMERIC: value = formatDoubleValue(cell); break; case BOOLEAN: value = formatBooleanValue(cell); break; case FORMULA: value = formatFormulaValue(cell); break; case BLANK: value = null; break; default: throw new RuntimeException("表格有不支援的列型別:" + cell.getCellType()); } return value; } /** * 字串型的 * @param cell * @return */ private static String formatStringValue(Cell cell) { return cell == null ? null : cell.getStringCellValue(); } /** * 數值型的 * @param cell * @return */ private static String formatDoubleValue(Cell cell) { Double d = cell.getNumericCellValue(); String s = d.toString(); if (s.contains("E")) { cell.setCellType(CellType.STRING); return cell.getStringCellValue(); } else { return s; } } /** * 布林型的 * @param cell * @return */ private static String formatBooleanValue(Cell cell) { Boolean b = cell.getBooleanCellValue(); return String.valueOf(b); } /** * 公式型的 * @param cell * @return */ private static String formatFormulaValue(Cell cell) { return cell.getCellFormula(); } }
拆完大概長這個樣子,記得開啟幾個看看精度、資料都對不對哈:
然後就是調介面進行匯入了:
package com.example.demo; import cn.hutool.http.HttpUtil; import com.google.common.collect.Sets; import java.io.File; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * @author: kuku * @description */ public class Demo2 { /** * 執行緒池 * 無界佇列 */ private static final ExecutorService EXECUTOR_SERVICE = new ThreadPoolExecutor( 16, 16, 20L, TimeUnit.SECONDS, new LinkedBlockingQueue<>() ); /** * 拆分的資料檔案路徑 */ private static String outDir = "D:\\data\\data\\"; /** * 介面地址 */ private static String REQUEST = "http://xxx"; /** * 特殊處理,比如排除哪些不處理 */ private static Set<String> NO_SET = Sets.newHashSet(); /** * 計數器,方便檢視當前進度 */ private static AtomicInteger total = new AtomicInteger(0); public static void main(String[] args) { // 檔案目錄 File outFile = new File(outDir); // 所有檔案 File[] files = outFile.listFiles(); for (File file : files) { // 訂單號檔案資訊 String name = file.getName(); String[] split = name.split("\\."); String orderNo = split[0]; if (!NO_SET.contains(orderNo)) { // 依次放進執行緒池 EXECUTOR_SERVICE.submit(() -> { // 介面 token 什麼的記得放 String res = HttpUtil .createPost(REQUEST) .form("file", file) .execute().body(); System.out.println(String.format("當前:%s, 當前單號:%s, 匯入結果:%s", total.incrementAndGet(), file.getName(), res.replaceAll("\\r\\n|\\r|\\n", ""))); }); } } } }
這就是匯入了。
你也可以將兩步合到一步裡,拆完直接匯入,但是還是嚴謹點,拆完先看看資料都對不對,然後再導。
3 小結
好啦,到這裡就結束了,有哪裡寫的不好的地方歡迎指正哈。