SpringBoot实现文件内容对比

     背景

        在上一篇博客中,我实践了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功能的准确度:

     后记

        这只是一个很简单的尝试,雷袭旨在通过这次实践来对之前的文件转换功能进行融汇贯通。从实用性来说,这其实也是个业务无关的小组件,如果有同行正巧需要实现类似的功能,可以直接把代码拷过去使用,人人为我,我为人人!

SpringBoot实现文件下载可以通过以下步骤: 1. 在Controller中添加一个请求处理方法,该方法返回一个ResponseEntity<byte[]>类型的结果。 2. 在该方法中,使用Java的FileInputStream类加载要下载的文件,并将其以byte数组形式返回。 3. 设置ResponseEntity的HTTP头信息,包括Content-Type和Content-Disposition,其中Content-Disposition的值为“attachment; filename=文件名.扩展名”,表示以附件形式下载文件。 4. 最后,返回ResponseEntity对象即可。 以下是示例代码: ```java @GetMapping("/download/{fileName}") public ResponseEntity<byte[]> downloadFile(@PathVariable String fileName) throws IOException { File file = new File("文件路径/" + fileName); FileInputStream fis = new FileInputStream(file); byte[] bytes = new byte[(int) file.length()]; fis.read(bytes); fis.close(); HttpHeaders headers = new HttpHeaders(); headers.add("Content-Type", "application/octet-stream"); headers.add("Content-Disposition", "attachment; filename=" + fileName); ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes, headers, HttpStatus.OK); return responseEntity; } ``` 其中,`@GetMapping("/download/{fileName}")`表示该方法处理的请求路径为/download/文件名,`@PathVariable`注解表示将路径中的文件名作为参数传入方法中。 需要注意的是,为了确保文件能够被正确下载,需要在HTTP头信息中设置Content-Length字段,该字段的值为要下载的文件大小。但是,由于文件大小通常比较大,因此这种做法可能会导致服务器资源浪费。因此,在示例代码中,我们没有设置Content-Length字段,而是将整个文件内容一次性返回。这种做法虽然不够优雅,但对于小文件来说是可行的。如果需要处理大文件,建议使用分块下载的方式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值