您当前的位置:首页 > 电脑百科 > 软件技术 > 软件技术

EasyExcel带格式多线程导出百万数据(实测好用)

时间:2023-03-09 14:33:45  来源:CSDN  作者:菜C++鸡

前言

以下为结合实际情况作的方案设计,导出阈值以及单sheet页条数都可以根据实际情况调整

大佬可直接跳过新手教程,直接查看文末代码

背景说明

针对明细报表,用户会选择针对当前明细数据进行导出,便于本地或者线下进行处理或者计算等需求。不过一般在这种大数据量的导出任务下,会引发以下问题:

 

  • 响应时间较慢;

  • 内存资源占用过大,基本上一个大数据量的导出会消耗可视化服务的所有资源,引起内存回收,其它接口无响应;

     

考虑到单个Excel文件过大,采用压缩文件流zip的方式,一个excel只有一个页签,一个页签最多十万条数据,所以少于十万条数据,会导出excel文件,而非zip压缩文件。

另外,这里导出功能的速率不能单以数据条数为量级进行衡量,平常一般一万条数据就是1M字节。较为准确的公式如下(借此就可以评估出很多数据导出的文件大小):

 

文件大小1M字节 = 字段列数15个 * 数据条数一万条
方案概述 1)大数据量导出问题主要是以下三个地方:
  • 资源占用

  • 内存(也是资源的一个,单独说明)

  • 响应时间

针对以上三个问题,大方向考虑的是多线程结合数据流写入的方式。多线程:使用空间换时间,主要是加快接口响应时间,但是这里线程数不宜过多,一味加快响应时间提升线程数,资源占用会非常严重,故会考虑线程池,线程池的线程数为10;数据流:数据的IO-读取/写入等操作一般都是通过“数据包”的方式,即将结果数据作为一个整体,这样如果数据量多的话,会非常占用内存,所以采用数据流的方式,而且导出的时候会进行格式设置(单元格合并、背景色、字体样式等),一直使用的是Alibaba EasyExcel组件,并且Alibaba EasyExcel组件支持数据流的方式读取/写入数据。

2)将写入导出Excel等功能单独分开成一个微服务:

注意:如果单个服务分配的资源足够的话,可以不用将导出功能与原应用服务拆开,这里可以省略

 

  • 抢占资源

  • 由于导出功能内存溢出,如果不做分离独立,整个应用服务也会宕机

  • JAVA技术栈路线:https://www.yoodb.com/

     

3)注意:
  • 多线程下,同一页签的写入不可同步,即Alibaba EasyExcel组件的文件写入流SheetWriter是异步的;

  • 多线程下,每个线程所用的文件写入SheetWriter是一个复制,依旧会占用大量内存;

  • 微服务拆分时,数据读取和文件写入是在一个线程下的,所以新的微服务也要实现一套数据读取逻辑;

  • 压缩文件使用压缩文件流,ZipOutputStream,不需要暂存本地;

     

4)方案设计:

 

标注说明

1) 阈值可以进行设置,考虑到业务场景以及资源使用,这里阈值数据量为100w条,超过一百万会导出空表(而非导出一百万数据)

2) 导出进行多线程,启用最多十个多线程(默认最多一百万条数据,一个sheet页十万条数据),每个线程会进行两个动作,查询数据以及数据写入操作,(如果数据量较少,依旧是适用的)

3) 说明图,以86万数据为例,也就是说会启用九个文件写入线程,一个文件写入线程生成一个excel导出文件;

4) 线程池为队列线程,即后来的线程进入排队等待,队列长度(线程池大小)为10;

5) 每个文件写入线程会生成最多十个sheet(默认一个sheet页十万数据)写入线程(最后一个文件写入线程可能会少于十个)。

5)maven依赖:
com.alibabagroupId>
easyexcelartifactId>
2.1.7version>
dependency>

详细设计

