python 以字符数量分割文档 (三)

针对 RAG / ChromaDB / book-to-skill / Claude Code Skill 工作流,建议给每个分片增加 metadata。

目标输出:

split/
├── 001.md
├── 001.yaml
├── 002.md
├── 002.yaml
├── 003.md
├── 003.yaml
└── index.json

例如:

001.yaml

id: 001
source: linux_book.md
title: Linux基础
section_path:
  - Linux基础
  - 文件系统
level: 2
characters: 87342
created_by: markdown_splitter

这样导入 ChromaDB 时:

collection.add(
    documents=[content],
    metadatas=[metadata]
)

可以直接过滤:

where={
    "title": "Linux基础"
}

主要增加三个能力:

  1. 标题路径追踪

例如:

原文:

# Linux基础

## 文件系统

### inode

内容

分片 metadata:

section_path:
  - Linux基础
  - 文件系统
  - inode

  1. 自动生成 JSON 索引

生成:

index.json

内容:

[
  {
    "id": "001",
    "file": "001.md",
    "title": "Linux基础",
    "characters": 87342
  }
]

方便程序读取。


  1. Markdown Front Matter 注入

每个 md 文件开头增加:

---
id: 001
source: linux_book.md
title: Linux基础
characters: 87342
---

# Linux基础

正文...

这样:

  • 人读 Markdown 正常
  • AI 读取自动获得上下文

完整增强版:

#!/usr/bin/env python3

import sys
import re
import json
from pathlib import Path
from datetime import datetime


INPUT = sys.argv[1] if len(sys.argv) > 1 else "book.md"

OUTPUT = Path("split")

MAX_CHARS = 90000


OUTPUT.mkdir(exist_ok=True)



def is_fence(line):

    return re.match(
        r"^\s*(`{3,}|~{3,})",
        line
    )



def scan_markdown(lines):
    """
    扫描 Markdown:
    返回:
    [
      {
        text:"",
        path:[标题],
        level:1
      }
    ]
    """

    sections=[]

    path=[]

    start=0

    current_level=0

    in_fence=False

    fence=None



    for i,line in enumerate(lines):

        f=is_fence(line)


        if f:

            c=f.group(1)[0]

            if not in_fence:

                in_fence=True
                fence=c

            elif c==fence:

                in_fence=False
                fence=None

            continue



        if not in_fence:

            m=re.match(
                r"^(#{1,3})\s+(.+)",
                line
            )


            if m:

                level=len(m.group(1))

                title=m.group(2).strip()


                if i>start:

                    sections.append(
                        {
                            "text":
                            "".join(lines[start:i]),

                            "path":
                            path.copy(),

                            "level":
                            current_level
                        }
                    )


                path=path[:level-1]

                path.append(title)


                current_level=level

                start=i



    sections.append(
        {
            "text":
            "".join(lines[start:]),

            "path":
            path.copy(),

            "level":
            current_level
        }
    )


    return sections




def hard_split(text):

    result=[]


    while len(text)>MAX_CHARS:

        cut=text.rfind(
            "\n\n",
            0,
            MAX_CHARS
        )


        if cut==-1:

            cut=MAX_CHARS


        result.append(
            text[:cut]
        )


        text=text[cut:]


    if text:

        result.append(text)


    return result





def build_chunks(sections):

    chunks=[]

    current=""

    meta=None



    for sec in sections:


        parts=hard_split(
            sec["text"]
        )


        for part in parts:


            if (
                current
                and
                len(current)+len(part)
                >
                MAX_CHARS
            ):

                chunks.append(
                    {
                        "text":current,
                        "path":meta["path"],
                        "level":meta["level"]
                    }
                )


                current=part

                meta=sec


            else:

                current+=part

                meta=sec



    if current:

        chunks.append(
            {
                "text":current,
                "path":meta["path"],
                "level":meta["level"]
            }
        )


    return chunks




def save_chunks(chunks):

    index=[]


    source=Path(INPUT).name



    for i,c in enumerate(chunks,1):

        sid=f"{i:03d}"


        md_file=OUTPUT/f"{sid}.md"


        chars=len(c["text"])



        metadata={

            "id":sid,

            "source":source,

            "title":
                c["path"][0]
                if c["path"]
                else "",

            "section_path":
                c["path"],

            "level":
                c["level"],

            "characters":
                chars,

            "created":
                datetime.now().isoformat(),

            "generator":
                "markdown_splitter"

        }



        front="\n".join(
            [
            "---",
            f"id: {sid}",
            f"source: {source}",
            f"title: {metadata['title']}",
            f"characters: {chars}",
            "---",
            ""
            ]
        )


        md_file.write_text(
            front+c["text"],
            encoding="utf-8"
        )



        yaml_file=OUTPUT/f"{sid}.json"


        yaml_file.write_text(
            json.dumps(
                metadata,
                ensure_ascii=False,
                indent=2
            ),
            encoding="utf-8"
        )


        index.append(
            metadata
        )


        print(
            md_file,
            chars
        )



    (OUTPUT/"index.json").write_text(

        json.dumps(
            index,
            ensure_ascii=False,
            indent=2
        ),

        encoding="utf-8"
    )




def main():

    lines=Path(INPUT).read_text(
        encoding="utf-8"
    ).splitlines(
        keepends=True
    )


    sections=scan_markdown(lines)


    chunks=build_chunks(sections)


    save_chunks(chunks)




if __name__=="__main__":

    main()

现在输出结构

例如:

输入:

linux.md

运行:

python split_md.py linux.md

生成:

split/

001.md
001.json

002.md
002.json

003.md
003.json

index.json

对 ChromaDB 非常友好

导入:

import json
from pathlib import Path


docs=[]
metas=[]


for f in Path("split").glob("*.md"):

    docs.append(
        f.read_text()
    )


    metas.append(
        json.loads(
            f.with_suffix(".json")
            .read_text()
        )
    )


collection.add(
    documents=docs,
    metadatas=metas
)

查询:

collection.query(
    query_texts=[
        "Linux inode是什么"
    ],
    n_results=5
)

返回:

001.md
section_path:
[
 "Linux基础",
 "文件系统",
 "inode"
]

这个版本已经接近一个小型 Markdown-aware Document Loader,后续可以继续扩展:

  • 自动生成 embedding
  • 直接写入 ChromaDB
  • 生成 Claude Code SKILL.md
  • 根据章节生成摘要和关键词

这几个方向与你之前研究的 book-to-skill + Agent Skill + RAG 工作流可以直接衔接。

posted @ 2026-08-07 05:17  立体风  阅读(4)  评论(0)    收藏  举报