本可直接替换原有的文档加载逻辑,无缝集成到之前的RAG系统中
一、核心原理
不同格式的文档本质都是"文本内容+格式标记",我们要做的就是:
-
剥离格式,提取纯文本:用PyPDF2解析PDF、python-docx解析Word、openpyxl解析Excel,去掉所有格式标记(字体、颜色、表格样式等),只保留可读的纯文本
-
统一格式入库:不管是PDF/Word/Excel,提取出的纯文本都按照"文件名+文本内容"的结构,批量导入到Elasticsearch知识库,后续的语义检索、大模型问答逻辑完全不变
-
离线适配:所有解析库都提前下载离线whl包,无外网也能安装,无任何外部依赖
二、第一步:离线准备多格式解析依赖包
2.1 有网环境下载离线whl包
Bash
# 创建whl包存放目录
mkdir -p /opt/offline-packages/whl-doc/
# 下载所有多格式解析依赖包(指定版本,避免兼容性问题)
pip download PyPDF2==3.0.1 python-docx==1.1.0 openpyxl==3.1.2 -d /opt/offline-packages/whl-doc/
下载完成后,将/opt/offline-packages/whl-doc/目录下的所有whl文件,拷贝到离线环境的/opt/offline-packages/whl-doc/目录。
2.2 离线环境安装依赖包
Bash
# 进入whl包目录
cd /opt/offline-packages/whl-doc/
# 离线安装所有依赖包
pip install --no-index --find-links=./ PyPDF2 python-docx openpyxl
2.3 验证安装成功
执行以下命令,无报错则说明安装成功:
Bash
python3 -c "import PyPDF2, docx, openpyxl; print('所有依赖安装成功')"
三、第二步:多格式文档解析核心函数
3.1 核心解析函数
将以下函数添加到原有rag_offline.py脚本中,替换原来的load_documents_from_folder函数:
Python
import PyPDF2
from docx import Document
import openpyxl
def extract_text_from_pdf(file_path):
"""离线解析PDF文件,提取纯文本"""
try:
with open(file_path, 'rb') as f:
pdf_reader = PyPDF2.PdfReader(f)
text = ""
# 遍历所有页面,提取文本
for page in pdf_reader.pages:
page_text = page.extract_text()
if page_text:
text += page_text + "\n"
return text.strip()
except Exception as e:
print(f"⚠️ PDF文件 {file_path} 解析失败:{str(e)}")
return ""
def extract_text_from_docx(file_path):
"""离线解析Word文件(.docx),提取纯文本"""
try:
doc = Document(file_path)
text = ""
# 提取段落文本
for para in doc.paragraphs:
if para.text.strip():
text += para.text + "\n"
# 提取表格文本(关键优化:Excel/Word表格转成可读文本)
for table in doc.tables:
for row in table.rows:
row_text = "\t".join([cell.text.strip() for cell in row.cells])
text += row_text + "\n"
return text.strip()
except Exception as e:
print(f"⚠️ Word文件 {file_path} 解析失败:{str(e)}")
return ""
def extract_text_from_excel(file_path):
"""离线解析Excel文件(.xlsx),提取纯文本"""
try:
wb = openpyxl.load_workbook(file_path, data_only=True) # data_only=True读取单元格计算后的值
text = ""
# 遍历所有工作表
for sheet_name in wb.sheetnames:
sheet = wb[sheet_name]
text += f"工作表名称:{sheet_name}\n"
# 遍历所有行
for row in sheet.iter_rows(values_only=True):
# 过滤空单元格,拼接行文本
row_text = "\t".join([str(cell) if cell is not None else "" for cell in row])
if row_text.strip():
text += row_text + "\n"
return text.strip()
except Exception as e:
print(f"⚠️ Excel文件 {file_path} 解析失败:{str(e)}")
return ""
def extract_text_from_txt(file_path):
"""离线解析TXT文件,兼容中文编码"""
try:
# 优先UTF-8,失败则用GBK(兼容中文文档)
with open(file_path, 'r', encoding='utf-8') as f:
return f.read().strip()
except UnicodeDecodeError:
with open(file_path, 'r', encoding='gbk') as f:
return f.read().strip()
except Exception as e:
print(f"⚠️ TXT文件 {file_path} 解析失败:{str(e)}")
return ""
def load_documents_from_folder():
"""【多格式适配版】加载所有支持格式的文档,生成ES批量写入数据"""
# 支持的文件格式
SUPPORTED_FORMATS = {
'.pdf': extract_text_from_pdf,
'.docx': extract_text_from_docx,
'.xlsx': extract_text_from_excel,
'.txt': extract_text_from_txt
}
if not os.path.exists(DATASET_FOLDER):
print(f"❌ 知识库目录 {DATASET_FOLDER} 不存在,请先创建目录并放入文档")
exit(1)
file_count = 0
success_count = 0
# 遍历目录下所有文件
for filename in os.listdir(DATASET_FOLDER):
# 获取文件后缀
file_ext = os.path.splitext(filename)[1].lower()
# 跳过不支持的格式
if file_ext not in SUPPORTED_FORMATS:
print(f"⚠️ 跳过不支持的文件格式:{filename}")
continue
file_path = os.path.join(DATASET_FOLDER, filename)
# 根据格式调用对应的解析函数
extract_func = SUPPORTED_FORMATS[file_ext]
file_content = extract_func(file_path)
# 跳过解析失败/空内容的文件
if not file_content:
print(f"⚠️ 文件 {filename} 解析后无内容,跳过导入")
continue
# 生成ES批量写入数据
yield {
"_index": INDEX_NAME,
"_source": {
"file_name": filename,
"file_content": file_content,
"file_format": file_ext[1:] # 新增字段:记录文件格式
}
}
file_count += 1
success_count += 1
if file_count == 0:
print(f"❌ 知识库目录 {DATASET_FOLDER} 中没有找到支持的文档(PDF/Word/Excel/TXT)")
exit(1)
print(f"📄 共找到 {file_count} 个支持格式的文档,成功解析 {success_count} 个,准备导入ES")
3.2 关键优化点说明
-
编码兼容:TXT文件优先用UTF-8解析,失败则自动切换GBK,解决中文乱码问题
-
表格解析:Word/Excel表格转成"单元格内容\t单元格内容"的格式,保留表格的行列逻辑,大模型能识别表格内容
-
容错处理:单个文件解析失败不会中断整体流程,只会跳过该文件并打印警告
-
格式记录 :新增
file_format字段,记录文件原始格式(pdf/docx/xlsx/txt),便于后续检索过滤
四、第三步:更新ES索引映射(适配新增字段)
修改原有脚本中的setup_knowledge_index函数,在mappings.properties中新增file_format字段,确保ES能正确存储文件格式信息:
Python
def setup_knowledge_index():
"""创建知识库索引,适配多格式文档"""
try:
if es_client.indices.exists(index=INDEX_NAME):
print(f"✅ 知识库索引 {INDEX_NAME} 已存在,无需重复创建")
return False
print(f"📦 正在创建知识库索引 {INDEX_NAME}...")
es_client.indices.create(
index=INDEX_NAME,
body={
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0,
"refresh_interval": "1s"
},
"mappings": {
"properties": {
"file_name": {
"type": "text",
"copy_to": "semantic_content"
},
"file_content": {
"type": "text",
"copy_to": "semantic_content"
},
"file_format": { # 新增:存储文件格式
"type": "keyword" # keyword类型,支持精确过滤
},
"semantic_content": {
"type": "semantic_text",
"inference_id": "offline-e5-small-embedding"
}
}
}
}
)
print(f"✅ 知识库索引 {INDEX_NAME} 创建成功")
return True
except Exception as e:
print(f"❌ 知识库索引创建失败,错误信息:{str(e)}")
exit(1)
五、第四步:扩展语义检索(支持按文件格式过滤)
新增一个可选的检索过滤函数,支持只检索指定格式的文档(比如只查PDF、只查Excel),按需使用:
Python
def semantic_search_with_filter(user_query, top_n=3, file_format=None):
"""语义检索,支持按文件格式过滤(可选)"""
start_time = time.time()
# 基础语义检索查询
search_query = {
"query": {
"bool": {
"must": [
{
"semantic": {
"field": "semantic_content",
"query": user_query
}
}
]
}
},
"size": top_n,
"_source": ["file_name", "file_content", "file_format"]
}
# 如果指定了文件格式,添加过滤条件
if file_format:
search_query["query"]["bool"]["filter"] = [
{"term": {"file_format": file_format.lower()}}
]
response = es_client.search(index=INDEX_NAME, body=search_query)
search_latency = (time.time() - start_time) * 1000
return response["hits"]["hits"], search_latency
使用示例(只检索PDF文档)
在脚本的主函数中,将原来的semantic_search调用替换为:
Python
# 只检索PDF格式的文档
search_results, search_latency = semantic_search_with_filter(USER_QUESTION, top_n=3, file_format="pdf")
六、完整测试流程(离线环境)
6.1 准备测试文档
在/opt/rag-offline/dataset目录放入:
-
测试PDF文档(比如《API性能报告.pdf》)
-
测试Word文档(比如《会议纪要.docx》)
-
测试Excel文档(比如《项目进度表.xlsx》)
-
测试TXT文档(比如《需求说明.txt》)
6.2 运行脚本
Bash
cd /opt/rag-offline/
python3 rag_offline.py
6.3 预期输出
Plain
📄 共找到 4 个支持格式的文档,成功解析 4 个,准备导入ES
✅ 成功导入 4 个文档到知识库
🔍 用户问题:请总结API存在的性能问题
✅ 找到 3 个相关文档,检索耗时:12ms
🤖 正在调用本地大模型 dolphin3.0-qwen2.5-0.5b-instruct.Q4_K_M.gguf 生成答案...
============================================================
💡 问题:请总结API存在的性能问题
📝 答案:
1. API在并发量超过1000次/分钟时,响应时间从200ms飙升至3秒(来源:[1] API性能报告.pdf)
2. 复杂查询无缓存层,导致重复计算,CPU占用率达到100%(来源:[2] 会议纪要.docx)
3. 数据库索引未优化,多条件过滤查询耗时超过2秒(来源:[3] 项目进度表.xlsx)
📚 引用来源:
[1] API性能报告.pdf
[2] 会议纪要.docx
[3] 项目进度表.xlsx
============================================================
🔍 语义检索耗时:12ms
🤖 大模型生成耗时:15800ms | 生成速度:9.6 tokens/s
✅ 离线RAG问答执行完成
七、离线环境常见问题与解决方案
| 问题现象 | 根因分析 | 解决方案 |
|---|---|---|
| PDF解析乱码/无内容 | PDF是扫描件(图片),或加密PDF | 1. 扫描件需先OCR转文本(离线OCR工具如Tesseract);2. 加密PDF需先解密 |
| Word解析丢失表格内容 | 表格嵌套层级过深,或使用了旧版.doc格式 | 1. 转换为.docx格式;2. 脚本已适配基础表格解析,复杂表格需手动调整解析逻辑 |
| Excel解析数字为None | 单元格是公式,未勾选data_only=True | 脚本中已设置data_only=True,会读取公式计算后的值,确保Excel保存时已计算所有公式 |
| 中文文档解析乱码 | 文档编码非UTF-8/GBK | TXT文件用记事本另存为UTF-8;PDF/Word/Excel无需处理,脚本已适配 |
| 大文件解析内存溢出 | 单文件超过100MB,内存不足 | 新增分块解析逻辑,将大文件拆分成多个小文本块导入ES |
八、进阶优化(可选)
8.1 大文件分块解析(解决内存溢出)
修改load_documents_from_folder函数,对超过指定大小的文件进行分块:
Python
def split_large_text(text, max_chunk_size=5000):
"""将大文本分块,避免单条记录过大"""
chunks = []
start = 0
while start < len(text):
end = start + max_chunk_size
# 按句子分割,避免截断语义
if end < len(text):
end = text.rfind('\n', start, end) + 1
if end <= start:
end = start + max_chunk_size
chunks.append(text[start:end].strip())
start = end
return chunks
# 在load_documents_from_folder函数中,生成ES数据时添加分块逻辑
file_content = extract_func(file_path)
# 大文件分块
if len(file_content) > 5000:
chunks = split_large_text(file_content)
for i, chunk in enumerate(chunks):
if chunk:
yield {
"_index": INDEX_NAME,
"_source": {
"file_name": f"{filename}_chunk{i+1}",
"file_content": chunk,
"file_format": file_ext[1:],
"original_file": filename # 记录原始文件名
}
}
success_count += 1
else:
# 小文件直接导入
yield {
"_index": INDEX_NAME,
"_source": {
"file_name": filename,
"file_content": file_content,
"file_format": file_ext[1:]
}
}
success_count += 1
8.2 离线OCR支持(解析扫描件PDF)
-
有网环境下载Tesseract离线安装包和中文语言包
-
离线安装Tesseract,安装pytesseract离线whl包
-
扩展PDF解析函数,扫描件PDF先OCR转文本再导入:
Python
import pytesseract
from PIL import Image
def extract_text_from_scanned_pdf(file_path):
"""离线OCR解析扫描件PDF"""
try:
# 需先安装poppler-utils(离线包),用pdf2image转PDF为图片
from pdf2image import convert_from_path
images = convert_from_path(file_path)
text = ""
for img in images:
text += pytesseract.image_to_string(img, lang='chi_sim') + "\n"
return text.strip()
except Exception as e:
print(f"⚠️ 扫描件PDF {file_path} OCR解析失败:{str(e)}")
return ""
总结
-
核心能力:通过PyPDF2/python-docx/openpyxl实现PDF/Word/Excel/TXT多格式文档离线解析,提取纯文本后统一导入ES,RAG问答逻辑完全复用
-
离线适配:所有依赖包均提供离线安装方式,无外网也能部署
-
容错处理:单个文件解析失败不影响整体流程,支持中文编码、表格解析、大文件分块
-
扩展能力:支持按文件格式过滤检索,可扩展离线OCR解析扫描件PDF