1)文件写入多线程,按每个文件十万条数据进行导出,每个文件写入线程生成一个excel文件(单页签);

 

  • ROW_SIZE:一次查询的数据量,此处设置为10000条

  • ROW_PAGE:一个页签多少次查询,此处设置为10次;

     

private static Interger ROW_SIZE = 10000;
private static interger ROW_PAGE = 10;
* divide into sheets with 10W data per sheet
int sheetCount = (rowCount/ (ROW_SIZE*ROW_PAGE))+1;
for(int i=0;i threadExecutor.submit(()->{sheetWrite()});

 

2)sheet写入多线程,最后一个文件写入线程的最后一个sheet写入线程可能不足1W条数据;

// 单sheet页写入数
int sheetThreadCount = rowCount - (i+1)*(ROW_SIZE*ROW_PAGE) > 0 ? ROW_PAGE : (rowCount - i*(ROW_SIZE*ROW_PAGE))/ROW_SIZE+1;
CountDownLatch threadSignal = new CountDownLatch(sheetThreadCount);
for(int j=0;j threadExecutor.submit(()->{excelWriter()});
threadSignal.awAIt();

3)异步写入sheet文件,不同的文件写入线程写入不同的文件,所以只需要保证同一个文件写入线程下不同sheet写入线程的excelWriter异步即可;

// 获取数据
// todo
// 数据格式处理
Synchronized(excelWriter){
WriteSheet writeSheet = EasyExcel.writerSheet(sheetNo, "第" + (sheetNo+1) + "页数据");
excelWriter.write(lists, writeSheet);

4)压缩文件,将多个excel压缩成一个zip,最后上传至fast dfs,返回前端下载地址,使用hutool封装的ZipUtil方法;

package cn.hutool.core.util;
String[] paths = new String[10];
FileInputStream[] ins = new FileInputStream[10];
ZipUtil.zip(OutputStream out, String[] paths, InputStream[] ins);
byte[] bytes = outputStream.toByteArray();
// 上传文件到FastDFS 返回上传路径
String path = fastWrApper.uploadFile(bytes, bytes.length, "zip");
return path + "?filename=" + fileName;

缓存

每次请求是生成一个文件并上传至FastDFS服务器上,然后将下载路径返回给前端,有时多个用户频繁下载同一个文件(或者用户短时间内点击同一个下载任务)。针对以上情况,考虑采用缓存,将第一次的数据缓存下来。另外,更多java面试资料,公众号Java精选,回复java面试,获取面试资料,支持在线刷题。

① 请求参数较多,需要根据参数判断是否为同一个下载文件请求;

 

  • 数据集ID

  • 过滤器

  • 数据量

  • 数据集字段(先根据ID排序,再进行拼接)

     

② 设置过期时间(30分钟),不考虑数据一致性的问题(即数据源数据更改后,再更新缓存)。仅仅是做初步工作,即短时间内,只要符合条件①且时间未过期,就采用同一份数据;

③ 当请求下载的为同一份文件时,只是文件名不同时,依旧采用同一份缓存数据;

 

注:针对于数据一致性的问题,不对单个数据的内容变更进行考虑,原因是大数据量下,数据是否有变更的判断较为复杂,不现实。这里只判断在相同的请求条件下的总条数。
可行性验证

 

1)单个文件写入下,176个字段,14140条数据,excel大小15M,响应时间为14.66s(未报错,未触发异常)

2)单个文件写入下,14个字段,98万数据,excel大小为96M,响应时间为42.41s(未报错,未触发异常)

3)拆分微服务下,14个字段,98万数据,zip大小为104M,平均响应时间为27.34s(未报错,未触发异常)

代码

文件导出核心代码

TableExport.java

