2026年8月31日 星期一

Python一下:用jieba整理水井村地方故事與關鍵詞

《Python一下:從風土資料到智慧生活》第 32 篇

用jieba整理水井村地方故事與關鍵詞

讓「水井三寶、姻緣花、柴井車、智慧養殖」不再被切散,也不讓演算法取代社區自己的說法。

CH14 斷詞處理jieba地方故事
學習目標
完成本篇後,你能解釋中文斷詞的必要性,使用jieba精確模式處理繁體中文,建立水井村自訂詞典與停用詞,統計詞頻、搜尋上下文、擷取TF-IDF關鍵詞,建立可人工審查的結果檔,並說明口述故事的同意、去識別、台語轉寫及地方詮釋限制。

一、電腦為何不知道「姻緣花」是一個詞?

英文常以空白分隔單字,中文句子通常沒有天然空白。「水井三寶包含烏龜、白馬與姻緣花」若逐字處理,就無法直接知道「水井三寶」和「姻緣花」是地方概念。斷詞的工作,就是提出一種詞語邊界。

原文可能結果問題
水井三寶水井/三寶地方品牌詞被拆開
姻緣花姻緣/花地方文化名稱失去完整性
魚菜共生淨水魚菜/共生/淨水可能需要保留「魚菜共生」
HUB 8735 Ultra英數與空白混合需另外設計技術詞正規化
斷詞沒有唯一答案:適合搜尋的切法、適合關鍵詞統計的切法,以及社區認同的地方詞,可能不同。結果必須依任務評估,不能把工具輸出當成地方語言的標準答案。

二、安裝jieba並記錄維護風險

import subprocess
import sys


subprocess.run(
    [sys.executable, "-m", "pip", "install", "jieba"],
    check=True,
)

截至本篇核對時,PyPI顯示的最新版仍是2020年發布的0.42.1。它適合教學與既有專案評估,但正式採用前應重新檢查維護狀態、Python相容性、授權、安全與替代工具,不能因為安裝成功就直接長期部署。

三、第一個繁體中文斷詞

import jieba


text = "水井三寶包含烏龜、白馬與姻緣花。"
tokens = list(jieba.cut(text, cut_all=False, HMM=True))

print(tokens)
print("/".join(tokens))

cut_all=False是精確模式,較適合一般文字分析;HMM可嘗試辨識未登錄詞,但不保證地方詞一定正確。

四、三種模式用途不同

sentence = "水井村推動智慧養殖與魚菜共生淨水。"

precise = list(jieba.cut(sentence, cut_all=False))
full = list(jieba.cut(sentence, cut_all=True))
search = list(jieba.cut_for_search(sentence))

print("精確模式:", precise)
print("全模式:", full)
print("搜尋模式:", search)
模式適合限制
精確模式詞頻、一般分析仍可能切錯地方詞
全模式觀察可能詞語重疊多,不能直接計算一般詞頻
搜尋引擎模式建立搜尋索引長詞會再次切分,詞數不可和精確模式混比

五、使用獨立Tokenizer,避免全域設定互相干擾

import jieba


tokenizer = jieba.Tokenizer()
for word in [
    "水井三寶",
    "姻緣花",
    "柴井車",
    "魚菜共生",
    "智慧養殖",
    "工作蝦",
]:
    tokenizer.add_word(word, freq=100000)

text = "水井三寶與柴井車承載地方記憶。"
print(list(tokenizer.cut(text, cut_all=False)))

在多人專案或測試中,獨立Tokenizer比修改全域 jieba.dt更容易控制。詞頻100000只是確保教學示例優先成詞,不代表真實語料頻率。

六、繁體中文詞典與地方詞典分工

jieba專案提供較大的 extra_dict/dict.txt.big,可作為繁體中文候選詞典;實際專案應把經審核的詞典檔納入版本管理,記錄來源、授權與雜湊,不在程式執行時臨時從不明網址下載。

from pathlib import Path
import jieba


def build_tokenizer(base_dictionary=None, user_dictionary=None):
    tokenizer = jieba.Tokenizer(
        dictionary=str(base_dictionary) if base_dictionary else None
    )
    if user_dictionary:
        path = Path(user_dictionary)
        if not path.is_file():
            raise FileNotFoundError(path)
        tokenizer.load_userdict(str(path))
    return tokenizer


# 專案備妥詞典後再啟用:
# tokenizer = build_tokenizer(
#     "data/dict.txt.big", "data/shuijing_words.txt"
# )

七、自訂詞典格式

自訂詞典每行為「詞語 詞頻 詞性」,詞頻與詞性可省略,檔案使用UTF-8。建議由社區與課程團隊共同審查:

水井三寶 100000 nz
姻緣花 100000 nz
柴井車 100000 nz
工作蝦 100000 nz
魚菜共生 100000 nz
智慧養殖 100000 nz
風頭水尾 100000 nz

nz只是常見自訂專名標記,詞性標註並非本篇主要目標。地方詞典還應另有說明表,記錄詞義、別稱、使用者、例句、審核者與版本日期。

八、先做最小文字清理

import re
import unicodedata


def normalize_text(text):
    text = unicodedata.normalize("NFKC", str(text))
    text = text.replace("\u3000", " ")
    text = re.sub(r"[ \t]+", " ", text)
    text = re.sub(r"\n{3,}", "\n\n", text)
    return text.strip()


sample = "水井村 推動  智慧養殖。"
print(normalize_text(sample))

NFKC會統一部分全形與相容字元,但也可能改變原始書寫形式。口述史原稿應另外保存;分析使用的是衍生副本,並記錄清理規則。

九、停用詞:刪掉常見詞也可能刪掉地方語氣

STOPWORDS = {
    "的", "了", "在", "和", "與", "是",
    "有", "也", "就", "我們", "一個",
}


def meaningful_tokens(text, tokenizer, stopwords=STOPWORDS):
    for token in tokenizer.cut(normalize_text(text)):
        token = token.strip()
        if not token:
            continue
        if token in stopwords:
            continue
        if not re.search(r"[\u3400-\u9fffA-Za-z0-9]", token):
            continue
        yield token


story = "我們在水井村用智慧養殖守護工作蝦。"
print(list(meaningful_tokens(story, tokenizer)))

停用詞必須依任務制定。長者口述中的「阮、咱、彼時、做伙」可能承載語氣、群體關係與台語特色,不應直接套用一般中文停用詞刪除。

十、用Counter統計地方詞頻

from collections import Counter


stories = [
    "水井村推動智慧養殖,也用魚菜共生淨水。",
    "水井三寶包含烏龜、白馬與姻緣花。",
    "姻緣花承載水井村的地方記憶與祝福。",
]

counter = Counter()
for story in stories:
    counter.update(meaningful_tokens(story, tokenizer))

for word, count in counter.most_common(10):
    print(word, count)

詞頻高表示在這批語料出現多,不等於對社區最重要。一次只被長者說出的罕見詞,可能反而是珍貴文化線索。

十一、同一詞在幾篇故事出現?

document_frequency = Counter()

for story in stories:
    unique_words = set(
        meaningful_tokens(story, tokenizer)
    )
    document_frequency.update(unique_words)

for word, documents in document_frequency.most_common(10):
    print(word, "出現在", documents, "篇")

詞頻計算總出現次數;文件頻率計算出現於幾篇文件。兩者回答不同問題,不能混稱「熱門程度」。

十二、顯示關鍵詞所在原句

def find_contexts(stories, keyword):
    results = []
    for index, story in enumerate(stories, start=1):
        sentences = re.split(r"(?<=[。!?])", story)
        for sentence in sentences:
            sentence = sentence.strip()
            if keyword in sentence:
                results.append({
                    "story": index,
                    "sentence": sentence,
                })
    return results


for item in find_contexts(stories, "姻緣花"):
    print(item)

詞頻表應能回到原句,才能讓社區夥伴確認用法與意義。公開結果若含可識別人物或私密經驗,仍需授權與去識別。

十三、TF-IDF擷取關鍵詞

import jieba.analyse


corpus_text = "\n".join(stories)
keywords = jieba.analyse.extract_tags(
    corpus_text,
    topK=8,
    withWeight=True,
)

for word, weight in keywords:
    print(word, round(weight, 4))

TF-IDF權重不是機率,也不是社區重要性分數。jieba預設IDF來自其內建語料,不一定代表臺灣農漁村或水井村;地方分析可建立經審查的領域語料與IDF,但需要足夠文件及清楚方法。

十四、讓TF-IDF使用自訂Tokenizer的地方詞

jieba.analyse預設使用全域斷詞器,可能和前面的獨立Tokenizer設定不同。簡單教材可在分析前把地方詞加入全域詞典,但要在單一初始化函式集中管理:

DOMAIN_WORDS = [
    "水井三寶", "姻緣花", "柴井車",
    "魚菜共生", "智慧養殖", "工作蝦",
]


def configure_global_jieba():
    for word in DOMAIN_WORDS:
        jieba.add_word(word, freq=100000, tag="nz")


configure_global_jieba()
keywords = jieba.analyse.extract_tags(
    "\n".join(stories), topK=8, withWeight=True
)
print(keywords)

