simple_object_parser.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. from minio import Minio
  2. from dotenv import load_dotenv
  3. import os, sys
  4. import hashlib
  5. import argparse
  6. from typing import List, Dict, Any, Optional
  7. from langchain_text_splitters import RecursiveCharacterTextSplitter
  8. sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  9. from parser.document_parser import DocumentParser
  10. from parser.office_parser import OfficeParser
  11. from parser.pdf_parser import PDFParser
  12. from parser.markdown_parser import MarkdownParser
  13. from parser.text_parser import TextParser
  14. from parser.none_parser import NoneParser
  15. from logger.logging import setup_logging
  16. load_dotenv()
  17. logger = setup_logging()
  18. class SimpleObjectParser:
  19. """简单对象解析器"""
  20. def __init__(self,minio_client: Minio, chunk_size: int = 2048, overlap_size: int = 128):
  21. """
  22. 初始化解析器
  23. Args:
  24. chunk_size (int): 文本片段最大长度,默认2048
  25. overlap_size (int): 重叠区域大小,默认128
  26. """
  27. self.chunk_size = chunk_size
  28. self.overlap_size = overlap_size
  29. # 初始化Minio客户端
  30. self.minio_client = minio_client
  31. # 注册解析器(按优先级排序)
  32. self.parsers = [
  33. PDFParser(), # PyPDF2作为PDF解析器
  34. OfficeParser(), # markitdown解析Office文档(Word、Excel、PowerPoint)
  35. #MinerUParser(), # MinerU解析器,专门处理PDF,暂不开启
  36. MarkdownParser(), # Markdown解析器,
  37. TextParser(), # 纯文本解析器
  38. NoneParser(), # 空解析器, 作为最后的备选
  39. ]
  40. def add_parser(self, parser: DocumentParser):
  41. """
  42. 添加新的解析器
  43. Args:
  44. parser (DocumentParser): 文档解析器实例
  45. """
  46. self.parsers.append(parser)
  47. def _get_parser(self, content_type: str) -> Optional[DocumentParser]:
  48. """
  49. 根据内容类型获取合适的解析器
  50. Args:
  51. content_type (str): 内容类型
  52. Returns:
  53. Optional[DocumentParser]: 解析器实例,如果没有找到则返回None
  54. """
  55. for parser in self.parsers:
  56. if parser.supports(content_type):
  57. return parser
  58. return None
  59. def _extract_doc_name(self, object_path: str) -> str:
  60. """
  61. 从对象路径中提取文档名称
  62. Args:
  63. object_path (str): 对象路径
  64. Returns:
  65. str: 文档名称
  66. """
  67. return os.path.basename(object_path)
  68. def _extract_doc_type(self, content_type: str) -> str:
  69. """
  70. 从content-type中提取文档类型
  71. Args:
  72. content_type (str): 内容类型
  73. Returns:
  74. str: 文档类型
  75. """
  76. if not content_type:
  77. return "unknown"
  78. # 提取主要类型
  79. main_type = content_type.split('/')[0].lower()
  80. sub_type = content_type.split('/')[-1].lower()
  81. # 映射常见类型
  82. type_mapping = {
  83. 'application/pdf': 'pdf',
  84. 'text/plain': 'txt',
  85. 'text/html': 'html',
  86. 'text/markdown': 'md',
  87. 'text/x-markdown': 'md',
  88. 'application/markdown': 'md',
  89. # Word文档
  90. 'application/msword': 'doc',
  91. 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
  92. # Excel文档
  93. 'application/vnd.ms-excel': 'xls',
  94. 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
  95. # PowerPoint文档
  96. 'application/vnd.ms-powerpoint': 'ppt',
  97. 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',
  98. }
  99. return type_mapping.get(content_type.lower(), sub_type)
  100. def _calculate_md5(self, data: bytes) -> str:
  101. """
  102. 计算数据的MD5值
  103. Args:
  104. data (bytes): 二进制数据
  105. Returns:
  106. str: MD5值
  107. """
  108. return hashlib.md5(data).hexdigest()
  109. def _split_text_by_langchain(self, text: str) -> List[str]:
  110. """
  111. 使用LangChain分割文本
  112. """
  113. text_splitter = RecursiveCharacterTextSplitter(chunk_size=self.chunk_size, chunk_overlap=self.overlap_size)
  114. return text_splitter.split_text(text)
  115. def parse_object(self, bucket_name: str, object_name: str) -> Dict[str, Any]:
  116. """
  117. 解析简单对象
  118. Args:
  119. bucket_name (str): 存储桶名称
  120. object_path (str): 对象路径
  121. Returns:
  122. Dict[str, Any]: 解析结果
  123. """
  124. try:
  125. # 先获取对象信息,检查文件大小
  126. logger.info(f"正在检查对象信息: {bucket_name}/{object_name}")
  127. try:
  128. stat = self.minio_client.stat_object(bucket_name, object_name)
  129. file_size = stat.size
  130. max_size = 512 * 1024 * 1024 # 512MB
  131. logger.info(f"对象大小: {file_size} 字节 ({file_size / 1024 / 1024:.2f} MB)")
  132. if file_size > max_size:
  133. logger.info(f"文件过大 ({file_size / 1024 / 1024:.2f} MB > 512 MB),跳过解析")
  134. return None
  135. except Exception as e:
  136. logger.info(f"获取对象信息失败: {e}")
  137. # 如果无法获取文件信息,继续尝试解析
  138. # 从Minio获取对象
  139. logger.info(f"正在获取对象内容: {bucket_name}/{object_name}")
  140. response = self.minio_client.get_object(bucket_name, object_name)
  141. # 获取对象数据和元数据
  142. data = response.data
  143. headers = response.headers
  144. logger.info(f"对象大小: {len(data)} 字节")
  145. logger.info(f"Content-Type: {headers.get('Content-Type', 'unknown')}")
  146. # 提取基本信息
  147. doc_name = self._extract_doc_name(object_name)
  148. doc_path_name = object_name # 不再包含bucket_name前缀
  149. content_type = headers.get('Content-Type', '')
  150. doc_type = self._extract_doc_type(content_type)
  151. doc_md5 = headers.get('ETag', '').strip('"')
  152. if len(doc_md5) != 32: # 如果ETag不是32位,则重新计算MD5, 多部分上传:ETag = {复合MD5}-{部分数量}(超过32字符)
  153. doc_md5 = self._calculate_md5(data)
  154. #doc_md5 = self._calculate_md5(data)
  155. doc_length = int(headers.get('Content-Length', len(data)))
  156. # 选择合适的解析器
  157. parser = self._get_parser(content_type)
  158. if not parser:
  159. logger.info(f"警告: 未找到适合 {content_type} 的解析器,使用空解析器兜底")
  160. parser = NoneParser()
  161. # 解析文档内容
  162. logger.info(f"使用解析器: {parser.__class__.__name__}")
  163. text_content = parser.parse(data)
  164. if not text_content:
  165. logger.info("警告: 未提取到文本内容")
  166. contents = []
  167. else:
  168. logger.info(f"提取到文本: {len(text_content)} 字符")
  169. # 分割文本为片段
  170. contents = self._split_text_by_langchain(text_content)
  171. logger.info(f"分割为 {len(contents)} 个片段")
  172. return {
  173. "doc_name": doc_name,
  174. "doc_path_name": doc_path_name,
  175. "doc_type": doc_type,
  176. "doc_md5": doc_md5,
  177. "doc_length": doc_length,
  178. "contents": contents
  179. }
  180. except Exception as e:
  181. logger.info(f"解析对象失败: {e}")
  182. return None
  183. finally:
  184. # 确保响应被正确关闭
  185. if 'response' in locals():
  186. response.close()
  187. response.release_conn()
  188. def get_parser_info(self) -> List[str]:
  189. """
  190. 获取已注册的解析器信息
  191. Returns:
  192. List[str]: 解析器类名列表
  193. """
  194. return [parser.__class__.__name__ for parser in self.parsers]
  195. # 使用示例和测试
  196. if __name__ == "__main__":
  197. parser = argparse.ArgumentParser(description='简单对象解析器 - 支持多种文档格式解析')
  198. # 基本参数
  199. parser.add_argument('--bucket', type=str, default='test', help='Minio桶名称 (默认: test)')
  200. parser.add_argument('--file', type=str, action='append', help='要解析的文件路径 (可指定多个文件)')
  201. parser.add_argument('--test', action='store_true', help='运行默认测试案例')
  202. # 解析器配置
  203. parser.add_argument('--chunk-size', type=int, default=2048, help='文本片段最大长度 (默认: 2048)')
  204. parser.add_argument('--overlap-size', type=int, default=128, help='重叠区域大小 (默认: 128)')
  205. # 输出选项
  206. parser.add_argument('--verbose', action='store_true', help='显示详细信息')
  207. args = parser.parse_args()
  208. print("=== 简单对象解析器 ===")
  209. # 创建解析器实例
  210. object_parser = SimpleObjectParser(
  211. chunk_size=args.chunk_size,
  212. overlap_size=args.overlap_size,
  213. )
  214. # 显示已注册的解析器
  215. print(f"已注册的解析器: {object_parser.get_parser_info()}")
  216. results = []
  217. if args.test:
  218. # 运行默认测试案例
  219. print("\n运行默认测试案例...")
  220. test_files = [
  221. ("test", "pdf1.pdf"),
  222. ("test", "demo/不屈的精神.docx"),
  223. ("test", "demo/demo2/中华人民共和国刑法.txt")
  224. ]
  225. for bucket_name, object_path in test_files:
  226. print(f"\n=== 解析文档: {bucket_name}/{object_path} ===")
  227. result = object_parser.parse_object(bucket_name, object_path)
  228. results.append(result)
  229. # 显示解析结果
  230. print(f"文档名称: {result['doc_name']}")
  231. print(f"文档路径: {result['doc_path_name']}")
  232. print(f"文档类型: {result['doc_type']}")
  233. print(f"文档MD5: {result['doc_md5']}")
  234. print(f"文档大小: {result['doc_length']} 字节")
  235. print(f"文本片段数量: {len(result['contents'])}")
  236. if 'error' in result:
  237. print(f"错误信息: {result['error']}")
  238. elif result['contents']:
  239. # 显示所有片段的信息
  240. for content in result['contents']:
  241. print(f"*************片段 (长度: {len(content)} 字符)")
  242. print(f" {content}")
  243. print("-" * 100)
  244. elif args.file:
  245. # 解析指定的文件
  246. for file_path in args.file:
  247. print(f"\n=== 解析文档: {args.bucket}/{file_path} ===")
  248. result = object_parser.parse_object(args.bucket, file_path)
  249. results.append(result)
  250. # 显示解析结果
  251. print(f"文档名称: {result['doc_name']}")
  252. print(f"文档路径: {result['doc_path_name']}")
  253. print(f"文档类型: {result['doc_type']}")
  254. print(f"文档MD5: {result['doc_md5']}")
  255. print(f"文档大小: {result['doc_length']} 字节")
  256. print(f"文本片段数量: {len(result['contents'])}")
  257. if 'error' in result:
  258. print(f"错误信息: {result['error']}")
  259. elif result['contents']:
  260. # 显示第一个片段的信息
  261. for content in result['contents']:
  262. print(f"*************片段 (长度: {len(content)} 字符)")
  263. print(f" {content}")
  264. print("-" * 100)
  265. else:
  266. # 显示使用说明
  267. parser.print_help()
  268. print("\n使用示例:")
  269. print(" python simple_object_parser.py --test")
  270. print(" python simple_object_parser.py --bucket test --file test/demo/GoogleIO.pdf")
  271. print(" python simple_object_parser.py --bucket test --file test/demo/不屈的精神.docx")
  272. print(f"\n=== 处理完成 ===")