public String exportTable(ExportTable exportTable) throws Exception {
StringBuffer path = new StringBuffer();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
StringBuffer sign = new StringBuffer ();
//redis key
sign.append(exportTable.getId());
try {
// 用来记录需要为 行 列设置样式
Map>>> map = new HashMap<>();
sign.append("#").append(String.join(",", fields.stream().map(e-> e.isShow()?"true":"false").collect(Collectors.toList())));
setFontStyle(0, 0, exportTable.getFields(), map);
// 获取表头长度
int headRow = head.stream().max(Comparator.comparingInt(List::size)).get().size();

 

// 数据量超过十万 则不带样式
// 只处理表头:表头合并 表头隐藏 表头冻结
if(rowCount*fields.size() > ROW_SIZE*6.4){
map.put("cellStyle", null);
}
sign.append("#").append(exportTable.getStyle());
// 数据量超过百万或者数据为空,只返回有表头得单元格
if(rowCount==0 || rowCount*fields.size() >= ROW_SIZE*1500){
EasyExcel.write(outputStream)
// 这里放入动态头
.head(head).sheet("数据")
// 传入表头样式
.registerWriteHandler(EasyExcelUtils.getStyleStrategy())
// 当然这里数据也可以用 List> 去传入
.doWrite(new LinkedList<>());
byte[] bytes = outputStream.toByteArray();
// 上传文件到FastDFS 返回上传路径
return fastWrapper.uploadFile(bytes, bytes.length, "xlsx") + "?filename=" + fileName + ".xlsx";
}
sign.append("#").append(rowCount);
String fieldSign = fields.stream().sorted(Comparator.comparing(ExportTable.ExportColumn::getId))
.map(e->e.getId()).collect(Collectors.joining(","));
sign.append("#").append(fieldSign);
/**
* 相同的下载文件请求 直接返回
* the redis combines with datasetId - filter - size of data - fields
*/
if (redisClientImpl.hasKey(sign.toString())){
return redisClientImpl.get(sign.toString()).toString();
}
/**
* 分sheet页
* divide into sheets with 10M data per sheet
*/
int sheetCount = (rowCount/ (ROW_SIZE*ROW_PAGE))+1;
String[] paths = new String[sheetCount];
ByteArrayInputStream[] ins = new ByteArrayInputStream[sheetCount];

CountDownLatch threadSignal = new CountDownLatch(sheetCount);
for(int i=0;i int finalI = i;
String finalTable = table;
Datasource finalDs = ds;
String finalOrder = order;
int finalRowCount = rowCount;
threadExecutor.submit(()->{
// excel文件流
ByteArrayOutputStream singleOutputStream = new ByteArrayOutputStream();
ExcelWriter excelWriter = EasyExcel.write(singleOutputStream).build();
// 单sheet页写入数
int sheetThreadCount = finalI == (sheetCount-1) ? (finalRowCount - finalI *(ROW_SIZE*ROW_PAGE))/ROW_SIZE+1 : ROW_PAGE;
CountDownLatch sheetThreadSignal = new CountDownLatch(sheetThreadCount);
for(int j=0;j int page = finalI *ROW_PAGE + j + 1;
// 最后一页数据
int pageSize = j==(sheetThreadCount-1)&& finalI ==(sheetCount-1) ? finalRowCount %ROW_SIZE : ROW_SIZE;
threadExecutor.submit(()->{
try {
writeExcel(dataSetTableRequest, datasetTable, finalTable, qp,
datasetTableFields, exportTable, page, pageSize, finalDs, datasourceProvider,
fieldArray, fields, head, map, headRow, excelWriter, mergeIndex, finalOrder);
sheetThreadSignal.countDown();
} catch (Exception e) {
e.printStackTrace();
}
});
}
try {
sheetThreadSignal.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 关闭写入流
excelWriter.finish();
paths[finalI] = (finalI +1) + "-" + fileName + ".xlsx";
// 单文件
if (sheetCount == 1){
// xlsx
// 将sign存入redis并设置过期时间
}
threadSignal.countDown();
});
}
threadSignal.await();

if (sheetCount != 1){
ZipUtil.zip(outputStream, paths, ins);
byte[] bytes = outputStream.toByteArray();
// 上传文件到FastDFS 返回上传路径
path.append(fastWrapper.uploadFile(bytes, bytes.length, "zip"))
.append("?filename=").append(fileName).append(".zip");
// 将sign存入redis并设置过期时间
redisClientImpl.set(sign.toString(), path.toString(), SYS_REDIS_EXPIRE_TIME);
}
} catch (Exception e) {
e.printStackTrace();
}
return path.toString();
}

private void writeExcel(ExcelWriter excelWriter){
//数据查询
// todo
synchronized (excelWriter) {
WriteSheet writeSheet = EasyExcel.writerSheet(0, "第" + 1 + "页数据")
// 这里放入动态头
.head(head)
//传入样式
.registerWriteHandler(EasyExcelUtils.getStyleStrategy())
.registerWriteHandler(new CellColorSheetWriteHandler(map, headRow))
.registerWriteHandler(new MergeStrategy(lists.size(), mergeIndex))
// 当然这里数据也可以用 List> 去传入
.build();
excelWriter.write(lists, writeSheet);
}
}

Excel导出的文件流样式处理类。

CellColorSheetWriteHandler.java

import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.util.StyleUtil;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import org.Apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFColor;
import org.apache.poi.xssf.usermodel.XSSFFont;

 

import java.awt.Color;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
* @Author 菜鸟
* @description 拦截处理单元格创建
*/
public class CellColorSheetWriteHandler implements CellWriteHandler
{
/**
* 多行表头行号
*/
private int headRow;

/**
* 字体
*/
private ExportTable.ExportColumn.Font columnFont = new ExportTable.ExportColumn.Font();

private static volatile XSSFCellStyle cellStyle = null;

public static XSSFCellStyle getCellStyle(Workbook workbook, WriteCellStyle contentWriteCellStyle) {
if(cellStyle == null) {
synchronized (XSSFCellStyle.class) {
if(cellStyle == null) {
cellStyle =(XSSFCellStyle) StyleUtil.buildHeadCellStyle(workbook, contentWriteCellStyle);
}
}
}
return cellStyle;
}

/**
* 字体
* Map 当前列的字段样式
* Map>> 当前行包含那几列需要设置样式
* String head:表头;
* String cell:内容;
*/
private Map>>> map;

/**
* 有参构造
*/
public CellColorSheetWriteHandler(Map>>> map, int headRow) {
this.map = map;
this.headRow = headRow;
}

/**
* 在单元上的所有操作完成后调用
*/
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
// 当前行的第column列
int column = cell.getColumnIndex();
// 当前第row行
int row = cell.getRowIndex();
AtomicInteger fixNum = new AtomicInteger();
// 处理行,表头
if (headRow > row && map.containsKey("head")){
Map>> fonts = map.get("head");
fonts.get(row).forEach(e->{
e.entrySet().forEach(ele -> {
// 获取冻结字段
if (null != ele.getValue().getFixed() && !StringUtils.isEmpty(ele.getValue().getFixed())) {
fixNum.getAndIncrement();
}
// 字段隐藏
if(!ele.getValue().isShow()){
writeSheetHolder.getSheet().setColumnHidden(ele.getKey(), true);
}
});
});
if (fixNum.get() > 0 && row == 0) {
writeSheetHolder.getSheet().createFreezePane(fixNum.get(), headRow, fixNum.get(), headRow);
}else{
writeSheetHolder.getSheet().createFreezePane(0, headRow, 0, headRow);
}
setStyle(fonts, row, column, cell, writeSheetHolder, head);
}
// 处理内容
if (headRow <= row && map.containsKey("cell") && !map.containsKey("cellStyle")) {
Map>> fonts = map.get("cell");
setStyle(fonts, -1, column, cell, writeSheetHolder, head);
}
}

private void setStyle(Map>> fonts, int row, int column, Cell cell, WriteSheetHolder writeSheetHolder, Head head){
fonts.get(row).forEach(e->{
if (e.containsKey(column)){
// 根据单元格获取workbook
Workbook workbook = cell.getSheet().getWorkbook();
//设置列宽
if(null != e.get(column).getWidth() && !e.get(column).getWidth().isEmpty()) {
writeSheetHolder.getSheet().setColumnWidth(head.getColumnIndex(), Integer.parseInt(e.get(column).getWidth()) * 20);
}else{
writeSheetHolder.getSheet().setColumnWidth(head.getColumnIndex(),2000);
}
// 单元格策略
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
// 设置垂直居中为居中对齐
contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
// 设置左右对齐方式
if(null != e.get(column).getAlign() && !e.get(column).getAlign().isEmpty()) {
contentWriteCellStyle.setHorizontalAlignment(getHorizontalAlignment(e.get(column).getAlign()));
}else{
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
}
if (!e.get(column).equal(columnFont) || column == 0){
/**
* Prevent the creation of a large number of objects
* Defects of the EasyExcel tool(巨坑,简直脱发神器)
*/
cellStyle = (XSSFCellStyle) StyleUtil.buildHeadCellStyle(workbook, contentWriteCellStyle);
// 设置单元格背景颜色
if(null != e.get(column).getBackground() && !e.get(column).getBackground().isEmpty()) {
cellStyle.setFillForegroundColor(new XSSFColor(hex2Color(e.get(column).getBackground())));
}else{
if(cell.getRowIndex() >= headRow)
cellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
}

// 创建字体实例
Font font = workbook.createFont();
// 设置字体是否加粗
if(null != e.get(column).getFontWeight() && !e.get(column).getFontWeight().isEmpty())
font.setBold(getBold(e.get(column).getFontWeight()));
// 设置字体和大小
if(null != e.get(column).getFontFamily() && !e.get(column).getFontFamily().isEmpty())
font.setFontName(e.get(column).getFontFamily());
if(0 != e.get(column).getFontSize())
font.setFontHeightInPoints((short) e.get(column).getFontSize());
XSSFFont xssfFont = (XSSFFont)font;
//设置字体颜色
if(null != e.get(column).getColor() && !e.get(column).getColor().isEmpty())
xssfFont.setColor(new XSSFColor(hex2Color(e.get(column).getColor())));
cellStyle.setFont(xssfFont);
// 记录上一个样式
columnFont = e.get(column);
}

// 设置当前行第column列的样式
cell.getRow().getCell(column).setCellStyle(cellStyle);
// 设置行高
cell.getRow().setHeight((short) 400);
}
});
}
}

