java实现Excel转pdf(完美格式、不使用商业库Aspose)
本文介绍了一种基于SpringBoot+Python的Excel转PDF方案,适用于Windows环境。主要内容包括:1)环境配置需安装JDK1.8和Python3.11;2)Python脚本使用win32com调用Office原生功能实现转换,包含文件验证、线程安全处理和错误处理;3)SpringBoot服务层通过调用Python脚本实现转换功能,包含文件上传、路径处理和结果返回。该方案提供了完
·
一、背景
如果有条件的可以使用Aspose商业库来实现,会简单很多。如果不想付费购买,可以使用我这个方案。
注意:本方案是基于SpringBoot+python,利用Office原生功能来实现的文档转换,因此必须运行在
window机器中。
二、环境安装
1、JDK安装
- JDK推荐1.8
2、Python安装
- python版本推荐3.11
三、Python脚本编写
import win32com.client
import os
import sys
import json
import threading
# 全局锁,确保Excel COM对象的线程安全
excel_lock = threading.Lock()
def excel_to_pdf(input_excel_path, output_pdf_path):
"""
Excel转PDF函数,支持并发安全
Args:
input_excel_path: 输入Excel文件路径
output_pdf_path: 输出PDF文件路径
Returns:
dict: 包含成功状态和消息的字典
"""
result = {
"success": False,
"message": "",
"input_path": input_excel_path,
"output_path": output_pdf_path
}
# 验证输入文件
if not os.path.exists(input_excel_path):
result["message"] = f"输入文件不存在: {input_excel_path}"
return result
# 验证输入文件格式
valid_extensions = ['.xlsx', '.xls', '.xlsm', '.xlsb']
if not any(input_excel_path.lower().endswith(ext) for ext in valid_extensions):
result["message"] = f"不支持的文件格式,支持的格式: {', '.join(valid_extensions)}"
return result
# 确保输出目录存在
output_dir = os.path.dirname(output_pdf_path)
if output_dir and not os.path.exists(output_dir):
try:
os.makedirs(output_dir, exist_ok=True)
except Exception as e:
result["message"] = f"创建输出目录失败: {str(e)}"
return result
excel = None
workbook = None
# 使用锁确保Excel COM对象的线程安全
with excel_lock:
try:
# 创建Excel应用对象
excel = win32com.client.Dispatch("Excel.Application")
excel.Visible = False # 不显示Excel界面
excel.DisplayAlerts = False # 关闭警告提示
excel.ScreenUpdating = False # 关闭屏幕更新以提高性能
# 打开工作簿
workbook = excel.Workbooks.Open(os.path.abspath(input_excel_path))
# 转换设置(核心代码)
workbook.ExportAsFixedFormat(
0, # 类型: 0=PDF, 1=XPS
os.path.abspath(output_pdf_path),
0, # 质量: 0=标准质量, 1=最小文件大小
True, # 包含文档属性
True, # 忽略打印区域
1, # 从第1页开始
1, # 到第1页结束(0表示所有页)
False # 打开PDF后不显示
)
result["success"] = True
result["message"] = f"转换成功: {output_pdf_path}"
except Exception as e:
result["message"] = f"转换失败: {str(e)}"
finally:
# 确保关闭工作簿和Excel进程
try:
if workbook:
workbook.Close(False) # 不保存更改
if excel:
excel.Quit()
# 释放COM对象
if workbook:
del workbook
if excel:
del excel
except Exception as cleanup_error:
# 记录清理错误,但不影响主要结果
if result["success"]:
result["message"] += f" (清理警告: {str(cleanup_error)})"
else:
result["message"] += f" (清理错误: {str(cleanup_error)})"
return result
def main():
"""
主函数,支持命令行调用
用法: python cover.py <input_excel_path> <output_pdf_path>
"""
if len(sys.argv) != 3:
print("用法: python cover.py <input_excel_path> <output_pdf_path>")
print("示例: python cover.py input.xlsx output.pdf")
sys.exit(1)
input_path = sys.argv[1]
output_path = sys.argv[2]
# 执行转换
result = excel_to_pdf(input_path, output_path)
# 输出JSON格式结果,便于Java程序解析
print(json.dumps(result, ensure_ascii=False, indent=2))
# 根据结果设置退出码
sys.exit(0 if result["success"] else 1)
# 使用示例
if __name__ == "__main__":
# 如果没有命令行参数,使用默认示例
if len(sys.argv) == 1:
print("Excel转PDF工具")
print("用法: python cover.py <input_excel_path> <output_pdf_path>")
print("\n示例转换:")
input_path = r"D:\test\1.xlsx" # 替换为你的Excel文件路径
output_path = r"D:\test\1.pdf" # 替换为输出PDF路径
if os.path.exists(input_path):
result = excel_to_pdf(input_path, output_path)
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(f"示例文件不存在: {input_path}")
else:
main()
可以在你的D:\test\1.xlsx 目录下创建一个1.xlsx,然后执行python cover.py来看一下转换是否成功,验证环境问题。
如果提示缺少包,则执行pip install pywin32
四、springboot调用层编写
不多说废话,直接上代码:
4.1 创建ExcelToPdfService类,代码全部如下:
package com.dhc.minboot.common;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Excel转PDF服务类 --高级版
*
* @author CGH
* @version 1.0.0
*/
@Service
public class ExcelToPdfService {
private static final Logger logger = LoggerFactory.getLogger(ExcelToPdfService.class);
private static final String tempDir = System.getProperty("java.io.tmpdir") + "excel2pdf-conversion\\";
@Value("${app.file.upload.max-size:10485760}") // 10MB
private long maxFileSize;
//@Value("${app.excel-to-pdf.python-script:C:\\Users\\Administrator\\Documents\\augment-projects\\cgh-tools\\cover.py}")
@Value("${app.excel-to-pdf.python-script:C:\\Users\\Administrator\\Desktop\\excel2pdf\\cover.py}")
private String pythonScript;
@Value("${app.excel-to-pdf.timeout:300}") // 5分钟超时
private int timeoutSeconds;
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 上传Excel文件并转换为PDF
*/
public Map<String, Object> convertExcelToPdf(MultipartFile file) {
Map<String, Object> result = new HashMap<>();
try {
// 验证文件
Map<String, Object> validation = validateFile(file);
if (!(Boolean) validation.get("success")) {
return validation;
}
// 生成唯一文件名
String fileId = UUID.randomUUID().toString();
String originalFilename = file.getOriginalFilename();
String fileExtension = getFileExtension(originalFilename);
// 创建临时目录
Path tempDirPath = Paths.get(tempDir);
if (!Files.exists(tempDirPath)) {
Files.createDirectories(tempDirPath);
}
// 保存上传的Excel文件
String inputFileName = fileId + "_input" + fileExtension;
String outputFileName = fileId + "_output.pdf";
Path inputFilePath = tempDirPath.resolve(inputFileName);
Path outputFilePath = tempDirPath.resolve(outputFileName);
// 保存文件
file.transferTo(inputFilePath.toFile());
logger.info("Excel文件已保存: {}", inputFilePath);
// 调用Python脚本进行转换
Map<String, Object> conversionResult = callPythonScript(
inputFilePath.toString(),
outputFilePath.toString()
);
if ((Boolean) conversionResult.get("success")) {
result.put("success", true);
result.put("message", "转换成功");
result.put("fileId", fileId);
result.put("originalFilename", originalFilename);
result.put("outputFilename", outputFileName);
result.put("inputPath", inputFilePath.toString());
result.put("outputPath", outputFilePath.toString());
// 检查输出文件是否存在
if (!Files.exists(outputFilePath)) {
result.put("success", false);
result.put("error", "PDF文件生成失败");
} else {
result.put("fileSize", Files.size(outputFilePath));
}
} else {
result.put("success", false);
result.put("error", "转换失败: " + conversionResult.get("message"));
// 清理输入文件
cleanupFile(inputFilePath);
}
} catch (Exception e) {
logger.error("Excel转PDF过程中发生错误", e);
result.put("success", false);
result.put("error", "转换过程中发生错误: " + e.getMessage());
}
return result;
}
/**
* 获取转换后的PDF文件
*/
public File getPdfFile(String fileId) throws IOException {
String outputFileName = fileId + "_output.pdf";
Path outputFilePath = Paths.get(tempDir).resolve(outputFileName);
if (!Files.exists(outputFilePath)) {
throw new FileNotFoundException("PDF文件不存在: " + outputFileName);
}
return outputFilePath.toFile();
}
/**
* 清理临时文件
*/
public void cleanupFiles(String fileId) {
try {
Path tempDirPath = Paths.get(tempDir);
String inputFileName = fileId + "_input";
String outputFileName = fileId + "_output.pdf";
// 清理输入文件(可能有不同扩展名)
Files.list(tempDirPath)
.filter(path -> path.getFileName().toString().startsWith(inputFileName))
.forEach(this::cleanupFile);
// 清理输出文件
Path outputFilePath = tempDirPath.resolve(outputFileName);
cleanupFile(outputFilePath);
logger.info("已清理临时文件: {}", fileId);
} catch (Exception e) {
logger.warn("清理临时文件失败: {}", fileId, e);
}
}
/**
* 验证上传的文件
*/
private Map<String, Object> validateFile(MultipartFile file) {
Map<String, Object> result = new HashMap<>();
if (file == null || file.isEmpty()) {
result.put("success", false);
result.put("error", "请选择要上传的文件");
return result;
}
// 检查文件大小
if (file.getSize() > maxFileSize) {
result.put("success", false);
result.put("error", String.format("文件大小超过限制,最大允许 %d MB", maxFileSize / 1024 / 1024));
return result;
}
// 检查文件类型
String filename = file.getOriginalFilename();
if (StringUtils.isEmpty(filename)) {
result.put("success", false);
result.put("error", "文件名不能为空");
return result;
}
String extension = getFileExtension(filename).toLowerCase();
if (!isValidExcelFile(extension)) {
result.put("success", false);
result.put("error", "不支持的文件格式,请上传Excel文件 (.xlsx, .xls, .xlsm, .xlsb)");
return result;
}
result.put("success", true);
return result;
}
/**
* 调用Python脚本进行转换
*/
private Map<String, Object> callPythonScript(String inputPath, String outputPath) {
Map<String, Object> result = new HashMap<>();
try {
// 构建命令
ProcessBuilder processBuilder = new ProcessBuilder(
"python", pythonScript, inputPath, outputPath
);
processBuilder.directory(new File(System.getProperty("user.dir")));
processBuilder.redirectErrorStream(true);
logger.info("执行Python脚本: python {} {} {}", pythonScript, inputPath, outputPath);
// 启动进程
Process process = processBuilder.start();
// 读取输出
StringBuilder output = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
}
// 等待进程完成
boolean finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly();
result.put("success", false);
result.put("message", "转换超时");
return result;
}
int exitCode = process.exitValue();
String outputStr = output.toString().trim();
logger.info("Python脚本执行完成,退出码: {}, 输出: {}", exitCode, outputStr);
if (exitCode == 0) {
// 尝试解析JSON输出
try {
JsonNode jsonResult = objectMapper.readTree(outputStr);
result.put("success", jsonResult.get("success").asBoolean());
result.put("message", jsonResult.get("message").asText());
} catch (Exception e) {
// 如果不是JSON格式,直接使用输出文本
result.put("success", true);
result.put("message", outputStr);
}
} else {
result.put("success", false);
result.put("message", outputStr);
}
} catch (Exception e) {
logger.error("调用Python脚本失败", e);
result.put("success", false);
result.put("message", "调用转换脚本失败: " + e.getMessage());
}
return result;
}
/**
* 获取文件扩展名
*/
private String getFileExtension(String filename) {
if (StringUtils.isEmpty(filename)) {
return "";
}
int lastDotIndex = filename.lastIndexOf('.');
return lastDotIndex > 0 ? filename.substring(lastDotIndex) : "";
}
/**
* 检查是否为有效的Excel文件
*/
private boolean isValidExcelFile(String extension) {
return extension.equals(".xlsx") || extension.equals(".xls") ||
extension.equals(".xlsm") || extension.equals(".xlsb");
}
/**
* 清理单个文件
*/
private void cleanupFile(Path filePath) {
try {
if (Files.exists(filePath)) {
Files.delete(filePath);
logger.debug("已删除文件: {}", filePath);
}
} catch (Exception e) {
logger.warn("删除文件失败: {}", filePath, e);
}
}
}
4.2 Controller层代码如下
/**
* 适用于Postman等工具直接调用下载
*/
@PostMapping("/test-excel2pdf2")
public ResponseEntity<Resource> convertExcelToPdfDirect(@RequestParam("file") MultipartFile file) {
try {
// 转换文件
Map<String, Object> result = excelToPdfService.convertExcelToPdf(file);
if (!(Boolean) result.get("success")) {
// 转换失败,返回错误信息
return ResponseEntity.badRequest()
.header("X-Error-Message", (String) result.get("error"))
.build();
}
// 转换成功,获取PDF文件
String fileId = (String) result.get("fileId");
String originalFilename = (String) result.get("originalFilename");
File pdfFile = excelToPdfService.getPdfFile(fileId);
Resource resource = new FileSystemResource(pdfFile);
// 生成PDF文件名
String pdfFilename = originalFilename.replaceAll("\\.[^.]+$", ".pdf");
String encodedFilename = URLEncoder.encode(pdfFilename, StandardCharsets.UTF_8.toString());
// 异步清理临时文件(延迟5秒)
new Thread(() -> {
try {
Thread.sleep(5000); // 等待5秒确保文件下载完成
excelToPdfService.cleanupFiles(fileId);
} catch (Exception e) {
// 忽略清理错误
}
}).start();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + pdfFilename + "\"; filename*=UTF-8''" + encodedFilename)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_PDF_VALUE)
.header("X-Original-Filename", originalFilename)
.header("X-Converted-Filename", pdfFilename)
.contentLength(pdfFile.length())
.body(resource);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
完结撒花,大家可以拿来直接用,无需任何改动哦!o( ̄▽ ̄)ブ
更多推荐
所有评论(0)