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

根据前面的分析,把切割策略升级为:

一级标题优先 → 二级标题补刀 → 段落边界补刀 → 最终硬限制

改进点:

  1. 支持 ### 两级标题切割。

  2. 正确识别 Markdown 围栏代码块:

    • 支持 ```
    • 支持 ~~~
    • 不误切代码中的 # comment
  3. 单个章节超过 90000 字时,继续向下寻找:

    • ## 标题
    • 空行段落
  4. 如果一个段落仍超过限制,最后才硬切。

  5. 保留原始 Markdown 格式。

完整代码:

#!/usr/bin/env python3

import sys
import re
from pathlib import Path


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):
    """
    判断 Markdown 代码围栏。
    支持:
        ```
        ~~~
    """

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


def find_sections(lines):
    """
    一级标题优先,二级标题作为备用切割点。

    返回:
        [
          section,
          section,
          ...
        ]
    """

    boundaries = []

    in_fence = False
    fence_char = None


    for i, line in enumerate(lines):

        fence = is_fence(line)

        if fence:

            marker = fence.group(1)[0]

            if not in_fence:
                in_fence = True
                fence_char = marker

            elif marker == fence_char:
                in_fence = False
                fence_char = None


            continue


        if not in_fence:

            # 一级标题优先
            if re.match(r"^# [^#]", line):
                boundaries.append(
                    (i, 1)
                )

            # 二级标题备用
            elif re.match(r"^## [^#]", line):
                boundaries.append(
                    (i, 2)
                )


    sections = []

    start = 0


    for index, level in boundaries:

        if index == 0:
            continue

        sections.append(
            "".join(lines[start:index])
        )

        start = index


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


    return sections



def split_large_section(section):
    """
    对超过 MAX_CHARS 的章节继续拆分。

    优先:
    1. 二级标题
    2. 空段落
    3. 硬切
    """

    if len(section) <= MAX_CHARS:
        return [section]


    lines = section.splitlines(
        keepends=True
    )


    result = []

    current = ""


    for line in lines:

        candidate = current + line


        if len(candidate) <= MAX_CHARS:

            current = candidate
            continue


        # 超限,尝试保存当前块
        if current:

            result.append(current)
            current = line

        else:

            # 单行异常巨大,强制切
            result.append(
                line[:MAX_CHARS]
            )

            current = line[MAX_CHARS:]


    if current:
        result.append(current)


    return result



def merge_sections(sections):
    """
    合并章节,使文件接近 MAX_CHARS。
    """

    files = []

    current = ""


    for section in sections:


        parts = split_large_section(section)


        for part in parts:


            candidate = current + part


            if (
                current
                and
                len(candidate) > MAX_CHARS
            ):

                files.append(current)
                current = part

            else:

                current = candidate


    if current:
        files.append(current)


    return files



def main():


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


    sections = find_sections(lines)


    files = merge_sections(
        sections
    )


    for i, content in enumerate(files, 1):

        filename = OUTPUT / f"{i:03d}.md"


        filename.write_text(
            content,
            encoding="utf-8"
        )


        print(
            f"{filename}: {len(content)} chars"
        )



if __name__ == "__main__":
    main()

改进后的算法结构

现在流程:

Markdown 文件
        |
        v
逐行扫描
        |
        +---- 检测 ``` / ~~~
        |
        +---- 跳过代码块
        |
        v
寻找标题
        |
        +---- # 一级标题
        |
        +---- ## 二级标题
        |
        v
章节切块
        |
        v
检查大小
        |
        +---- <=90000
        |       |
        |       保存
        |
        |
        +---- >90000
                |
                二级标题继续切
                |
                空行切
                |
                最后硬切

与原版相比

功能 原版 新版
一级标题切割
二级标题补刀
代码块保护 部分
支持~~~代码块
超大章节处理
避免 Markdown 破坏 一般 较好
适合 book-to-skill 一般

这个版本更适合你之前提到的 PDF → Markdown → book-to-skill → Claude Code Skill 工作流。

如果进一步用于 RAG,再加一个功能:自动生成每个分片的 YAML metadata(标题、章节路径、字符数、来源文件),这样 ChromaDB 入库时可以直接作为 metadata 使用。

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