with one click
data-pipeline
文档摄入管道——多格式解析(PDF/MD/HTML)、递归分块、去重、批量摄入 CLI,完整可运行代码
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
文档摄入管道——多格式解析(PDF/MD/HTML)、递归分块、去重、批量摄入 CLI,完整可运行代码
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
Execute the check workflow from this AI development team preset. Use when the user writes /check, asks for check, or wants the corresponding team process in Codex.
Execute the dev workflow from this AI development team preset. Use when the user writes /dev, asks for dev, or wants the corresponding team process in Codex.
Execute the fix workflow from this AI development team preset. Use when the user writes /fix, asks for fix, or wants the corresponding team process in Codex.
Execute the plan workflow from this AI development team preset. Use when the user writes /plan, asks for plan, or wants the corresponding team process in Codex.
Execute the project-preset workflow from this AI development team preset. Use when the user writes /project-preset, asks for project-preset, or wants the corresponding team process in Codex.
Execute the review-all workflow from this AI development team preset. Use when the user writes /review-all, asks for review-all, or wants the corresponding team process in Codex.
| name | data-pipeline |
| description | 文档摄入管道——多格式解析(PDF/MD/HTML)、递归分块、去重、批量摄入 CLI,完整可运行代码 |
文档解析、清洗、分块的完整实现。
# ingestion/parsers.py
from pathlib import Path
from abc import ABC, abstractmethod
class BaseParser(ABC):
@abstractmethod
async def parse(self, path: Path) -> tuple[str, dict]:
"""Returns (text, metadata)"""
...
class PDFParser(BaseParser):
async def parse(self, path: Path) -> tuple[str, dict]:
import pypdf
reader = pypdf.PdfReader(path)
pages = []
for i, page in enumerate(reader.pages):
text = page.extract_text()
if text.strip():
pages.append(f"[第{i+1}页]\n{text}")
return "\n\n".join(pages), {"pages": len(reader.pages), "source": str(path)}
class MarkdownParser(BaseParser):
async def parse(self, path: Path) -> tuple[str, dict]:
text = path.read_text(encoding="utf-8")
title = next((l.lstrip("# ") for l in text.splitlines() if l.startswith("#")), path.stem)
return text, {"title": title, "source": str(path)}
class DocumentParser:
_parsers = {".pdf": PDFParser, ".md": MarkdownParser, ".txt": MarkdownParser}
@classmethod
def for_file(cls, path: Path) -> BaseParser:
parser_cls = cls._parsers.get(path.suffix.lower())
if not parser_cls:
raise ValueError(f"Unsupported file type: {path.suffix}")
return parser_cls()
# ingestion/chunkers.py
from dataclasses import dataclass
@dataclass
class Chunk:
content: str
metadata: dict
content_hash: str = ""
def __post_init__(self):
import hashlib
self.content_hash = hashlib.md5(self.content.encode()).hexdigest()
class RecursiveChunker:
"""按段落/句子递归分块,保留语义完整性"""
SEPARATORS = ["\n\n", "\n", "。", ".", " ", ""]
def __init__(self, chunk_size: int = 512, overlap: int = 50) -> None:
self.chunk_size = chunk_size
self.overlap = overlap
def split(self, text: str, metadata: dict) -> list[Chunk]:
raw_chunks = self._recursive_split(text, self.SEPARATORS)
chunks = []
for i, content in enumerate(raw_chunks):
if content.strip():
chunks.append(Chunk(
content=content.strip(),
metadata={**metadata, "chunk_index": i, "total_chunks": len(raw_chunks)},
))
return chunks
def _recursive_split(self, text: str, separators: list[str]) -> list[str]:
if len(text) <= self.chunk_size or not separators:
return [text]
sep = separators[0]
parts = text.split(sep) if sep else list(text)
chunks = []
current = ""
for part in parts:
if len(current) + len(part) + len(sep) <= self.chunk_size:
current += part + sep
else:
if current:
chunks.append(current)
current = current[-self.overlap:] + part + sep # overlap
else:
# part itself is too large, recurse
chunks.extend(self._recursive_split(part, separators[1:]))
if current.strip():
chunks.append(current)
return chunks
# scripts/ingest.py
import asyncio
from pathlib import Path
import click
@click.command()
@click.argument("directory")
@click.option("--pattern", default="**/*", help="文件 glob 模式")
async def ingest(directory: str, pattern: str):
root = Path(directory)
files = [f for f in root.glob(pattern) if f.suffix in (".pdf", ".md", ".txt")]
click.echo(f"找到 {len(files)} 个文件")
store = PgVectorStore(settings.database_url)
total_chunks = 0
for file in files:
try:
n = await ingest_document(file, store)
total_chunks += n
click.echo(f" ✓ {file.name} → {n} chunks")
except Exception as e:
click.echo(f" ✗ {file.name}: {e}", err=True)
click.echo(f"\n完成,共摄入 {total_chunks} 个分块")
if __name__ == "__main__":
asyncio.run(ingest())