文章目录
1. POI介绍
在项目中经常做Excel的导入导出,Excel导入到处常用的工具就是POI和easyExcel,本文先来介绍POI,下一篇文章介绍EasyExcel。
1.1 POI
官网:https://poi.apache.org/
这主要介绍Excel相关的功能
1.2 Excel概念
-
工作簿
一张excel文件就是一个工作簿
-
工作表
excel中的一个sheet就是一个工作表
-
行
sheet中的一行就是一个单元行
-
单元格
一行中的一个列就是一个单元格
1.3 POI中的对象介绍
-
工作簿
使用WorkBook对象进行操作,实现类主要有HSSFWorkBook、XSSFWorkBook、SXSSFWorkBook
-
工作表
使用Sheet对象操作工作表,一个excel中有多个sheet
-
行
使用Row对象操作工作表中的一行
-
单元格
使用Cell对象操作一行中的某一列
2. POI操作
文章对应源码地址: https://gitee.com/he_linhai/spring-boot-csdn/tree/master/02-poi-easyExcel
pom.xml
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.17</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.17</version>
</dependency>
2.1 POI Excel写
03 xls 写
/**
* xls写
* 使用 HSSFWorkbook 进行操作
*/
@Test
public void poi03Write() {
// 工作簿
try(Workbook workbook = new HSSFWorkbook();
FileOutputStream fileOutputStream = new FileOutputStream(PATH + "03simple.xls")) {
// 工作表
Sheet sheet1 = workbook.createSheet("sheet1");
// 行
Row row1 = sheet1.createRow(0);
// 列
Cell cell11 = row1.createCell(0);
Cell cell12 = row1.createCell(1);
// 1-1 1-2
cell11.setCellValue("1-1");
cell12.setCellValue("1-2");
Row row2 = sheet1.createRow(1);
Cell cell21 = row2.createCell(0);
Cell cell22 = row2.createCell(1);
// 2-1 2-2
cell21.setCellValue("2-1");
cell22.setCellValue("2-2");
// 写出文件
workbook.write(fileOutputStream);
} catch (Exception e) {
e.printStackTrace();
}
}
07 xlsx 写
- XSSFWorkBook
/**
* xlsx 写
* 使用 XSSFWorkbook 进行操作
*/
@Test
public void poi07Write() {
// 工作簿
try(Workbook workbook = new XSSFWorkbook();
FileOutputStream fileOutputStream = new FileOutputStream(PATH + "07simple.xlsx")) {
// 工作表
Sheet sheet1 = workbook.createSheet("sheet1");
// 行
Row row1 = sheet1.createRow(0);
// 列
Cell cell11 = row1.createCell(0);
Cell cell12 = row1.createCell(1);
// 1-1 1-2
cell11.setCellValue("1-1");
cell12.setCellValue("1-2");
Row row2 = sheet1.createRow(1);
Cell cell21 = row2.createCell(0);
Cell cell22 = row2.createCell(1);
// 2-1 2-2
cell21.setCellValue("2-1");
cell22.setCellValue("2-2");
// 写出文件
workbook.write(fileOutputStream);
} catch (Exception e) {
e.printStackTrace();
}
}
- SXSSFWrokBook
/**
* xlsx 写
* 使用 SXSSFWorkbook 进行操作(优化之后的)
*/
@Test
public void poi07WriteSXSSF() {
// 工作簿
try(Workbook workbook = new SXSSFWorkbook();
FileOutputStream fileOutputStream = new FileOutputStream(PATH + "07simple.xlsx")) {
// 工作表
Sheet sheet1 = workbook.createSheet("sheet1");
// 行
Row row1 = sheet1.createRow(0);
// 列
Cell cell11 = row1.createCell(0);
Cell cell12 = row1.createCell(1);
// 1-1 1-2
cell11.setCellValue("1-1");
cell12.setCellValue("1-2");
Row row2 = sheet1.createRow(1);
Cell cell21 = row2.createCell(0);
Cell cell22 = row2.createCell(1);
// 2-1 2-2
cell21.setCellValue("2-1");
cell22.setCellValue("2-2");
// 写出文件
workbook.write(fileOutputStream);
} catch (Exception e) {
e.printStackTrace();
}
}
2.2 POI Excel 读
03 xls 读
/**
* xls读
* 使用 HSSFWorkbook 进行操作
*/
@Test
public void poi03Read() {
// 工作簿
try(Workbook workbook = new HSSFWorkbook(new FileInputStream(PATH + "03simple.xls"))) {
// 工作表
Sheet sheet1 = workbook.getSheet("sheet1");
// 行
Row row1 = sheet1.getRow(0);
// 列
Cell cell11 = row1.getCell(0);
Cell cell12 = row1.getCell(1);
// 1-1 1-2
System.out.println("1-1" + ": " + cell11.getStringCellValue());
System.out.println("1-2" + ": " + cell12.getStringCellValue());
Row row2 = sheet1.getRow(1);
Cell cell21 = row2.getCell(0);
Cell cell22 = row2.getCell(1);
// 2-1 2-2
System.out.println("2-1" + ": " + cell21.getStringCellValue());
System.out.println("2-2" + ": " + cell22.getStringCellValue());
} catch (Exception e) {
e.printStackTrace();
}
}
- 结果
07 xlsx 读
- XSSFWorkbook
/**
* xlsx 读
* 使用 XSSFWorkbook 进行操作
*/
@Test
public void poi07Read() {
// 工作簿
try(Workbook workbook = new XSSFWorkbook(new FileInputStream(PATH + "07simple.xlsx"))) {
// 工作表
Sheet sheet1 = workbook.getSheet("sheet1");
// 行
Row row1 = sheet1.getRow(0);
// 列
Cell cell11 = row1.getCell(0);
Cell cell12 = row1.getCell(1);
// 1-1 1-2
System.out.println("1-1" + ": " + cell11.getStringCellValue());
System.out.println("1-2" + ": " + cell12.getStringCellValue());
Row row2 = sheet1.getRow(1);
Cell cell21 = row2.getCell(0);
Cell cell22 = row2.getCell(1);
// 2-1 2-2
System.out.println("2-1" + ": " + cell21.getStringCellValue());
System.out.println("2-2" + ": " + cell22.getStringCellValue());
} catch (Exception e) {
e.printStackTrace();
}
}
- SXSSFWorkbook
/**
* xlsx 读
* 使用 SXSSFWorkbook 进行操作(优化之后的)
*/
@Test
public void poi07ReadSXSSF() {
// 工作簿
try(Workbook workbook = new XSSFWorkbook(new FileInputStream(PATH + "07simple.xlsx"))) {
// 工作表
Sheet sheet1 = workbook.getSheet("sheet1");
// 行
Row row1 = sheet1.getRow(0);
// 列
Cell cell11 = row1.getCell(0);
Cell cell12 = row1.getCell(1);
// 1-1 1-2
System.out.println("1-1" + ": " + cell11.getStringCellValue());
System.out.println("1-2" + ": " + cell12.getStringCellValue());
Row row2 = sheet1.getRow(1);
Cell cell21 = row2.getCell(0);
Cell cell22 = row2.getCell(1);
// 2-1 2-2
System.out.println("2-1" + ": " + cell21.getStringCellValue());
System.out.println("2-2" + ": " + cell22.getStringCellValue());
} catch (Exception e) {
e.printStackTrace();
}
}
2.3 POI Cell多格式读取
org.apache.poi.ss.usermodel.CellType
中规定了cell的几种数据类型
类型 | 描述 |
---|---|
@Internal(since=“POI 3.15 beta 3”) _NONE(-1) |
未知类型,仅限内部使用 |
NUMERIC(0) | 数值类型,小数,日期 |
STRING(1) | 字符串 |
FORMULA(2) | 公式 |
BLANK(3) | 空单元个,没值,但是有单元格样式 |
BOOLEAN(4) | 布尔值 |
ERROR(5) | 错误单元格 |
/**
* 解析列
* @param cell 单元格
* @param workbook 工作簿
* @return 解析完的数据
*/
private Object getCellData(Cell cell, Workbook workbook) {
Object result = null;
CellType cellType = cell.getCellTypeEnum();
switch (cellType) {
case STRING:
result = cell.getStringCellValue();
break;
case BOOLEAN:
result = cell.getBooleanCellValue();
break;
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
result = cell.getDateCellValue();
} else {
result = cell.getNumericCellValue();
}
break;
case FORMULA:
FormulaEvaluator formulaEvaluator = null;
if (workbook instanceof HSSFWorkbook) {
formulaEvaluator = new HSSFFormulaEvaluator((HSSFWorkbook) workbook);
} else if (workbook instanceof XSSFWorkbook) {
formulaEvaluator = new XSSFFormulaEvaluator((XSSFWorkbook) workbook);
} else if (workbook instanceof SXSSFWorkbook) {
formulaEvaluator = new SXSSFFormulaEvaluator((SXSSFWorkbook) workbook);
}
if (formulaEvaluator != null) {
CellValue evaluate = formulaEvaluator.evaluate(cell);
result = getCellData(evaluate, workbook);
}
break;
default:
break;
}
return result;
}
/**
* 获取CellValue的值
* @param cellValue CellValue
* @param workbook 工作薄
* @return 解析完的数据
*/
private Object getCellData(CellValue cellValue, Workbook workbook) {
Object result = null;
CellType cellType = cellValue.getCellTypeEnum();
switch (cellType) {
case STRING:
result = cellValue.getStringValue();
break;
case BOOLEAN:
result = cellValue.getBooleanValue();
break;
case NUMERIC:
result = cellValue.getNumberValue();
break;
default:
break;
}
return result;
}
2.4 POI Excel大数据量写
一下数据为JVM 8G内存的导出测试
03 xls 大数据量写
03版本的xls后缀的文件,单sheet最多存储65536条数据,
使用HSSFWorkBook对象写出即可
单出单sheet最多数据,耗时2s,没有优化的必要
/**
* xls大文件写
* 使用 HSSFWorkbook 进行操作
* 使用xls格式最多写入65536行数据 2.174s
*/
@Test
public void poi03Write() {
// 工作簿
try(Workbook workbook = new HSSFWorkbook();
FileOutputStream fileOutputStream = new FileOutputStream(PATH + "03big.xls")) {
long start = System.currentTimeMillis();
// 工作表
Sheet sheet = workbook.createSheet();
// 写 65536 条数据
for (int i = 0; i < 65536; i++) {
// 行
Row row = sheet.createRow(i);
for (int j = 0; j < 50; j++) {
// 列
Cell cell = row.createCell(j);
cell.setCellValue(j);
}
}
// 写出文件
workbook.write(fileOutputStream);
long end = System.currentTimeMillis();
System.out.println(((double)end - start ) / 1000);
} catch (Exception e) {
e.printStackTrace();
}
}
07 xlsx 导出
使用XSSFWorkBook进行操作
以8g内存为例,25万条数据会报OOM
实测20万可以,但是会占用大量内存,加上业务操作,很容易OOM,不推荐使用
/**
* xlsx 写
* 使用 XSSFWorkbook 进行操作
* 写入 25万 条数据 java 最高8G 空间 25万条就会内存溢出OOM
* 20万条测试可以 66.928s
* 单sheet最多到处1048575行
*/
@Test
public void poi07Write() {
// 工作簿
try(Workbook workbook = new XSSFWorkbook();
FileOutputStream fileOutputStream = new FileOutputStream(PATH + "07big.xlsx")) {
long start = System.currentTimeMillis();
// 工作表
Sheet sheet1 = workbook.createSheet();
// 写 1000000 条数据
for (int i = 0; i < 200000; i++) {
// 行
Row row = sheet1.createRow(i);
for (int j = 0; j < 50; j++) {
// 列
Cell cell = row.createCell(j);
cell.setCellValue(j);
}
}
// 写出文件
workbook.write(fileOutputStream);
long end = System.currentTimeMillis();
System.out.println(((double)end - start ) / 1000);
} catch (Exception e) {
e.printStackTrace();
}
}
POI 针对xlsx版本的excel导出进行优化,使用SXSSFWorkbook对象进行操作
会分批刷盘,减少内存使用量
原理
其原理是可以定义一个window size(默认100),生成Excel期间只在内存维持window size那么多的行数Row,超时window size时会把之前行Row写到一个临时文件并且remove释放掉,这样就可以达到释放内存的效果。SXSSFSheet在创建Row时会判断并刷盘、释放超过window size的Row。
/**
* xlsx 写
* 使用 SXSSFWorkbook 进行操作(优化之后的)
* 100万条数据,占用内存500多M,耗时43.288秒
* 单sheet最多到处1048576行
*/
@Test
public void poi07WriteSXSSF() {
/// 工作簿
try(Workbook workbook = new SXSSFWorkbook();
FileOutputStream fileOutputStream = new FileOutputStream(PATH + "07big.xlsx")) {
long start = System.currentTimeMillis();
// 工作表
Sheet sheet1 = workbook.createSheet();
// 写 1000000 条数据
for (int i = 0; i < 1000000; i++) {
// 行
Row row = sheet1.createRow(i);
for (int j = 0; j < 50; j++) {
// 列
Cell cell = row.createCell(j);
cell.setCellValue(j);
}
}
// 写出文件
workbook.write(fileOutputStream);
long end = System.currentTimeMillis();
System.out.println(((double)end - start ) / 1000);
} catch (Exception e) {
e.printStackTrace();
}
}
2.5 POI Excel 大数据量读
03 xls 导入
使用HSSFWorkBook导入,
单sheet极限数据65536行数据,用时30s
/**
* xls读
* 使用 HSSFWorkbook 进行操作
* 读取 65536行数据 占用内存 2G左右,用时 30.602s
*/
@Test
public void poi03Read() {
long start = System.currentTimeMillis();
List<List<Object>> list = new ArrayList<>();
// 工作簿
try (Workbook workbook = new HSSFWorkbook(new FileInputStream(PATH + "03big.xls"))) {
// 工作表
Sheet sheet1 = workbook.getSheetAt(0);
int lastRowNum = sheet1.getLastRowNum();
if (lastRowNum == 0) {
return;
}
Iterator<Row> iterator = sheet1.iterator();
while (iterator.hasNext()) {
ArrayList<Object> rowData = new ArrayList<>();
Row row = iterator.next();
Iterator<Cell> cellIterator = row.cellIterator();
while (cellIterator.hasNext()) {
Cell cell = cellIterator.next();
Object cellValue = getCellData(cell, workbook);
rowData.add(cellValue);
}
list.add(rowData);
}
System.out.println(list);
} catch (Exception e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println();
System.out.println(((double) end - start) / 1000);
}
07 xlsx 导入
使用XSSFWorkbook操作
读取30万数据,直接OOM,而且POI并没有为导入优化的SXSSFWorkBook
/**
* xlsx 读
* 使用 XSSFWorkbook 进行操作
* 读取30万条数据,直接OOM
*/
@Test
public void poi07Read() {
long start = System.currentTimeMillis();
List<List<Object>> list = new ArrayList<>();
// 工作簿
try (Workbook workbook = new XSSFWorkbook(new FileInputStream(PATH + "07big.xlsx"))) {
// 工作表
Sheet sheet1 = workbook.getSheetAt(0);
// 最后的行号 真实行号-1,标识下标
int lastRowNum = sheet1.getLastRowNum();
// 物理行数,真实行数
// int physicalNumberOfRows = sheet1.getPhysicalNumberOfRows();
if (lastRowNum == 0) {
return;
}
for (int i = 0; i <= lastRowNum; i++) {
ArrayList<Object> rowData = new ArrayList<>();
Row row = sheet1.getRow(i);
Iterator<Cell> cellIterator = row.cellIterator();
while (cellIterator.hasNext()) {
Cell cell = cellIterator.next();
Object cellValue = getCellData(cell, workbook);
rowData.add(cellValue);
}
list.add(rowData);
}
System.out.println(list);
} catch (Exception e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println();
System.out.println(((double) end - start) / 1000);
}
2.6 导入优化
POI对导入分为3种模式,用户模式User Model,事件模式Event Model,还有Event User Model。
- User Model(用户模式)就类似于DOM方式的解析,把文件一次性读入内存,构造一颗dom树。并且在POI对excel的抽象而知,每一行,每一个单元格都是一个对象。当文件大时,数据量多时,对内存的占用可想而知。如果文件太大,会导致OOM
- Event Model(事件模式)就是SAX解析。Event Model使用的方式时边读取,边解析,并且不会将这些数据封装成Row,Cell这些对象。而都是普通的数字或者字符串。并且这些解析出来的对象,不需要一直驻留到内存中,而是解析完,使用后就可以回收。所以相对于User Model,更加节省内存,效率更高。但是相比User Model的功能更少,门槛更高。需要了解xml解析规则
- Event User Model也是采用流式解析,但是不同于Event Model,POI基于EventModel为我们封装了一层。我们不再面对Element的事件编程,而是面相StartRow,EndRow,Cell等事件编程。而提供的数据,也不想之前是原始数据,而是全部格式化好的,方便开发者开箱急用。大大简化了我们的开发效率
XLSX
POI对XLSX支持Event Model和Event User Model,重点介绍EventUserModel模式的代码实现,因为简单易用。其他模式仅做介绍
XLSX的Event Model
使用
最直接,权威就是参考官网例子简单来说就是需要继承DefaultHandler,覆盖其startElement,endElement方法。然后方法里获取你想要的数据。
原理
DefaultHandler相信熟悉的人都知道,这是JDK自带的对XML的SAX解析用到处理类,POI在进行SAX解析时,把读取到每个XML的元素时则会回调这两个方法,然后我们就可以获取到想用的数据了。我们回忆一下上面说到的XLSX存储格式中sheet存储数据的格式。再看看官方例子中的解析过程
@Override
public void startElement(String uri, String localName, String name,
Attributes attributes) throws SAXException {
//c代表是一个单元格cell,判断c这个xml元素里面属性attribute t
// c => cell
if(name.equals("c")) {
// Print the cell reference
System.out.print(attributes.getValue("r") + " - ");
// Figure out if the value is an index in the SST
String cellType = attributes.getValue("t");
nextIsString = cellType != null && cellType.equals("s");
inlineStr = cellType != null && cellType.equals("inlineStr");
}
// Clear contents cache
lastContents = "";
}
@Override
public void endElement(String uri, String localName, String name)
throws SAXException {
// Process the last contents as required.
// Do now, as characters() may be called more than once
if(nextIsString) {
Integer idx = Integer.valueOf(lastContents);
lastContents = lruCache.get(idx);
if (lastContents == null && !lruCache.containsKey(idx)) {
lastContents = new XSSFRichTextString(sst.getEntryAt(idx)).toString();
lruCache.put(idx, lastContents);
}
nextIsString = false;
}
//v 元素代表这个cell的内容
// v => contents of a cell
// Output after we've seen the string contents
if(name.equals("v") || (inlineStr && name.equals("c"))) {
System.out.println(lastContents);
}
}
可以看出你需要对XLSX的XML格式清楚,才能获取到你想要的东西。
XLSX的Event User Model
使用
官方例子简单来说就是继承XSSFSheetXMLHandler.SheetContentsHandler,覆盖其startRow,endRow,cell 等方法。POI每开始读行,结束读行,读取一个cell。从方法名上看Event User Model有更好的用户体验。
EventUserModelTest
public class EventUserModelTest {
/**
* EventUserModel模式可以支持大数据量导出
* 100万数据
* 每次处理1000条 耗时97.786s 内存1G左右
* 每次处理10000条 耗时107.24s 内存1.3G左右
*/
@Test
public void test() {
long start = System.currentTimeMillis();
new ExcelEventUserModelParse("/media/hlh/13B69828E0E35204/A-IdeaProject-UOS/spring-boot-csdn/02-poi-easyExcel/src/file/07big.xlsx")
.setHandler(new SimpleSheetContentsHandler(1000)).parse();
long end = System.currentTimeMillis();
System.out.println();
System.out.println(((double) end - start) / 1000);
}
}
ExcelEventUserModelParse
public class ExcelEventUserModelParse {
/** 文件名称 */
private final String filename;
/** 分批处理的条数 */
private int handlerNum = 1000;
/** 解析类 */
private XSSFSheetXMLHandler.SheetContentsHandler handler;
public ExcelEventUserModelParse(String filename) {
this.filename = filename;
}
public ExcelEventUserModelParse(String filename, int handlerNum) {
this.filename = filename;
this.handlerNum = handlerNum;
}
public ExcelEventUserModelParse setHandler(XSSFSheetXMLHandler.SheetContentsHandler handler) {
this.handler = handler;
return this;
}
/**
* 统一解析入口
*/
public void parse() {
OPCPackage pkg = null;
InputStream sheetInputStream = null;
try {
pkg = OPCPackage.open(filename, PackageAccess.READ);
XSSFReader xssfReader = new XSSFReader(pkg);
StylesTable styles = xssfReader.getStylesTable();
ReadOnlySharedStringsTable readOnlySharedStringsTable = new ReadOnlySharedStringsTable(pkg);
sheetInputStream = xssfReader.getSheetsData().next();
processSheet(styles, readOnlySharedStringsTable, sheetInputStream);
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
} finally {
if (sheetInputStream != null) {
try {
sheetInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (pkg != null) {
try {
pkg.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
*
* @param styles
* @param strings
* @param sheetInputStream
* @throws SAXException
* @throws ParserConfigurationException
* @throws IOException
*/
private void processSheet(StylesTable styles, ReadOnlySharedStringsTable strings, InputStream sheetInputStream)
throws SAXException, ParserConfigurationException, IOException {
XMLReader sheetParser = SAXHelper.newXMLReader();
if (handler != null) {
sheetParser.setContentHandler(new XSSFSheetXMLHandler(styles, strings, handler, false));
} else {
sheetParser.setContentHandler(
new XSSFSheetXMLHandler(styles, strings, new SimpleSheetContentsHandler(handlerNum), false));
}
sheetParser.parse(new InputSource(sheetInputStream));
}
}
SimpleSheetContentsHandler
public class SimpleSheetContentsHandler implements XSSFSheetXMLHandler.SheetContentsHandler {
/** 当前行 */
int currRow = 0;
/** 总记录 */
List<List<String>> listData = new ArrayList<>();
/** 单条临时记录 */
List<String> rowData;
/** 分批处理的条数 */
private int handlerNum = 1000;
public SimpleSheetContentsHandler(int handlerNum) {
this.handlerNum = handlerNum;
}
/**
* 开始一行的回调
* @param rowNum 行号
*/
@Override
public void startRow(int rowNum) {
// System.out.println("startRow: " + rowNum);
rowData = new ArrayList<>();
}
/**
* 结束一行的回调
* @param rowNum 行号
*/
@Override
public void endRow(int rowNum) {
// System.out.println("endRow: " + rowNum);
// 将临时记录添加到总记录中
listData.add(rowData);
rowData = null;
currRow++;
// 如果到达指定处理数量,进行处理,清空总记录
if (currRow % handlerNum == 0) {
System.out.println(listData);
listData.clear();
}
}
/**
* 当行处理完成的回调
* @param cellReference 当前所在的单元个
* @param formattedValue 格式化之后的字符串
* @param comment 注释
*/
@Override
public void cell(String cellReference, String formattedValue, XSSFComment comment) {
// 处理每一行中的每一列
rowData.add(formattedValue);
}
@Override
public void headerFooter(String text, boolean isHeader, String tagName) {
System.out.println("headerFooter...");
System.out.println(text);
System.out.println(isHeader);
System.out.println(tagName);
}
}
原理
其实Event User Model也是 Event Model的封装,在XSSFSheetXMLHandler(其实也是一个DefaultHandler来的)中持有一个SheetContentsHandler,在其startElement,endElement方法中会调用SheetContentsHandler的startRow,endRow,cell等方法。
XLS
POI对XLS支持Event Model
使用
官网例子
需要继承HSSFListener,覆盖processRecord 方法,POI每读取到一个单元格的数据则会回调次方法。
原理
这里涉及BIFF8格式以及POI对其的封装,大家可以了解一下(因为其格式比较复杂,我也不是很清楚)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之家整理,本文链接:https://www.bmabk.com/index.php/post/68320.html