Excel导出的默认样式设置类。

EasyExcelUtils.java

public static HorizontalCellStyleStrategy getStyleStrategy(){
// 头的策略
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
// 背景设置为灰色
headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
WriteFont headWriteFont = new WriteFont();
headWriteFont.setFontHeightInPoints((short)12);
// 字体样式
headWriteFont.setFontName("Frozen");
// 字体颜色
headWriteFont.setColor(IndexedColors.BLACK1.getIndex());
headWriteCellStyle.setWriteFont(headWriteFont);
// 自动换行
headWriteCellStyle.setWrapped(false);
// 水平对齐方式(修改默认对齐方式——4.14 版本1.3.2)
headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
// 垂直对齐方式
headWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);

 

// 内容的策略
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
// 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.头默认了 FillPatternType所以可以不指定
// contentWriteCellStyle.setFillPatternType(FillPatternType.SQUARES);
// 背景白色
contentWriteCellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
// 水平对齐方式(修改默认对齐方式——4.14 版本1.3.2)
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
WriteFont contentWriteFont = new WriteFont();
// 字体大小
contentWriteFont.setFontHeightInPoints((short)12);
// 字体样式
contentWriteFont.setFontName("Calibri");
contentWriteCellStyle.setWriteFont(contentWriteFont);
// 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现
return new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
}

