"""数据处理模块使用的无副作用算法。 本模块不访问数据库、文件系统或网络,便于 API、后台任务和测试共同复用。 所有偏移量均为 Python 字符串偏移量,``TextChunk.content`` 始终等于 ``source[chunk.start:chunk.end]``。 """ from __future__ import annotations import csv import hashlib import io import json import math import re import unicodedata import xml.etree.ElementTree as ET import zipfile from bisect import bisect_left from collections import Counter from collections.abc import Iterable, Mapping, Sequence from copy import deepcopy from dataclasses import dataclass from datetime import date, datetime, time from pathlib import Path, PurePosixPath from typing import Any, Literal from urllib.parse import unquote, urlsplit from docx import Document from docx.oxml.table import CT_Tbl from docx.oxml.text.paragraph import CT_P from docx.table import Table from docx.text.paragraph import Paragraph from llama_index.core.node_parser import SentenceSplitter, TokenTextSplitter from openpyxl import load_workbook from openpyxl.utils.cell import range_boundaries from pptx import Presentation from pypdf import PdfReader TextFormat = Literal[ "json", "jsonl", "csv", "markdown", "txt", "pdf", "docx", "xlsx", "pptx", ] ChunkMethod = Literal["structure", "fixed", "custom"] DatasetSplit = Literal["train", "validation", "test"] StructuredPreprocessOption = Literal[ "clean_invalid", "detect_structure", "deduplicate", "normalize_format", "filter_anomaly", "desensitize", ] SUPPORTED_TEXT_FORMATS: tuple[TextFormat, ...] = ( "json", "jsonl", "csv", "markdown", "txt", "pdf", "docx", "xlsx", "pptx", ) _FORMAT_ALIASES: dict[str, TextFormat] = { "json": "json", "jsonl": "jsonl", "ndjson": "jsonl", "csv": "csv", "tsv": "csv", "md": "markdown", "markdown": "markdown", "txt": "txt", "text": "txt", "pdf": "pdf", "docx": "docx", "xlsx": "xlsx", "pptx": "pptx", } _LEGACY_OFFICE_FORMATS: dict[str, str] = { "doc": "docx", "xls": "xlsx", "ppt": "pptx", } _OFFICE_OPEN_XML_FORMATS = {"docx", "xlsx", "pptx"} _MAX_ARCHIVE_ENTRIES = 10_000 _MAX_ARCHIVE_UNCOMPRESSED_BYTES = 512 * 1024 * 1024 _MAX_ARCHIVE_ENTRY_BYTES = 128 * 1024 * 1024 _MAX_ARCHIVE_COMPRESSION_RATIO = 200 _MAX_EXTRACTED_TEXT_CHARS = 20_000_000 _MAX_PDF_PAGES = 2_000 _MAX_PRESENTATION_SLIDES = 2_000 _MAX_WORKBOOK_SHEETS = 100 _MAX_WORKBOOK_ROWS = 100_000 _MAX_WORKBOOK_SCANNED_ROWS = 200_000 _MAX_WORKBOOK_COLUMNS = 256 _MAX_WORKBOOK_CELLS = 2_000_000 _MAX_WORKBOOK_HEADER_ROWS = 8 _MAX_WORKBOOK_HEADER_SCAN_ROWS = 64 _MAX_WORKBOOK_MERGED_RANGES = 100_000 _MAX_STRUCTURED_FIELDS = 1_024 _MAX_STRUCTURED_DEPTH = 16 _MAX_ANOMALY_TEXT_CHARS = 1_000_000 _STRUCTURED_OPTIONS = { "clean_invalid", "detect_structure", "deduplicate", "normalize_format", "filter_anomaly", "desensitize", } _IDENTITY_FIELD_PATTERN = re.compile(r"(?:^|[._])(?:id|uuid|key|code)$|(?:^|[._]).+_id$") _MOJIBAKE_MARKERS = ("\ufffd", "锟斤拷", "烫烫烫", "屯屯屯", "Ã", "Â", "â€") _NAME_FIELD_NAMES = { "name", "full_name", "fullname", "real_name", "contact_name", "customer_name", "recipient_name", "姓名", "真实姓名", "联系人", "联系人姓名", "收件人", } _EMAIL_PATTERN = re.compile( r"(?姓名|真实姓名|联系人(?:姓名)?|收件人)" r"(?P\s*(?:[::=]|为)\s*|\s+)" r"(?P[\u3400-\u4dbf\u4e00-\u9fff·]{2,8})" ) _ENGLISH_NAME_CONTEXT_PATTERN = re.compile( r"(?im)(?P