NLP Day 3 文本前處理與斷詞:spaCy、jieba 與中文的挑戰
執行需求:CPU 可跑。今天進入 NLP 系列的第一個技術篇章:文本前處理與斷詞。英文用 spaCy、中文用 jieba,並且會示範中文特有的難題——沒有空格、繁簡混用、新詞偵測。讀完之後你會知道怎麼把一段原始文字切成乾淨的詞序列,作為後續分類、檢索、生成的基礎。
引言
為什麼要先談前處理?因為無論是做分類、檢索還是生成,模型收到的第一個輸入都是「已經切好的詞或 token」。如果前處理沒做好,後面再好的模型也救不回來。這在中文尤其明顯:英文有天然的空格當詞界,中文沒有,於是「蘋果手機」可以切成「蘋果 / 手機」或「蘋 / 果手 / 機」,不同切法會讓模型學到不同的特徵。
今天會做三件事:第一,示範 spaCy 在英文上的標準前處理管線(tokenization、lemmatization、stop word 過濾);第二,示範 jieba 在中文上的斷詞、詞性標註、關鍵詞抽取;第三,用一個完整的「中文客服訊息前處理」範例把這兩套工具串起來。我們會在最後討論中文特有的挑戰:繁簡轉換、新詞偵測、半形全形混用,這些都是英文 NLP 工作者不會遇到的問題。
提醒一點:今天的範例以「統計式」前處理為主(基於詞典、頻率、共現)。Day 7 會介紹 BERT 的子詞(subword)斷詞,兩者的設計理念不同,解決的問題也不同。本系列對這兩條路線都會用到,理解各自的優勢是後續做模型選擇的基礎。
英文前處理:spaCy 標準管線
spaCy 是英文 NLP 最常用的函式庫之一。3.8 世代(2025 年 3 月)的 API 已經穩定,安裝後需要再下載一個英文模型:
pip install spacy==3.8.2
python -m spacy download en_core_web_sm
en_core_web_sm 約 12 MB,包含詞性標註、依存句法、命名實體辨識等功能。如果需要更高品質,可以改裝 en_core_web_trf(基於 Transformer,約 500 MB),速度會慢但品質更好。這個系列大部分篇章用 en_core_web_sm 就夠。
先用一個最簡單的範例跑通 spaCy:
import spacy
nlp = spacy.load("en_core_web_sm")
text = "Apple is looking at buying a U.K. startup for $1 billion."
doc = nlp(text)
# 每個 token 包含豐富資訊
print("原文".ljust(15) + "lemma".ljust(15) + "POS".ljust(8) + "is_stop".ljust(8) + "is_alpha")
print("-" * 60)
for token in doc:
print(token.text.ljust(15) + token.lemma_.ljust(15) + token.pos_.ljust(8) + str(token.is_stop).ljust(8) + str(token.is_alpha))
# 輸出範例:
# 原文 lemma POS is_stop is_alpha
# ------------------------------------------------------------
# Apple Apple PROPN False True
# is be AUX True False
# looking look VERB False True
# at at ADP True False
# buying buy VERB False True
# a a DET True False
# U.K. U.K. PROPN False False
# startup startup NOUN False True
# for for ADP True False
# $ $ SYM False False
# 1 1 NUM False False
# billion billion NUM False True
# . . PUNCT False False
# 實際欄位會依語言模型略有不同
這段程式展示 spaCy 的核心概念:nlp(text) 把一段文字轉成 Doc 物件,裡面每個 token 都有 lemma(詞幹)、POS(詞性)、is_stop(是否停用詞)、is_alpha(是否純字母)等豐富屬性。實務上做英文前處理,最常用的就是過濾掉停用詞、標點、數字,只留下有語意的詞,例如「Apple looking buy startup billion」這五個詞就能保留原句 90% 的語意。
中文斷詞:jieba 的三種切法
jieba 是中文斷詞最常用的函式庫,0.42 世代(2025 年 3 月仍持續維護)。它提供三種切詞模式:
- 精確模式:預設模式,盡量把句子切成最準確的詞,適合文本分析。
- 全模式:把句子中所有可能的詞都切出來,速度快但歧義多,適合搜尋引擎索引。
- 搜尋引擎模式:在精確模式基礎上再對長詞做細切,適合用於搜尋引擎的倒排索引。
先看 jieba 的基本用法:
import jieba
text = "我來到北京清華大學,發現校園裡的同學都在討論人工智慧的未來。"
# 精確模式(預設)
seg_precise = list(jieba.cut(text, cut_all=False))
print("精確模式:" + " / ".join(seg_precise))
# 輸出:精確模式:我 / 來到 / 北京 / 清華大學 / , / 發現 / 校園 / 裡 / 的 / 同學 / 都 / 在 / 討論 / 人工智慧 / 的 / 未來 / 。
# 全模式
seg_full = list(jieba.cut(text, cut_all=True))
print("全模式:" + " / ".join(seg_full))
# 輸出:全模式:我 / 來到 / 北京 / 清華 / 大學 / , / 發現 / 校園 / 裡 / 的 / 同學 / 都 / 在 / 討論 / 人工 / 智慧 / 的 / 未來 / 。
# 搜尋引擎模式
seg_search = list(jieba.cut_for_search(text))
print("搜尋引擎模式:" + " / ".join(seg_search))
# 輸出:搜尋引擎模式:我 / 來到 / 北京 / 清華 / 大學 / 清華大學 / , / 發現 / 校園 / 裡 / 的 / 同學 / 都 / 在 / 討論 / 人工 / 智慧 / 人工智慧 / 的 / 未來 / 。
這段程式展示三種模式的差異:精確模式把「清華大學」當一個詞、不會切成「清華 / 大學」,對語意保留較好;全模式把所有可能的詞都切出來,連「人工」、「智慧」分開也保留下來,但引入歧義;搜尋引擎模式折衷,把長詞再切一次,讓搜尋引擎能找到更精準的片段。實務上做文本分類、情緒分析、命名實體辨識時,建議用精確模式。
jieba 的詞性標註與關鍵詞抽取
jieba 也能做詞性標註(POS tagging)與關鍵詞抽取(TF-IDF based):
import jieba.posseg as pseg
import jieba.analyse
text = "台灣的半導體產業在全球供應鏈中扮演關鍵角色,台積電的先進製程技術領先全球。"
# 詞性標註
words = pseg.cut(text)
print("詞".ljust(10) + "詞性".ljust(10) + "說明")
print("-" * 50)
for w, flag in words:
note = ""
if flag == "n":
note = "名詞"
elif flag.startswith("v"):
note = "動詞"
elif flag == "ns":
note = "地名"
elif flag == "t":
note = "時間詞"
print(w.ljust(10) + flag.ljust(10) + note)
# 輸出範例:
# 詞 詞性 說明
# --------------------------------------------------
# 台灣 ns 地名
# 的 uj
# 半導體 n 名詞
# 產業 n 名詞
# 在 p
# 全球 n 名詞
# 供應鏈 n 名詞
# 中 f
# 扮演 v 動詞
# 關鍵 n 名詞
# 角色 n 名詞
# , x
# 台積電 n 名詞
# 的 uj
# 先進 a
# 製程 n 名詞
# 技術 n 名詞
# 領先 v 動詞
# 全球 n 名詞
# 。 x
# TF-IDF 關鍵詞抽取
keywords = jieba.analyse.extract_tags(text, topK=5, withWeight=True)
print()
print("關鍵詞(TF-IDF):")
for kw, weight in keywords:
print(f" {kw}(權重 {weight:.4f})")
# 輸出範例:
# 關鍵詞(TF-IDF):
# 半導體(權重 0.5123)
# 製程(權重 0.4321)
# 台積電(權重 0.4012)
# 供應鏈(權重 0.3789)
# 全球(權重 0.3456)
# 實際權重會依 jieba 內建 IDF 表略有不同
詞性標註用 jieba.posseg.cut,每個詞會附帶詞性標記(名詞 n、動詞 v、地名 ns、時間詞 t 等)。關鍵詞抽取用 TF-IDF 演算法,會自動把語料庫中常見詞(如「的」、「在」)的權重壓低,把罕見但重要的詞(如「半導體」、「製程」)的權重拉高。後續篇章做主題分析時,這個工具會很方便。
中文特有的難題:繁簡、標點、半形全形
中文 NLP 比英文難處理,其中一個原因是「同一份文本可能混用繁體、簡體、半形、全形標點」。下表列出常見的混用情境:
- 繁簡混用:「台湾」vs「臺灣」、「资讯」vs「資訊」。同一份文本裡可能兩種寫法都出現。
- 半形全形標點:英文逗號「,」vs 中文逗號「,」、英文句號「.」vs 中文句號「。」。
- 半形全形英數字:「1」(全形)vs「1」(半形)、英文「A」(全形)vs「A」(半形)。
- 語碼混用:中文裡直接插入英文「請 call 客服專線」或「這是 demo 版」。
這些混用會讓 jieba 斷詞結果不穩定。最直接的解法是在斷詞之前先做「標準化」:
import jieba
import re
# 1. 全形轉半形(只處理 ASCII 範圍內的字元)
def fullwidth_to_halfwidth(text):
result = []
for ch in text:
code = ord(ch)
# 全形 ASCII 區間:0xFF01 到 0xFF5E
high_byte = code // 256
low_byte = code - high_byte * 256
diff_from_low = low_byte - 0x01
diff_from_high = 0x5E - low_byte
is_fullwidth_ascii = high_byte == 0xFF and diff_from_low >= 0 and diff_from_high >= 0
if is_fullwidth_ascii:
result.append(chr(code - 0xFEE0))
else:
result.append(ch)
return "".join(result)
# 2. 簡體轉繁體(OpenCC)
try:
import opencc
cc = opencc.OpenCC("s2t") # 簡體轉繁體
def simplified_to_traditional(text):
return cc.convert(text)
except ImportError:
def simplified_to_traditional(text):
return text # 沒裝就跳過
# 3. 統一標點
PUNCT_MAP = {
",": ",", ".": "。",
"!": "!", "?": "?",
";": ";", ":": ":",
"(": "(", ")": ")",
}
def normalize_punct(text):
for src, dst in PUNCT_MAP.items():
text = text.replace(src, dst)
return text
# 4. 整合:標準化 → 斷詞
def preprocess_zh(text):
text = fullwidth_to_halfwidth(text)
text = simplified_to_traditional(text)
text = normalize_punct(text)
return list(jieba.cut(text, cut_all=False))
# 測試
samples = [
"請問這個產品多少錢?",
"台灣的半导体产业很重要",
"Hello, 我是 John,從台灣來!",
]
for s in samples:
tokens = preprocess_zh(s)
print(f"原文:{s}")
print(f"前處理後:" + " / ".join(tokens))
print()
# 輸出範例:
# 原文:請問這個產品多少錢?
# 前處理後:請問 / 這個 / 產品 / 多少 / 錢 / ?
#
# 原文:台灣的半导体产业很重要
# 前處理後:台灣 / 的 / 半導體 / 產業 / 很 / 重要
#
# 原文:Hello, 我是 John,從台灣來!
# 前處理後:Hello / , / 我 / 是 / John / , / 從 / 台灣 / 來 / !
# 實際輸出會依 jieba 詞典略有不同
這段工具函式做四件事:全形轉半形、簡體轉繁體(用 OpenCC)、統一標點、jieba 斷詞。整合後的 preprocess_zh 就是中文前處理的最小可用版本。後續篇章做中文任務時,會把這段擴充成完整的 ChineseTokenizer class。
spaCy 的 nlp.pipe 批次處理
當文本量大的時候,逐句呼叫 nlp(text) 會有效能問題。spaCy 提供 nlp.pipe 做批次處理,可以把多個 Doc 物件並行處理,CPU 上大約能快 2–3 倍。
import spacy
import time
nlp = spacy.load("en_core_web_sm")
texts = ["This is a sample sentence." for _ in range(500)]
# 逐句處理
start = time.time()
docs = [nlp(t) for t in texts]
elapsed_single = time.time() - start
print(f"逐句處理 {len(texts)} 句:{elapsed_single:.2f} 秒")
# 批次處理(一次丟整個 list)
start = time.time()
docs = list(nlp.pipe(texts, batch_size=50, n_process=1))
elapsed_pipe = time.time() - start
print(f"批次處理 {len(texts)} 句:{elapsed_pipe:.2f} 秒")
if elapsed_single > 0:
print(f"加速比:{elapsed_single / elapsed_pipe:.2f}x")
# 輸出範例:
# 逐句處理 500 句:2.81 秒
# 批次處理 500 句:1.07 秒
# 加速比:2.62x
# 實際數字會依 CPU 而定
nlp.pipe(texts, batch_size=50) 會把 texts 切成每 50 句一個批次處理;n_process=2 可以開兩個 process 做平行(適用於多核 CPU)。實務上做大型語料前處理時,nlp.pipe 是必備工具。jieba 也提供類似的平行介面 jieba.enable_parallel(4),開 4 個 process 並行斷詞。
完整實作:中文客服訊息前處理管線
把今天所有觀念整合成一個實戰範例:處理五句中文客服訊息,輸出每句的斷詞結果與關鍵詞。這個範例可以作為 Day 5 文本分類的輸入前處理。
import jieba
import jieba.posseg as pseg
import jieba.analyse
import re
from collections import Counter
# 自訂詞典:把「台積電」、「鴻海」當作一個詞,避免被切開
jieba.load_userdict("custom_dict.txt") # 若檔案不存在可跳過這行
messages = [
"我的訂單還沒收到,已經等了五天。",
"請問這款筆電的電池續航力如何?",
"您好,我想退貨,因為商品有瑕疵。",
"台積電的股價最近漲很多,能分析嗎?",
"系統一直當機,無法登入帳號,急!",
]
# 停用詞:常見但沒語意貢獻的字
STOPWORDS = {
"的", "了", "在", "是", "我", "有", "和", "就",
"不", "人", "都", "一", "也", "很", "到", "要",
"因為", "所以", "這", "那", "請", "您", "嗎",
",", "。", "!", "?", "、", "(", ")",
}
def preprocess(text):
# 統一標點
text = text.replace(",", ",").replace("?", "?").replace("!", "!")
# 斷詞、詞性標註、停用詞過濾
words = pseg.cut(text)
tokens = [w for w, flag in words if w not in STOPWORDS and flag != "x"]
return tokens
# 處理所有訊息
all_tokens = []
for msg in messages:
tokens = preprocess(msg)
all_tokens.extend(tokens)
print(f"訊息:{msg}")
print(f" 斷詞:{' / '.join(tokens)}")
keywords = jieba.analyse.extract_tags(msg, topK=3)
print(f" 關鍵詞:{keywords}")
print()
# 統計詞頻
counter = Counter(all_tokens)
print("詞頻統計(前 10 名):")
for word, count in counter.most_common(10):
print(f" {word}:{count} 次")
# 輸出範例:
# 訊息:我的訂單還沒收到,已經等了五天。
# 斷詞:我的 / 訂單 / 還 / 沒 / 收到 / 已 / 經 / 等 / 五 / 天
# 關鍵詞:['訂單', '收到', '天']
#
# 訊息:請問這款筆電的電池續航力如何?
# 斷詞:請問 / 這款 / 筆電 / 電池 / 續航力 / 如何
# 關鍵詞:['電池', '續航力', '筆電']
#
# 訊息:您好,我想退貨,因為商品有瑕疵。
# 斷詞:您好 / 我 / 想 / 退貨 / 商品 / 瑕疵
# 關鍵詞:['瑕疵', '退貨', '商品']
#
# 訊息:台積電的股價最近漲很多,能分析嗎?
# 斷詞:台積電 / 股價 / 最近 / 漲 / 很多 / 能 / 分析
# 關鍵詞:['台積電', '股價', '漲']
#
# 訊息:系統一直當機,無法登入帳號,急!
# 斷詞:系統 / 一直 / 當機 / 無法 / 登入 / 帳號 / 急
# 關鍵詞:['當機', '登入', '帳號']
#
# 詞頻統計(前 10 名):
# 訂單:1 次
# 收到:1 次
# 電池:1 次
# 續航力:1 次
# 退貨:1 次
# 瑕疵:1 次
# 台積電:1 次
# 股價:1 次
# 當機:1 次
# 帳號:1 次
# 實際斷詞結果會依 jieba 詞典略有不同
這段程式把今天所有工具整合起來:先用 jieba.posseg 做詞性標註,再過濾掉停用詞與標點(詞性 "x"),最後用 TF-IDF 抽取每句的關鍵詞,並統計整個語料庫的詞頻。這個管線可以直接接到 Day 5 的分類模型,把斷詞結果轉成 TF-IDF 特徵或 Word2Vec 輸入。實際輸出會依 jieba 詞典與 IDF 表略有不同。
常見錯誤與踩雷
第一個雷是「jieba 詞典沒更新」。jieba 的預設詞典是 2014 年的版本,新詞(例如「元宇宙」、「ChatGPT」、「台積電」)不會被正確切出來。對應排查:用 jieba.load_userdict("custom_dict.txt") 載入自訂詞典;或用 jieba.add_word("台積電") 動態新增。Day 7 會介紹 BERT 子詞斷詞,它的詞彙表是模型訓練時固定下來的,能涵蓋更新穎的詞。
第二個雷是「停用詞清單太長或太短」。中文常用停用詞約 300–500 個,太短(如只 50 個)會讓「的」、「了」這類高頻詞干擾統計;太長(如 2000 個)會誤刪有語意的詞(例如「不」在「不好」中很重要)。對應排查:以 300–500 個停用詞為基準,再根據任務手動增刪。
第三個雷是「英文與中文混雜時 jieba 把英文也切碎」。例如「使用 ChatGPT 寫程式」可能被切成「使用 / Chat / G / P / T / 寫 / 程式」。對應排查:用正則表達式先把英文 token 抓出來單獨處理,或用 jieba.tokenize 看實際切法再決定。
第四個雷是「忘記統一編碼」。Python 讀檔時如果忘記指定 encoding="utf-8",遇到中文會出現 UnicodeDecodeError。對應排查:所有檔案 I/O 顯式指定 UTF-8。
效能與實務提醒
spaCy 在 CPU 上每秒可處理數千個 token,jieba 在 CPU 上每秒可處理數十萬個中文 token。對中小型語料(幾萬到幾十萬句)來說,CPU 速度都足夠。如果是大規模語料(千萬句以上),建議用 spaCy 的 nlp.pipe 或 jieba 的平行模式(jieba.enable_parallel)做批次處理。
另外,前處理的速度瓶頸常常不在斷詞本身,而在 IO。讀取上百萬行的純文字檔案時,建議用 pandas.read_csv(chunksize=10000) 或 jsonlines 函式庫做分塊讀取,避免一次把全部檔案塞進記憶體。寫出也一樣:把處理結果直接寫到 parquet 之類的欄位式格式,比 CSV 或 pickle 都快上一截,這對中型語料庫的處理時間影響很大。實務上,中文維基百科約 2,000 萬句,用 jieba 平行模式在 8 核 CPU 上大約 40 分鐘可以跑完;spaCy 跑 IMDB 評論集(5 萬句)只要 2 分鐘左右。
前處理是「一次做完、後續重複用」的工序:把斷詞結果存成 pickle 或 parquet,後續訓練時直接讀取,避免每次跑訓練時重新斷詞。對大型語料來說,這個小技巧可以省下數小時。
最後提醒:前處理不是「切完就好」。下游模型如果是 BERT(subword tokenization),jieba 的斷詞結果會被丟棄;但下游模型如果是 TF-IDF 或 Word2Vec,jieba 斷詞結果就是輸入本身。本系列 Day 4 會示範兩種模型各自適合的前處理方式。
小結
今天把文本前處理與斷詞的基礎一次走完:英文用 spaCy 做 tokenization、lemmatization、stop word 過濾;中文用 jieba 做精確、全、搜尋引擎三種切法,並示範詞性標註與 TF-IDF 關鍵詞抽取。中文特有的繁簡、標點、語碼混用問題,可以用 OpenCC + 自訂標準化函式解決。我們用一個中文客服訊息的完整前處理管線把這些觀念整合起來。明天 Day 4 會進入詞向量:從 Word2Vec、GloVe 一路到 sentence-transformers 的句向量,這是後續 RAG 與語意搜尋的基礎。
結語
文本前處理看起來不起眼,但它是 NLP 管線成敗的第一個關卡。英文有空格、有現成的詞性標註器、有大量預訓練模型;中文什麼都沒有,得自己一個一個處理。今天示範的工具組合(spaCy + jieba + OpenCC)能應付 80% 的中文 NLP 場景;剩下 20% 的艱難任務(醫療、法律、財經),需要客製化詞典與規則,會在 Day 7、Day 11 的標註篇章深入討論。明天,我們會進入詞向量與語意表示的世界:從古典的 Word2Vec、GloVe,到 2025 年最常用的 sentence-transformers 多語言模型,並用一個語意相似度範例說明「為什麼句向量比詞向量更實用」。
延伸資源
- spaCy 3.8 中文模型說明(2025):
https://spacy.io/models/zh - jieba 0.42 官方文件(2025):
https://github.com/fxsjy/jieba - OpenCC 簡繁轉換函式庫(2025):
https://github.com/BYVoid/OpenCC - NLTK 9 文本前處理對比(2025):
https://www.nltk.org/book/ch03.html - Hugging Face Tokenizers 文件(2025):
https://huggingface.co/docs/transformers/tokenizer_summary
留言
張貼留言