Excel导出合并单元格处理类。

MergeStrategy.class

import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import org.apache.commons.collections.map.HashedMap;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;

 

import java.util.*;

/**
* @Author 菜鸡
* @description 合并单元格策略
*/
public class MergeStrategy extends AbstractMergeStrategy
{

/**
* 合并的列编号,从0开始
* 指定的index或自己按字段顺序数
*/
private Set mergeCellIndex = new HashSet<>();

/**
* 数据集大小,用于区别结束行位置
*/
private Integer maxRow = 0;

// 禁止无参声明
private MergeStrategy() {
}

public MergeStrategy(Integer maxRow, Set mergeCellIndex) {
this.mergeCellIndex = mergeCellIndex;
this.maxRow = maxRow;
}

private Map lastRow = new HashedMap();

@Override
protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
int currentCellIndex = cell.getColumnIndex();
// 判断该行是否需要合并
if (mergeCellIndex.contains(currentCellIndex)) {
String currentCellValue = cell.getStringCellValue();
int currentRowIndex = cell.getRowIndex();
if (!lastRow.containsKey(currentCellIndex)) {
// 记录首行起始位置
lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex));
return;
}
//有上行这列的值了,拿来对比.
MergeRange mergeRange = lastRow.get(currentCellIndex);
if (!(mergeRange.lastValue != null && mergeRange.lastValue.equals(currentCellValue))) {
// 结束的位置触发下合并.
// 同行同列不能合并,会抛异常
if (mergeRange.startRow != mergeRange.endRow || mergeRange.startCell != mergeRange.endCell) {
sheet.addMergedRegionUnsafe(new CellRangeAddress(mergeRange.startRow, mergeRange.endRow, mergeRange.startCell, mergeRange.endCell));
}
// 更新当前列起始位置
lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex));
}
// 合并行 + 1
mergeRange.endRow += 1;
// 结束的位置触发下最后一次没完成的合并
if (relativeRowIndex.equals(maxRow - 1)) {
MergeRange lastMergeRange = lastRow.get(currentCellIndex);
// 同行同列不能合并,会抛异常
if (lastMergeRange.startRow != lastMergeRange.endRow || lastMergeRange.startCell != lastMergeRange.endCell) {
sheet.addMergedRegionUnsafe(new CellRangeAddress(lastMergeRange.startRow, lastMergeRange.endRow, lastMergeRange.startCell, lastMergeRange.endCell));
}
}
}
}
}

