背景
在上一篇博客中,我实践了WORD转换成PDF/TXT的实现方式,本周接到一个新的需求,恰好就用上了这个成果。需求如下:客户提供一个WORD范本给用户,用户范本进行修改后,再反馈给客户。反馈的成果多种多样,可以是WORD,PDF或TXT,然后客户希望有一个文件对比的功能,将范本和用户修改的内容进行比对,以此来找出用户修改了哪些内容。不出所料,这个光荣而艰巨的任务又落到了雷袭的头上。
代码实践
这个小需求其实没多少技术含量,就当是一个练手小游戏吧。参考了网上的诸多实践后,我决定这么规划:后台提供接口,将范本和用户提交的文件转换成TXT,并获取TXT内容。前端得到后台的TXT内容后,通过JS函数分析比对内容,并将新增,修改,删除的内容分类展示出来,以下是代码实践。
1、在原来的后端代码的基础上,增加一个转换方法,对上传的文件进行转换,输出文件内容。
package com.leixi.fileTrans.utils;
import com.aspose.words.Document;
import com.aspose.words.SaveFormat;
import com.leixi.fileTrans.pojo.FileResponse;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.io.RandomAccessRead;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.UUID;
/**
*
* @author leixiyueqi
* @since 2024/09/03 19:39
*/
public class FileUtils {
private static final String outPath = "D:\\upload\\";
public static FileResponse compareFile(MultipartFile leftFile, MultipartFile rightFile) throws Exception {
String leftPath = transFileToTxt(leftFile);
String rightPath = transFileToTxt(rightFile);
FileResponse fileResponse = new FileResponse();
fileResponse.setFileLeftStr(readText(leftPath));
fileResponse.setFileRightStr(readText(rightPath));
return fileResponse;
}
private static String transFileToTxt(MultipartFile file) throws Exception {
String fileName =file.getOriginalFilename();
String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);
String filePath = outPath + UUID.randomUUID() + ".txt";
switch (suffix) {
case "doc":
case "docx":
transDocToTxt(file, filePath); break;
case "txt":
file.transferTo(new File(filePath));break;
case "pdf":
transPdfToTxt(file, filePath);break;
default:
throw new RuntimeException("不支持的文件类型");
}
return filePath;
}
private static void transDocToTxt(MultipartFile file, String filePath) throws Exception {
Document doc = new Document(file.getInputStream());
doc.save(filePath, SaveFormat.TEXT);
}
public static void transPdfToTxt(MultipartFile file, String filePath) throws Exception {
BufferedWriter wr = null;
File output = new File(filePath);
PDDocument pd = Loader.loadPDF(file.getBytes());
pd.save("CopyOf" + file.getName().split("\\.")[0] + ".pdf");
PDFTextStripper stripper = new PDFTextStripper();
wr = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(output)));
stripper.writeText(pd, wr);
if (pd != null) {
pd.close();
}
wr.close();
}
private static String readText(String filePath) {
StringBuilder contentBuilder = new StringBuilder();
try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
String currentLine;
while ((currentLine = br.readLine()) != null) {
String[] arr = currentLine.split("\\|\\|");
if (arr.length > 1) {
contentBuilder.append(arr[1]);
} else {
contentBuilder.append(currentLine);
}
contentBuilder.append(System.lineSeparator()); // 添加换行符
}
return contentBuilder.toString();
} catch (IOException e) {
throw new RuntimeException("读取文件失败", e);
}
}
}
2、添加一个Controller方法
@PostMapping("/compare")
public Object compare(@RequestParam(value = "leftFile") MultipartFile leftFile,
@RequestParam(value = "rightFile") MultipartFile rightFile) throws Exception{
FileResponse response = FileUtils.compareFile(leftFile, rightFile);
return response;
}
3、在resources/static下添加一个compare.html文件,内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document Comparison</title>
<style>
.wrap {
background-color: #fff;
border-radius: 4px;
padding: 10px;
}
.top {
margin: 0 -10px;
display: flex;
align-items: center;
padding-left: 20px;
}
.text-view-box {
height: 100%;
min-height: 600px;
margin: 0px -10px;
width: calc(100% + 18px);
position: relative;
background: #fff;
border-radius: 8px;
padding: 10px;
overflow: hidden;
}
.text-march-box {
width: 100%;
display: flex;
overflow: hidden;
justify-content: space-between;
}
.text-march-box._01 {
margin: 0px 10px 10px 10px;
padding-bottom: 10px;
border-bottom: 1px solid #dcdfe6;
width: calc(100% - 20px);
}
.text-march-box._02 {
height: calc(100% - 80px);
overflow-y: auto;
overflow-x: hidden;
}
.text-march-box._02::-webkit-scrollbar {
width: 3px;
height: 3px;
}
.text-view-item {
height: 100%;
margin: 0px 10px;
width: 50%;
box-sizing: border-box;
display: flex;
}
.c_warning {
color: #409eff;
}
.text-view-name {
padding-bottom: 10px;
position: relative;
color: #1f2424;
font-size: 15px;
font-weight: bold;
}
.file-name {
font-size: 13px;
padding-bottom: 10px;
display: flex;
align-items: center;
}
.source-text {
border: 1px solid #dcdfe6;
border-radius: 2px;
padding: 10px;
font-size: 14px;
color: #606266;
background: #f2f6fc;
min-height: 100%;
width: 100%;
font-family: fangsong;
line-height: 20px;
}
.spin-box {
position: absolute;
top: 0px;
width: 50%;
left: 0px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.spin-box._02 {
right: 0px;
left: auto;
}
body {
font-family: Arial, sans-serif;
background-color: #f4f4f9;
margin: 0;
padding: 20px;
}
label {
font-weight: bold;
color: #333333;
}
input[type="file"] {
padding: 10px;
border-radius: 4px;
outline: none;
transition: border-color 0.3s;
}
input[type="file"]:focus {
border-color: #007bff;
}
button {
padding: 10px 20px;
background-color: #007bff;
color: #ffffff;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #0056b3;
}
#oldFileInput::file-selector-button{
padding: 6px 10px;
background-color: #1E9FFF;
border: 1px solid #1E9FFF;
border-radius: 3px;
cursor: pointer;
color: #fff;
font-size: 12px;
}
#newFileInput::file-selector-button{
padding: 6px 10px;
background-color:#1E9FFF;
border: 1px solid #1E9FFF;
border-radius: 3px;
cursor: pointer;
color: #fff;
font-size: 12px;
}
</style>
</head>
<body>
<div class="wrap">
<div class="top">
<label for="oldFileInput">当前文档:</label>
<input type="file" id="oldFileInput" style="margin-right: 16px">
<label for="newFileInput">对比文档:</label>
<input type="file" id="newFileInput" >
<button type="primary" style="margin-left: 16px" onclick="handleCompare()">文档比对</button>
</div>
<!-- 假设这里有一个用于显示文件对比结果的地方 -->
<div class="text-view-box" style = "display: none" id="resultDiv">
<div class="text-march-box">
<div class="text-view-item">
<div class="text-view-name">源文件:</div>
<div class="file-name"><span id = "leftFileName"/></div>
</div>
<div class="text-view-item">
<div class="text-view-name">
<span class="c_warning">对比文件:</span>
</div>
<div class="file-name"><span id = "rightFileName"/></div>
</div>
</div>
<div class="text-march-box _02">
<div class="text-view-item">
<div class="source-text" id="sourceText"></div>
</div>
<div class="text-view-item">
<div class="source-text" id="targetHtml"></div>
</div>
</div>
<div class="spin-box" id="spinBox"></div>
<div class="spin-box _02" id="spinBox2"></div>
</div>
</div>
</body>
<script src="./js/diff.min.js"></script>
<script>
window.onload = function() {
showResultDiv(false);
};
function handleCompare() {
console.log("开始比较文档!")
if (oldFileInput.files[0] && newFileInput.files[0]) {
console.log(`比较文档: ${oldFileInput.files[0].name} 和 ${newFileInput.files[0].name}`);
let formData = new FormData();
formData.append('leftFile', oldFileInput.files[0]);
formData.append('rightFile', newFileInput.files[0]);
fetch('/leixi/compare', {
method: 'POST',
body: formData
}).then(response => response.json())
.then(data => {
console.log('后端返回的数据:', data);
showResultDiv(true); // 不显示 div
document.getElementById("leftFileName").textContent = oldFileInput.files[0].name;
document.getElementById("rightFileName").textContent = newFileInput.files[0].name;
// 在这里处理返回的数据
getTargetHtml(data.fileLeftStr, data.fileRightStr);
}).catch(error => {
console.error('请求失败:', error);
showResultDiv(false); // 不显示 div
}).finally(() => {
});
} else {
alert('请选择文档');
}
}
function showResultDiv(show) {
document.getElementById('resultDiv').style.display= show ? "" : "none";
}
let leftFile = {};
let rightFile = {};
let sourceTextDiv = document.getElementById('sourceText');
let targetHtmlDiv = document.getElementById('targetHtml');
function getTargetHtml(leftText, rightText) {
sourceTextDiv.innerHTML = '';
targetHtmlDiv.innerHTML = '';
const diff = Diff.diffChars(leftText, rightText);
let updateLength = 0;
for(let i = 0; i < diff.length; i++) {
let item = diff[i];
if (item.added || item.removed) {
updateLength += item.value.length;
} else {
targetHtmlDiv.innerHTML += `<span>${item.value}</span>`;
sourceTextDiv.innerHTML += `<span>${item.value}</span>`;
continue;
}
if (item.removed && diff[i + 1] && diff[i + 1].added) {
item.value = setItemValue(item.value, 'rgba(184,62,255,.4)');
sourceTextDiv.innerHTML += `<span style='background:rgba(184,62,255,.4);'>${item.value}</span>`;
continue;
}
if (item.added && diff[i - 1] && diff[i - 1].removed) {
item.value = setItemValue(item.value, 'rgba(184,62,255,.4)');
targetHtmlDiv.innerHTML += `<span style='background:rgba(184,62,255,.4);'>${item.value}</span>`;
continue;
}
if (item.added) {
item.value = setItemValue(item.value, 'rgba(103,194,58,.4)');
targetHtmlDiv.innerHTML +=`<span style='background :rgba(103,194,58,.4);'>${item.value}</span>`;
}
if (item.removed) {
item.value = setItemValue(item.value, 'rgba(255,71,109,.4)');
sourceTextDiv.innerHTML += `<span style='background:rgba(255,71,109,.4);'>${item.value}</span>`;
}
}
}
function setItemValue(value, color) {
value = value || '';
return value;
}
</script>
</html>
文中引用了一个diff.min.js文件,是一个通用的工具文件,在网上可以轻易搜到,这里就不补充了。
4、测试环节,打开页面,输入:http://127.0.0.1:19200/leixi/compare.html,选择上篇博客里转换的文件,对文档略作修改,对比的效果还是蛮准确的,通过这个功能,也可以检验上篇博客中WORD转PDF功能的准确度:
后记
这只是一个很简单的尝试,雷袭旨在通过这次实践来对之前的文件转换功能进行融汇贯通。从实用性来说,这其实也是个业务无关的小组件,如果有同行正巧需要实现类似的功能,可以直接把代码拷过去使用,人人为我,我为人人!