File size: 6,829 Bytes
5316124 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | """
TextChunker — 文本文档分块。
使用 LangChain 的 RecursiveCharacterTextSplitter 进行语义边界感知的分块。
"""
import re
import uuid
from typing import Dict, List, Optional
class TextChunker:
"""文本文档分块器。
使用递归字符分割, 优先在段落/句子边界处分块,
保持语义完整性的同时控制块大小。
Usage:
chunker = TextChunker(chunk_size=512, chunk_overlap=64)
chunks = chunker.chunk("长文本内容...", metadata={"source": "doc.txt"})
"""
def __init__(
self,
chunk_size: int = 512,
chunk_overlap: int = 64,
separators: Optional[List[str]] = None,
):
self.chunk_size = chunk_size
self.chunk_overlap = chunk_overlap
self.separators = separators or [
"\n\n",
"\n",
"。",
". ",
"? ",
"! ",
";",
"; ",
",",
", ",
" ",
"",
]
def chunk(
self,
text: str,
metadata: Optional[dict] = None,
source_file: Optional[str] = None,
) -> List[dict]:
"""将文本分割为块。
Args:
text: 输入文本。
metadata: 附加元数据 (合并到每个 chunk)。
source_file: 来源文件路径。
Returns:
chunk 字典列表, 每个包含:
- chunk_id: 唯一 ID
- text: 块文本
- metadata: 元数据 (含 source_file, chunk_index 等)
- modality: "text"
"""
try:
from langchain_text_splitters import RecursiveCharacterTextSplitter
except ImportError:
raise ImportError(
"需要 langchain-text-splitters。请安装: pip install langchain-text-splitters"
)
splitter = RecursiveCharacterTextSplitter(
chunk_size=self.chunk_size,
chunk_overlap=self.chunk_overlap,
separators=self.separators,
length_function=len,
is_separator_regex=False,
)
docs = splitter.create_documents(
texts=[text],
metadatas=[metadata or {}],
)
chunks = []
for i, doc in enumerate(docs):
chunks.append({
"chunk_id": str(uuid.uuid4()),
"text": doc.page_content.strip(),
"metadata": {
"source_file": source_file or metadata.get("source", "unknown"),
"chunk_index": i,
"chunk_count": len(docs),
"modality": "text",
"content_type": "text_chunk",
**(metadata or {}),
},
"modality": "text",
})
return chunks
def chunk_with_token_limit(
self,
text: str,
max_tokens: int = 512,
metadata: Optional[dict] = None,
source_file: Optional[str] = None,
) -> List[dict]:
"""使用 token 计数的分块 (需要 tiktoken)。
Args:
text: 输入文本。
max_tokens: 每块最大 token 数。
metadata: 附加元数据。
source_file: 来源文件路径。
Returns:
chunk 字典列表。
"""
try:
import tiktoken
except ImportError:
raise ImportError("需要 tiktoken。请安装: pip install tiktoken")
try:
enc = tiktoken.get_encoding("cl100k_base")
except Exception:
enc = tiktoken.get_encoding("o200k_base")
sentences = self._split_sentences(text)
chunks = []
current_chunk: List[str] = []
current_tokens = 0
for sentence in sentences:
sent_tokens = len(enc.encode(sentence))
if current_tokens + sent_tokens > max_tokens and current_chunk:
chunk_text = "".join(current_chunk)
chunks.append({
"chunk_id": str(uuid.uuid4()),
"text": chunk_text.strip(),
"metadata": {
"source_file": source_file or (metadata or {}).get("source", "unknown"),
"chunk_index": len(chunks),
"modality": "text",
"content_type": "text_chunk",
"token_count": current_tokens,
**(metadata or {}),
},
"modality": "text",
})
current_chunk = []
current_tokens = 0
# 重叠: 保留最后一句
if self.chunk_overlap > 0:
overlap_sentences = self._estimate_overlap_sentences(
chunk_text, enc, self.chunk_overlap
)
current_chunk = overlap_sentences
current_tokens = sum(len(enc.encode(s)) for s in current_chunk)
current_chunk.append(sentence)
current_tokens += sent_tokens
# 最后一块
if current_chunk:
chunk_text = "".join(current_chunk)
chunks.append({
"chunk_id": str(uuid.uuid4()),
"text": chunk_text.strip(),
"metadata": {
"source_file": source_file or (metadata or {}).get("source", "unknown"),
"chunk_index": len(chunks),
"modality": "text",
"content_type": "text_chunk",
"token_count": current_tokens,
**(metadata or {}),
},
"modality": "text",
})
# 更新 chunk_count
for c in chunks:
c["metadata"]["chunk_count"] = len(chunks)
return chunks
@staticmethod
def _split_sentences(text: str) -> List[str]:
"""简单的句子分割, 兼顾中英文。"""
# 按中英文标点分割但保留标点
pattern = r"(?<=[.!?。!?;;])\s*"
sentences = re.split(pattern, text)
return [s for s in sentences if s.strip()]
@staticmethod
def _estimate_overlap_sentences(
chunk_text: str, enc, target_overlap_tokens: int
) -> List[str]:
"""估计重叠所需的句子数。"""
sentences = TextChunker._split_sentences(chunk_text)
overlap = []
tokens = 0
for s in reversed(sentences):
s_tokens = len(enc.encode(s))
if tokens + s_tokens > target_overlap_tokens and overlap:
break
overlap.insert(0, s)
tokens += s_tokens
return overlap
|