class MergeRange {
public int startRow;
public int endRow;
public int startCell;
public int endCell;
public String lastValue;

public MergeRange(String lastValue, int startRow, int endRow, int startCell, int endCell) {
this.startRow = startRow;
this.endRow = endRow;
this.startCell = startCell;
this.endCell = endCell;
this.lastValue = lastValue;
}
}

 

作者:菜C++鸡java https://blog.csdn.NET/qq_40921561/article/details/126764038


Tags:EasyExcel   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
EasyExcel带格式多线程导出百万数据(实测好用)
前言以下为结合实际情况作的方案设计,导出阈值以及单sheet页条数都可以根据实际情况调整大佬可直接跳过新手教程,直接查看文末代码背景说明针对明细报表,用户会选择针对当前明...【详细内容】
2023-03-09  Search: EasyExcel  点击:(237)  评论:(0)  加入收藏
▌简易百科推荐
如何在Windows 10中查看电脑的名称?这里提供详细步骤
你想在有多台计算机组成的网络上查找你的计算机吗?一种方法是找到你的电脑名称,然后在网络上匹配该名称。下面是如何在Windows 10中使用图形和命令行方法查看你的计算机名称。...【详细内容】
2024-04-10  驾驭信息纵横科技    Tags:Windows 10   点击:(2)  评论:(0)  加入收藏
移动版 Outlook 解锁新技能,可验证登录 OneDrive 等微软服务
IT之家 4 月 9 日消息,微软公司近日发布新闻稿,宣布用户可以使用 Outlook 手机应用,轻松登录 Teams、OneDrive、Microsoft 365 以及 Windows 等微软账号服务。移动端 Outlook...【详细内容】
2024-04-09    IT之家  Tags:Outlook   点击:(0)  评论:(0)  加入收藏
Win10/Win11和 macOS用户反馈:谷歌云服务“捆绑”系统 DNS 设置
IT之家 4 月 6 日消息,谷歌公司承认旗下的 Google One 订阅服务中存在问题,在 Windows 10、Windows 11 以及 macOS 系统上会更改系统 DNS 设置,变更为 8.8.8.8 地址。Google On...【详细内容】
2024-04-08    IT之家  Tags:Win10   点击:(7)  评论:(0)  加入收藏
电脑卡顿怎么重装系统,快看这篇
电脑卡顿时,重装系统确实是一种可能的解决方案。以下是重装系统的详细步骤:备份重要数据:首先,你需要将电脑中的重要文件和数据备份到外部存储设备(如U盘、移动硬盘或云存储)中,以...【详细内容】
2024-04-04  科技数码前锋    Tags:重装系统   点击:(2)  评论:(0)  加入收藏
如何检查电脑的最近历史记录?这里提供详细步骤
如果你怀疑有人在使用你的计算机,并且你想查看他们在做什么,下面是如何查看是否有访问内容的痕迹。如何检查我的计算机的最近历史记录要检查计算机的最近历史记录,应该从web浏...【详细内容】
2024-03-30  驾驭信息纵横科技    Tags:历史记录   点击:(0)  评论:(0)  加入收藏
关于Windows中AppData的相关知识,看这篇文章就可以了
如果AppData文件夹占用了你电脑上的太多空间,则需要清理AppData文件夹。下面是一些帮助你在Windows计算机上进行AppData清理的方法。什么是AppData文件夹AppData文件夹是保存...【详细内容】
2024-03-30  驾驭信息纵横科技    Tags:AppData   点击:(2)  评论:(0)  加入收藏
微软 Edge 浏览器将迎来“内存限制器”功能,用户可自主控制 Edge 内存占用
IT之家 3 月 28 日消息,微软即将为其 Edge 浏览器带来一项实用新功能,据悉该公司正在测试一项内置的内存限制器,这项功能可以让用户限制 Edge 所占用的内存,防止浏览器超出内存...【详细内容】
2024-03-29    IT之家  Tags:Edge   点击:(15)  评论:(0)  加入收藏
一寸照片的大小如何压缩?四个实测效果很好的方法
一寸照片作为生活中常见的尺寸之一,常用于各类证件照与证明文件的制作。然而,受限于其较为狭小的尺寸,上传及打印过程中很容易出现尺寸超限的情况。所以,这个时候就需要对其体积...【详细内容】
2024-03-18  宠物小阿涛    Tags:压缩   点击:(14)  评论:(0)  加入收藏
手机投屏到电脑/电视的方法
方法一:Win10自带的投影功能1、将手机和电脑连接同一个无线网络。2、选择【开始】>【设置】>【系统】>【投影到此电脑】3、将默认的始终关闭的选项更改为所有位置都可用。4、...【详细内容】
2024-03-18    老吴讲I  Tags:投屏   点击:(16)  评论:(0)  加入收藏
微软商店怎么卸载应用 一分钟快速看懂!
微软商店怎么卸载应用 一分钟快速看懂!微软公司(Microsoft Corporation)是一家全球领先的科技企业,总部位于美国华盛顿州的雷德蒙德。成立于1975年,由比尔&middot;盖茨和保罗&mid...【详细内容】
2024-02-27  婷婷说体育    Tags:微软商店   点击:(36)  评论:(0)  加入收藏
相关文章
    无相关信息
站内最新
站内热门
站内头条