EN

3.7 動手實踐:文件分塊

用純 Python 實作三種分塊方法,並用同一份文件比較它們切出來的結果。

RAG

把固定大小、遞迴切分和依標題切分套在同一份文件上,差異會比只看定義清楚得多。以下程式只用 Python 內建功能,不需要另外安裝套件。

測試用的文件

一份很短的員工手冊,Markdown 格式,共 214 個字:

# 員工手冊

## 特別休假

員工在同一公司服務滿六個月未滿一年者,每年有三天特別休假。滿一年未滿兩年者,有七天。滿兩年未滿三年者,有十天。

特別休假的日期,由員工和公司協商安排。年度結束時沒有休完的天數,公司應該折算成工資發給員工。

## 加班規定

加班必須事先經主管同意。平日加班前兩小時,以平日每小時工資的 1.34 倍計算。第三小時起,以 1.67 倍計算。

休息日加班另有規定,不得低於勞動基準法的標準。

完整程式

存成 chunk_demo.py:

DOC = """# 員工手冊

## 特別休假

員工在同一公司服務滿六個月未滿一年者,每年有三天特別休假。滿一年未滿兩年者,有七天。滿兩年未滿三年者,有十天。

特別休假的日期,由員工和公司協商安排。年度結束時沒有休完的天數,公司應該折算成工資發給員工。

## 加班規定

加班必須事先經主管同意。平日加班前兩小時,以平日每小時工資的 1.34 倍計算。第三小時起,以 1.67 倍計算。

休息日加班另有規定,不得低於勞動基準法的標準。
"""


def fixed_chunks(text, size, overlap):
    """固定大小:每 size 個字切一刀,相鄰兩塊重疊 overlap 個字"""
    step = size - overlap
    chunks = []
    for start in range(0, len(text), step):
        piece = text[start:start + size]
        if piece.strip():
            chunks.append(piece)
        if start + size >= len(text):
            break
    return chunks


def split_keep(text, sep):
    """用 sep 切開,但把 sep 留在前一段的結尾"""
    parts = text.split(sep)
    out = [p + sep for p in parts[:-1]] + [parts[-1]]
    return [p for p in out if p]


def recursive_chunks(text, size, seps=("\n\n", "\n", "。", "")):
    """遞迴切分:先試著用大的分隔(段落),太長再用小的(換行、句號),最後才硬切"""
    if len(text) <= size:
        return [text]
    sep, rest = seps[0], seps[1:]
    if sep == "":
        return [text[i:i + size] for i in range(0, len(text), size)]
    chunks, current = [], ""
    for part in split_keep(text, sep):
        if len(part) > size:
            if current:
                chunks.append(current)
                current = ""
            chunks.extend(recursive_chunks(part, size, rest))
        elif len(current) + len(part) <= size:
            current += part
        else:
            chunks.append(current)
            current = part
    if current:
        chunks.append(current)
    return [c.strip() for c in chunks if c.strip()]


def heading_chunks(text):
    """依 Markdown 標題切:每一節是一塊,並記下它所在的標題路徑"""
    chunks, path, body = [], [], []

    def flush():
        content = "\n".join(body).strip()
        if content:
            chunks.append({"headings": " > ".join(path), "text": content})
        body.clear()

    for line in text.splitlines():
        if line.startswith("#"):
            flush()
            level = len(line) - len(line.lstrip("#"))
            path[:] = path[: level - 1] + [line.lstrip("# ").strip()]
        else:
            body.append(line)
    flush()
    return chunks


if __name__ == "__main__":
    print(f"全文共 {len(DOC)} 個字\n")

    print("=== 固定大小:size=80, overlap=20 ===")
    for i, c in enumerate(fixed_chunks(DOC, 80, 20), 1):
        print(f"[{i}] ({len(c)} 字) {c!r}")

    print("\n=== 遞迴切分:size=80 ===")
    for i, c in enumerate(recursive_chunks(DOC, 80), 1):
        print(f"[{i}] ({len(c)} 字) {c!r}")

    print("\n=== 依標題切分 ===")
    for i, c in enumerate(heading_chunks(DOC), 1):
        print(f"[{i}] {c['headings']} ({len(c['text'])} 字)")

執行:

python chunk_demo.py

執行結果

全文共 214 個字

=== 固定大小:size=80, overlap=20 ===
[1] (80 字) '# 員工手冊\n\n## 特別休假\n\n員工在同一公司服務滿六個月未滿一年者,每年有三天特別休假。滿一年未滿兩年者,有七天。滿兩年未滿三年者,有十天。\n\n特別休假的日'
[2] (80 字) '兩年未滿三年者,有十天。\n\n特別休假的日期,由員工和公司協商安排。年度結束時沒有休完的天數,公司應該折算成工資發給員工。\n\n## 加班規定\n\n加班必須事先經主管'
[3] (80 字) '\n\n## 加班規定\n\n加班必須事先經主管同意。平日加班前兩小時,以平日每小時工資的 1.34 倍計算。第三小時起,以 1.67 倍計算。\n\n休息日加班另有規定,'
[4] (34 字) '.67 倍計算。\n\n休息日加班另有規定,不得低於勞動基準法的標準。\n'

=== 遞迴切分:size=80 ===
[1] (72 字) '# 員工手冊\n\n## 特別休假\n\n員工在同一公司服務滿六個月未滿一年者,每年有三天特別休假。滿一年未滿兩年者,有七天。滿兩年未滿三年者,有十天。'
[2] (55 字) '特別休假的日期,由員工和公司協商安排。年度結束時沒有休完的天數,公司應該折算成工資發給員工。\n\n## 加班規定'
[3] (57 字) '加班必須事先經主管同意。平日加班前兩小時,以平日每小時工資的 1.34 倍計算。第三小時起,以 1.67 倍計算。'
[4] (23 字) '休息日加班另有規定,不得低於勞動基準法的標準。'

=== 依標題切分 ===
[1] 員工手冊 > 特別休假 (103 字)
[2] 員工手冊 > 加班規定 (82 字)

三種方法比較

固定大小 遞迴切分 依標題切分
塊數 4 4 2
句子完整嗎 不完整,切在句子中間 完整 完整
數字 1.67 被切斷了嗎 是 否 否
標題和內容對得上嗎 否,標題被拆開 大致,但第 2 塊尾端多了下一節的標題 是,標題是 metadata
每塊大小 一致 接近上限 不一致(103、82)
需要文件有結構嗎 不需要 不需要 需要

觀察與思考

  • 固定大小最粗糙,但在沒有結構的文字上還是可用。
  • 遞迴切分已經大幅改善,大部分一般文字拿它當預設就夠了。
  • 依標題切分得到的塊最符合人類的閱讀單位,但前提是文件要有結構,而且每一節的長度要在可接受的範圍。
  • 這份文件太短,所以差別看起來不大。實際的文件有幾十頁,差異會更明顯。
  • 沒有哪一種永遠最好,要看資料的型態和你的問題。

自己試試看

  1. 把 size 改成 40、120、200,看看切法怎麼變。
  2. 把 overlap 改成 0 和 40,觀察重疊的效果。
  3. 換成你自己的文件,例如一篇筆記或一份規章,看哪一種切得最自然。
  4. 試著把依標題切分的結果,如果某一節超過 200 字,再用 recursive_chunks 切一次,把兩種方法合起來。
  5. 把標題路徑加進每一塊的開頭(3.5 提到的技巧),看文字變成怎樣。
← 全部文章