文件上传与解析文档
本文档描述了如何通过文件上传接口实现文件的去重检测、上传以及内容解析,供大模型文问答系统作为上下文信息使用。内容主要分为以下几个部分:
- API 设计:包括文件上传前的去重检测接口以及文件上传接口的说明。
- 数据库设计:存储上传文件基本信息的数据库表设计。
- 文件解析实现:针对不同类型文件的解析实现代码(PDF、Word、Excel、PPT、图片及音视频文件)。
- 服务实现:如何将文件解析与上传服务整合,包括文件上传服务
ChatUploadService
和 API 请求处理类ApiChatUploadHandler
。
1. API 设计
1.1 文件去重检测接口
- 接口地址:
/api/v1/chat/file
- 说明:
前端在上传文件前需计算文件的 MD5 值,通过此接口检测文件是否已经上传过。
1.2 文件上传接口
- 接口地址:
/api/v1/chat/upload
- 说明:
通过该接口上传文件。上传成功后,返回一个标识文件的 id(即 file_id)。
支持的文件类型分为三类:
- 文档类:
- PDF (.pdf)
- Word (.docx)
- Excel (.xlsx)
- PPT (.pptx)
- Txt (.txt)
- Markdown (.md)
- 图片类:
- PNG (.png)
- JPG (.jpg)
- JPEG (.jpeg)
- 音视频类:
- FLAC (.flac)
- MP3 (.mp3)
- MP4 (.mp4)
- MPEG (.mpeg)
- MPGA (.mpga)
- M4A (.m4a)
- OGG (.ogg)
- WAV (.wav)
- WEBM (.webm)
请求参数
file: binary
category: string
响应参数
{
"data": {
"name": "7月商品销售表.xlsx",
"id": "484148184794959872",
"size": "10257",
"content": null,
"targetName": null,
"md5": "3eb755b2bf70b2c47b273ef00f9e44ea",
"url": "https://rumiapp.s3.us-west-1.amazonaws.com/chat/484148181326270464.xlsx"
},
"msg": null,
"code": 1,
"ok": true,
"error": null
}
2. 数据库设计
数据库中保存了上传文件的基本信息。表结构如下所示(使用 SQL 语句创建表):
drop table if exists chat_upload_file;
CREATE TABLE chat_upload_file (
id BIGINT primary key,
md5 VARCHAR(32) NOT NULL,
name VARCHAR(1024) NOT NULL,
content text,
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted SMALLINT NOT NULL DEFAULT 0,
tenant_id BIGINT NOT NULL DEFAULT 0
);
说明:
- 每个文件均有唯一的
id
标识,使用 MD5 值来进行文件去重检测。 content
字段保存了文件解析后的文本内容,方便后续进行大模型文问答上下文的构建。
3. 文件解析实现
在文件上传后,需要对文件进行解析以提取文本内容。下面分别介绍各类文件的解析实现代码。
3.1 PDF 文件解析
使用 Apache PDFBox 对 PDF 文件进行解析。示例代码如下:
package com.litongjava.llm.utils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
public class PdfUtils {
/**
* 从PDF字节数据中提取文本内容
* @param pdfBytes PDF文件的字节数组
* @return 提取的文本内容
* @throws IOException 如果PDF解析失败
*/
public static String parseContent(byte[] pdfBytes) throws IOException {
try (InputStream is = new ByteArrayInputStream(pdfBytes); PDDocument document = PDDocument.load(is)) {
PDFTextStripper stripper = new PDFTextStripper();
stripper.setSortByPosition(true); // 按页面布局排序
stripper.setAddMoreFormatting(true); // 保留更多格式信息
return stripper.getText(document);
}
}
}
3.2 Word 文档解析
利用 Apache POI 读取 docx 文档的段落和表格内容,示例代码如下:
package com.litongjava.llm.utils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
public class DocxUtils {
public static String parseDocx(byte[] fileData) throws IOException {
StringBuilder content = new StringBuilder();
try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(fileData))) {
// 读取段落
for (XWPFParagraph paragraph : document.getParagraphs()) {
content.append(paragraph.getText()).append("\n");
}
// 读取表格
for (XWPFTable table : document.getTables()) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
content.append(cell.getText()).append("\t");
}
content.append("\n");
}
}
}
return content.toString().trim();
}
}
3.3 Excel 文件解析
利用 Apache POI 的 XSSF 对 xlsx 文件进行解析,遍历所有工作表、行和单元格,示例代码如下:
package com.litongjava.llm.utils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
public class ExcelUtils {
/**
* 从xlsx字节数据中提取文本内容
* @param fileData xlsx文件的字节数组
* @return 提取的文本内容
* @throws IOException 如果xlsx解析失败
*/
public static String parseXlsx(byte[] fileData) throws IOException {
StringBuilder content = new StringBuilder();
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(fileData))) {
int numberOfSheets = workbook.getNumberOfSheets();
for (int i = 0; i < numberOfSheets; i++) {
Sheet sheet = workbook.getSheetAt(i);
content.append("Sheet: ").append(sheet.getSheetName()).append("\n");
for (Row row : sheet) {
for (Cell cell : row) {
content.append(cell.toString()).append("\t");
}
content.append("\n");
}
content.append("----- End of Sheet -----\n");
}
}
return content.toString().trim();
}
}
3.4 PPT 文件解析
通过 Apache POI 的 XMLSlideShow 类解析 pptx 文件,将每一页幻灯片中的文本提取出来:
package com.litongjava.llm.utils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import org.apache.poi.xslf.usermodel.XMLSlideShow;
import org.apache.poi.xslf.usermodel.XSLFSlide;
import org.apache.poi.xslf.usermodel.XSLFShape;
import org.apache.poi.xslf.usermodel.XSLFTextShape;
public class PptxUtils {
/**
* 从pptx字节数据中提取文本内容
* @param fileData pptx文件的字节数组
* @return 提取的文本内容
* @throws IOException 如果pptx解析失败
*/
public static String parsePptx(byte[] fileData) throws IOException {
StringBuilder content = new StringBuilder();
try (XMLSlideShow slideShow = new XMLSlideShow(new ByteArrayInputStream(fileData))) {
int slideIndex = 1;
for (XSLFSlide slide : slideShow.getSlides()) {
content.append("Slide ").append(slideIndex++).append(":\n");
for (XSLFShape shape : slide.getShapes()) {
if (shape instanceof XSLFTextShape) {
XSLFTextShape textShape = (XSLFTextShape) shape;
content.append(textShape.getText()).append("\n");
}
}
content.append("----- End of Slide -----\n");
}
}
return content.toString().trim();
}
}
3.5 文件解析逻辑整合 – ChatFileService
该服务根据文件类型选择对应的解析方法,同时支持文本类文件(txt、md)直接转换字符串,以及图片类型文件由 OCR 服务解析。代码如下:
package com.litongjava.llm.service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import com.litongjava.groq.GropConst;
import com.litongjava.groq.GropModel;
import com.litongjava.groq.GroqSpeechClient;
import com.litongjava.groq.TranscriptionsRequest;
import com.litongjava.groq.TranscriptionsResponse;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.llm.utils.DocxUtils;
import com.litongjava.llm.utils.PdfUtils;
import com.litongjava.llm.utils.ExcelUtils;
import com.litongjava.llm.utils.PptxUtils;
import com.litongjava.tio.http.common.UploadFile;
import com.litongjava.tio.utils.hutool.FilenameUtils;
public class ChatFileService {
public String parseContent(UploadFile uploadFile) throws IOException {
String name = uploadFile.getName();
byte[] data = uploadFile.getData();
String suffix = FilenameUtils.getSuffix(name).toLowerCase();
String text = null;
if ("txt".equals(suffix) || "md".equals(suffix)) {
text = new String(data, StandardCharsets.UTF_8);
} else if (GropConst.SUPPORT_LIST.contains(suffix)) {
TranscriptionsRequest transcriptionsRequest = new TranscriptionsRequest();
transcriptionsRequest.setModel(GropModel.WHISPER_LARGE_V3_TURBO);
TranscriptionsResponse transcriptions = GroqSpeechClient.transcriptions(data, name, transcriptionsRequest);
text = transcriptions.getText();
} else if ("pdf".equals(suffix)) {
text = PdfUtils.parseContent(data);
} else if ("docx".equals(suffix)) {
text = DocxUtils.parseDocx(data);
} else if ("xlsx".equals(suffix)) {
text = ExcelUtils.parseXlsx(data);
} else if ("pptx".equals(suffix)) {
text = PptxUtils.parsePptx(data);
} else if ("jpg".equals(suffix) || "jpeg".equals(suffix) || "png".equals(suffix)) {
text = Aop.get(LlmOcrService.class).parse(data, name);
}
return text;
}
}
4. 文件上传服务实现
4.1 ChatUploadService
该服务负责文件的上传、去重检测、存储记录以及调用文件解析服务将解析结果存入数据库。关键流程如下:
- 文件上传:调用
uploadBytes
方法上传文件至 AWS S3,生成目标文件名并计算 MD5 值; - 去重检测:根据 MD5 值查询数据库,如果文件已存在,则直接返回缓存数据;
- 解析文件:若未重复,则调用
ChatFileService.parseContent
解析文件内容,并将文件基本信息和解析结果存入数据库; - 返回结果:最后返回上传成功后的相关信息(包括文件 id、原始文件名、大小、下载地址和 MD5)。
代码实现如下:
package com.litongjava.llm.service;
import java.util.ArrayList;
import java.util.List;
import com.jfinal.kit.Kv;
import com.jfinal.kit.StrKit;
import com.litongjava.db.TableInput;
import com.litongjava.db.TableResult;
import com.litongjava.db.activerecord.Db;
import com.litongjava.db.activerecord.Row;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.llm.consts.AgentTableNames;
import com.litongjava.model.body.RespBodyVo;
import com.litongjava.table.services.ApiTable;
import com.litongjava.tio.boot.admin.costants.TioBootAdminTableNames;
import com.litongjava.tio.boot.admin.dao.SystemUploadFileDao;
import com.litongjava.tio.boot.admin.services.StorageService;
import com.litongjava.tio.boot.admin.services.SystemUploadFileService;
import com.litongjava.tio.boot.admin.utils.AwsS3Utils;
import com.litongjava.tio.boot.admin.vo.UploadResultVo;
import com.litongjava.tio.http.common.UploadFile;
import com.litongjava.tio.utils.crypto.Md5Utils;
import com.litongjava.tio.utils.hutool.FilenameUtils;
import com.litongjava.tio.utils.snowflake.SnowflakeIdUtils;
import lombok.extern.slf4j.Slf4j;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
@Slf4j
public class ChatUploadService implements StorageService {
ChatFileService chatFileService = Aop.get(ChatFileService.class);
public RespBodyVo upload(String category, UploadFile uploadFile) {
if (StrKit.isBlank(category)) {
category = "default";
}
UploadResultVo uploadResultVo = uploadBytes(category, uploadFile);
Long id = uploadResultVo.getId();
if (!Db.exists(AgentTableNames.chat_upload_file, "id", id)) {
try {
String content = chatFileService.parseContent(uploadFile);
if (content == null) {
return RespBodyVo.fail("un support file type");
} else {
Row row = Row.by("id", id).set("name", uploadFile.getName()).set("content", content).set("md5", uploadResultVo.getMd5());
Db.save(AgentTableNames.chat_upload_file, row);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
return RespBodyVo.fail(e.getMessage());
}
}
return RespBodyVo.ok(uploadResultVo);
}
public UploadResultVo uploadBytes(String category, UploadFile uploadFile) {
// 上传文件
long id = SnowflakeIdUtils.id();
String suffix = FilenameUtils.getSuffix(uploadFile.getName());
String newFilename = id + "." + suffix;
String targetName = category + "/" + newFilename;
return uploadBytes(id, targetName, uploadFile, suffix);
}
/**
* @param id
* @param originFilename
* @param targetName
* @param fileContent
* @param size
* @param suffix
* @return
*/
public UploadResultVo uploadBytes(long id, String targetName, UploadFile uploadFile, String suffix) {
String originFilename = uploadFile.getName();
long size = uploadFile.getSize();
byte[] fileContent = uploadFile.getData();
String md5 = Md5Utils.digestHex(fileContent);
Row record = Aop.get(SystemUploadFileDao.class).getFileBasicInfoByMd5(md5);
if (record != null) {
log.info("select table reuslt:{}", record.toMap());
id = record.getLong("id");
String url = this.getUrl(record.getStr("bucket_name"), record.getStr("target_name"));
Kv kv = record.toKv();
kv.remove("target_name");
kv.remove("bucket_name");
kv.set("url", url);
kv.set("md5", md5);
return new UploadResultVo(id, uploadFile.getName(), uploadFile.getSize(), url, md5);
} else {
log.info("not found from cache table:{}", md5);
}
String etag = null;
try (S3Client client = AwsS3Utils.buildClient();) {
PutObjectResponse response = AwsS3Utils.upload(client, AwsS3Utils.bucketName, targetName, fileContent, suffix);
etag = response.eTag();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
// Log and save to database
log.info("Uploaded with ETag: {}", etag);
TableInput kv = TableInput.create().set("name", originFilename).set("size", size).set("md5", md5)
//
.set("platform", "aws s3").set("region_name", AwsS3Utils.regionName).set("bucket_name", AwsS3Utils.bucketName)
//
.set("target_name", targetName).set("file_id", etag);
TableResult<Kv> save = ApiTable.save(TioBootAdminTableNames.tio_boot_admin_system_upload_file, kv);
String downloadUrl = getUrl(AwsS3Utils.bucketName, targetName);
return new UploadResultVo(save.getData().getLong("id"), originFilename, Long.valueOf(size), downloadUrl, md5);
}
@Override
public String getUrl(String bucketName, String targetName) {
return Aop.get(SystemUploadFileService.class).getUrl(bucketName, targetName);
}
@Override
public UploadResultVo getUrlById(String id) {
return Aop.get(SystemUploadFileService.class).getUrlById(id);
}
@Override
public UploadResultVo getUrlById(long id) {
return Aop.get(SystemUploadFileService.class).getUrlById(id);
}
@Override
public UploadResultVo getUrlByMd5(String md5) {
return Aop.get(SystemUploadFileService.class).getUrlByMd5(md5);
}
public RespBodyVo file(String md5) {
boolean exists = Db.exists(AgentTableNames.chat_upload_file, "md5", md5);
UploadResultVo uploadResultVo = Aop.get(SystemUploadFileService.class).getUrlByMd5(md5);
if (exists && uploadResultVo != null) {
return RespBodyVo.ok(uploadResultVo);
}
return RespBodyVo.fail();
}
public List<UploadResultVo> getFileBasicInfoByIds(List<Long> file_ids) {
List<Row> row = Aop.get(SystemUploadFileDao.class).getFileBasicInfoByIds(file_ids);
List<UploadResultVo> files = new ArrayList<>();
for (Row record : row) {
Long id = record.getLong("id");
String url = this.getUrl(record.getStr("bucket_name"), record.getStr("target_name"));
String originFilename = record.getStr("name");
String md5 = record.getString("md5");
Long size = record.getLong("size");
UploadResultVo uploadResultVo = new UploadResultVo(id, originFilename, size, url, md5);
Row contentRow = Db.findColumnsById(AgentTableNames.chat_upload_file, "content", id);
if (row != null) {
String content = contentRow.getStr("content");
uploadResultVo.setContent(content);
files.add(uploadResultVo);
} else {
log.error("not found content of id:" + id);
}
}
return files;
}
}
4.2 ApiChatUploadHandler
该类用于处理 HTTP 请求,调用 ChatUploadService
完成文件上传和文件去重检测两个 API 接口。代码如下:
package com.litongjava.llm.handler;
import com.litongjava.jfinal.aop.Aop;
import com.litongjava.llm.service.ChatUploadService;
import com.litongjava.model.body.RespBodyVo;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.http.common.UploadFile;
import com.litongjava.tio.http.server.model.HttpCors;
import com.litongjava.tio.http.server.util.CORSUtils;
import com.litongjava.tio.http.server.util.Resps;
public class ApiChatUploadHandler {
ChatUploadService chatUploadService = Aop.get(ChatUploadService.class);
public HttpResponse upload(HttpRequest request) {
HttpResponse httpResponse = TioRequestContext.getResponse();
CORSUtils.enableCORS(httpResponse, new HttpCors());
UploadFile uploadFile = request.getUploadFile("file");
String category = request.getParam("category");
if (uploadFile != null) {
RespBodyVo respBodyVo = chatUploadService.upload(category, uploadFile);
return Resps.json(httpResponse, respBodyVo);
}
return Resps.json(httpResponse, RespBodyVo.ok("Fail"));
}
public HttpResponse file(HttpRequest request) {
HttpResponse httpResponse = TioRequestContext.getResponse();
CORSUtils.enableCORS(httpResponse, new HttpCors());
String md5 = request.getParam("md5");
RespBodyVo vo = chatUploadService.file(md5);
return Resps.json(httpResponse, vo);
}
}
5. 依赖说明
为了保证各个依赖版本一致,需要在项目的 pom.xml
文件中增加如下依赖:
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.24</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>4.0.3</version>
</dependency>
总结
本文档详细介绍了文件上传与解析的整体设计与实现流程。通过前端计算文件 MD5 值进行去重检测,上传文件到 AWS S3,再根据文件类型采用相应的解析工具(如 PDFBox、POI 等)提取文件内容,最终将解析结果存入数据库。后续系统可以利用解析后的文本内容作为大模型问答系统的上下文信息,实现更加精准和高效的文问答功能。