測試要先重新初始化或固定執行順序,避免全域狀態讓測試互相污染。大型系統可評估更易注入Tokenizer的分析設計。

十五、比較詞頻與TF-IDF,不急著選「唯一答案」

frequency_top = [
    word for word, count in counter.most_common(8)
]
tfidf_top = [word for word, weight in keywords]

comparison = {
    "both": sorted(set(frequency_top) & set(tfidf_top)),
    "frequency_only": [
        word for word in frequency_top if word not in tfidf_top
    ],
    "tfidf_only": [
        word for word in tfidf_top if word not in frequency_top
    ],
}

print(comparison)

共同出現的詞可優先檢視;只在一邊出現的詞也不能直接丟掉。最終主題命名應回到原句、訪談脈絡與社區共同討論。

十六、為地方詞典建立回歸測試

CASES = {
    "水井三寶包含姻緣花": {"水井三寶", "姻緣花"},
    "魚菜共生協助淨水": {"魚菜共生"},
    "智慧養殖記錄工作蝦": {"智慧養殖", "工作蝦"},
}


def test_domain_dictionary(tokenizer):
    failures = []
    for sentence, expected in CASES.items():
        actual = set(tokenizer.cut(sentence, cut_all=False))
        missing = expected - actual
        if missing:
            failures.append({
                "sentence": sentence,
                "missing": sorted(missing),
                "actual": sorted(actual),
            })
    return failures


failures = test_domain_dictionary(tokenizer)
assert not failures, failures
print("地方詞典測試通過")

每次更新jieba、基礎詞典或地方詞典都執行測試,避免原本正確的地方詞突然被拆開。

十七、輸出可審查的JSON,而非只有詞雲

import json
from datetime import datetime, timezone


report = {
    "created_at": datetime.now(timezone.utc).isoformat(),
    "method": "jieba精確模式+自訂地方詞+詞頻與TF-IDF",
    "documents": len(stories),
    "top_frequency": counter.most_common(10),
    "top_tfidf": [
        {"word": word, "weight": weight}
        for word, weight in keywords
    ],
    "review_status": "待社區人工審查",
}

print(json.dumps(report, ensure_ascii=False, indent=2))

詞雲視覺吸引人,但會省略方法、分母與上下文。研究或USR成果應保留版本、語料範圍、清理規則、詞典、停用詞、程式與人工審查紀錄。

十八、台語口述故事要多一層語言尊重

階段應注意
語音轉文字台語辨識可能誤轉人名、地名、農產與料理
轉寫選擇漢字、台羅或混合書寫需保留原則與原始錄音
jieba斷詞主要針對中文文字,不等於台語語言分析工具
修訂由說話者或熟悉地方語言的人確認,不擅自「改成標準中文」
公開確認用途、範圍、署名/匿名、撤回及保存期限

十九、個資去識別不能只靠正規表示式

import re


def flag_possible_sensitive_text(text):
    patterns = {
        "phone": r"(?<!\d)09\d{8}(?!\d)",
        "email": r"[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}",
        "address_hint": r"(?:路|街|巷|弄|號)",
    }
    return {
        name: bool(re.search(pattern, text))
        for name, pattern in patterns.items()
    }


print(flag_possible_sensitive_text(
    "這是一段不含聯絡方式的示範故事。"
))

這只能標記部分可能風險,無法辨識暱稱、親屬關係、罕見事件或組合資訊。去識別必須人工審查;即使移除姓名,故事仍可能讓社區成員辨識當事人。

二十、課堂挑戰

挑戰A|基礎:以5段匿名地方故事建立詞頻表,列出有效詞數、文件頻率及每個前10詞的原句。
挑戰B|進階:建立20個水井村地方詞的詞典與至少10個回歸測試案例,記錄新增前後斷詞差異。
挑戰C|USR場域:請長者或社區夥伴檢視演算法選出的關鍵詞,標記「認同、需改名、不宜公開、演算法漏掉」,比較機器結果與地方觀點。

二十一、用AI協助整理,但不能代替受訪者確認

請擔任地方故事斷詞與關鍵詞分析助教。
語料來自已同意教學分析並完成去識別的社區故事。
請先檢查繁體中文、台語轉寫、地方專名、同義詞、停用詞與個資風險,
再協助比較詞頻、文件頻率與TF-IDF結果。
不得把演算法關鍵詞稱為「社區最重要的文化」,
不得擅自改寫長者原意,也不要要求提供真實姓名或未授權逐字稿。
請輸出待人工確認清單、原句索引與地方詞典修訂建議。

二十二、延伸閱讀

沒有留言:

張貼留言