simple_object_parser.py 12 KB

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