python 以字符数量分割文档 (二)
根据前面的分析,把切割策略升级为:
一级标题优先 → 二级标题补刀 → 段落边界补刀 → 最终硬限制
改进点:
-
支持
#、##两级标题切割。 -
正确识别 Markdown 围栏代码块:
- 支持 ```
- 支持 ~~~
- 不误切代码中的
# comment
-
单个章节超过 90000 字时,继续向下寻找:
##标题- 空行段落
-
如果一个段落仍超过限制,最后才硬切。
-
保留原始 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 使用。

浙公网安备 33010602011771号