若要在 Java 中從字串移除標點符號,請呼叫 String.replaceAll("\\p{P}+", "") — \p{P} 這個 Unicode 屬性逸出會比對七個 Unicode 標點子分類(Pc、Pd、Ps、Pe、Po、Pi 和 Pf)中的每一個字元,而 replaceAll 會在單一線性掃描中將它們全部刪除。Java 開發人員會在清理日誌行、為自然語言處理進行文字斷詞、在儲存前正規化使用者輸入,或為全文索引準備字串時選用這個模式。java.util.regex 內建的 regex 引擎預設具有 Unicode 感知能力,這代表 \p{P} 能做到大多數手刻 [^a-zA-Z0-9 ] 模式做不到的事 — 它確實會移除真正的標點,而非憑猜測處理 ASCII。不過,純 regex 有已知的弱點:縮寫會塌縮、連字複合詞會失去連字號、小數點會從價格中消失,引號會被當成撇號處理。本指南先介紹 Java 程式碼,接著說明 移除標點 工具如何補抓 regex 留下的 bug。

remove punctuation from string java
remove punctuation from string java

七個 Unicode 標點子分類

Unicode 將每個碼位分組到通用分類中,標點歸於分類 P,並細分為七個子分類。Pc 是連接符標點(底線及特定連接記號),Pd 是破折號(連字號、en dash 和 em dash),Ps 是起始標點如 ( [ {,Pe 是結束標點如 ) ] },Po 是「其他」標點,包含逗號、句號、冒號、分號、問號、驚嘆號以及省略號,Pi 是起始引號(花式左雙引號、書名號、左單引號),Pf 是結束引號(花式右雙引號、右書名號、右單引號)。Java 的 regex 引擎透過 Unicode 屬性逸出 \p{P} 讀取這些分類,並將它們視為單一字元類別 — 一個模式,七個子分類,Unicode 認識的每個標點都涵蓋在內,包括全形 CJK 標點如表意句號 (。)。每個子分類的宣稱都已逐字元對照 Unicode 字元資料庫 驗證,而非對照 ASCII 簡表。這裡使用的 \p{...} 屬性語法記載於 Unicode 字元類別逸出參考;Java 的 java.util.regex 與 JavaScript 共用同一套 Unicode 屬性名稱。

子分類名稱範例
Pc連接符標點_ ‿ ⁔
Pd破折號- – —
Ps起始標點( [ {
Pe結束標點) ] }
Po其他標點. , ; : ! ? …
Pi起始引號" « ' "
Pf結束引號" » ' '

三種 Java 移除標點的方法

Java 中有三種從字串移除標點的方法。第一種是 String.replaceAll("\\p{P}+", "") — 為建議做法。它具有 Unicode 感知能力,以單一線性掃描執行,+ 量詞會將連續標點塌縮,因此 !!! 會變成空字串,而非在刪除的記號之間留下空隙。第二種是 String.replaceAll("\\p{Punct}+", "") — POSIX 風格的字元類別。它比對的集合略有不同(特別是包含底線和特定 ASCII 符號),在你只需要處理 ASCII 標點時很實用。第三種是搭配 Character.isLetterOrDigit() 進行手動字元迭代 — 速度較慢、程式碼較多,而且在 Unicode 字母存在於 ASCII 範圍之外時會悄悄出錯。生產環境的 Java 程式碼應優先採用第一種做法,除非專案明確要求 POSIX 語意。

用一行 Java 程式碼移除字串中的標點

具體來說,在 Java 原始檔中:

String input = "Hello, world! It's 2024 — and prices start at $9.99."; String stripped = input.replaceAll("\\p{P}+", "");

那一行程式碼就會在一次掃描中移除每個逗號、驚嘆號、em dash 和句號。錢號保留下來 — 符號並非標點,\p{P} 不會比對 $。It's 中的撇號也被移除,因為純粹的 \p{P}+ 無法區分撇號與其他 Po 字元。em dash 之後留下兩個相鄰空格,因為 replaceAll 把符合的碼位取代為空字串;若你想要單一空格的輸出,可以再執行一輪將連續空格塌縮。

若要保留縮寫,更謹慎的 regex 會加入 lookbehind 與 lookahead 斷言,要求任何記號的兩側都必須有字母或數字:

String preserve = input.replaceAll("(?<![\\p{L}\\p{N}])\\p{P}+|(?<=[\\p{L}\\p{N}])\\p{P}+(?![\\p{L}\\p{N}])", "");

該 regex 會保護單字內部的記號 — don't 保留、state-of-the-art 保留、2020-2021 保留 — 同時仍會移除飄浮在單字邊界或空格之間的標點。這個規則有一個誠實的後果:像 the dogs' bowls 這種結尾所有格的撇號會被移除,因為後面沒有內容讓 lookahead 比對。在註解中記載這點,比隱瞞它更好。

符號、貨幣符號和表情符號並非標點

即便再完美的 Java regex 仍有尖銳的邊角:絕不能移除看起來像標點的符號。Unicode 將符號放在獨立的分類中 — Sm(數學)、Sc(貨幣)、Sk(修飾)、So(其他) — 因此錢號 $、加號 +、等號 =、版權符號 ©、@ 符號、% 符號,以及你能從手機貼上的每個表情符號,都歸類為符號而非標點。使用 [^a-zA-Z0-9 ] 作為速記的 regex 通常會一併刪除它們,從 2014 年的論壇解答複製該模式的開發人員,最後會得到 1999 tax,原來的價格已不見。

字元Unicode 分類\p{P} 是否比對?Java 移除後是否保留?
$Sc(貨幣符號)
+Sm(數學符號)
=Sm(數學符號)
©So(其他符號)
😀So(其他符號)
,Po(其他標點)
.Po(其他標點)
Pd(破折號)

標點與符號的區分,正是「remove punctuation from string java」這個問題需要仔細回答的全部原因。

讓每個 Java 實作踩坑的邊界情況

在 regex 進入生產環境前,有三種情況需要明確注意。

數字會失去小數點與千分位分隔符。"9.99" 會變成 "999","1,000" 會變成 "1000",因為句號和逗號都屬於標點。這就是移除標點的意義。若你的程式碼需要保留各部分,請將每次符合取代為單一空格 — replaceAll("\\p{P}+", " ") — 接著塌縮連續空格;數字會變成 "9 99" 而非黏在一起。對財務資料來說,幾乎從來不該直接移除標點;應該改用解析數字的方式。

縮寫與所有格預設會塌縮。"don't" 變成 "dont","it's" 變成 "its","James's book" 變成 "Jamess book"。前面使用 lookbehind/lookahead 的 regex 可修補多數情況,但像 "the dogs' bowls" 這種結尾所有格仍會失去撇號 — 後面沒有內容,lookahead 永遠不會觸發。老實記載此情況的 Java 檔案,比用未記載的 if 隱藏後果的版本更有用。

換行與 Tab 不受影響。Java 的 \p{P} 不會比對 \n 或 \t,因此多行輸入的形狀得以保留。在為日誌行做索引前移除標點時,這點很重要。

以「移除標點」工具驗證 Java 輸出

Java 程式碼編譯慢且在本機執行,這使得它不適合「我只想看移除標點後文字長什麼樣」這種情境。Remove Punctuation 工具在瀏覽器中進行同樣的 Unicode 分類檢查 — 精準移除七個標點子分類、不動符號和表情符號,並提供獨立開關以保護單字內部的撇號與連字號。貼上範例字串,決定縮寫與複合詞是否保留記號,按下按鈕,頁面底部的計數器會精準回報刪除了多少個碼位。

這個工具最適合作為驗證步驟。對同一輸入執行 Java regex,把 Java 輸出在未啟用任何保護開關的情況下貼進工具,確認計數器維持在零 — 代表你的 regex 與 Unicode 標準定義的移除集合相同。若 Java 版本遺失了工具保留的字元,表示你的 regex 比對過多;若工具遺失了 Java 版本保留的字元,則表示你的 regex 漏了七個子分類中的某個。此操作具等冪性,再次處理輸出結果不會改變任何東西。

舉例來說,輸入 One, two. Three! 正好包含三個標點:一個逗號、一個句號、一個驚嘆號。Java replaceAll("\\p{P}+", "") 的結果是 One two Three。把該結果在未啟用任何開關的情況下貼回工具,會得到相同字串且移除計數為零。若計數器未維持在零,代表 regex 比對的範圍超過七個標點子分類。

何時使用 Java 程式碼,何時使用工具

分工很單純。任何在生產環境執行的東西 — 資料管線、日誌清理器、索引步驟 — 寫 Java;regex 一行、測試一行,稽核軌跡留在你的版本庫。任何點擊比編譯更快的事 — 快速檢查從同事那貼來的一段文字、在程式碼審查前對 regex 做健全性檢查、向非工程師展示「移除標點」實際上是什麼意思 — 用工具。Java 程式碼是大規模可重複工作的正解;瀏覽器工具則適合用在讀完本文到撰寫 regex 之間的那個當下。

想進一步了解,請參閱 如何從 Excel 儲存格移除特殊字元

想進一步了解,請參閱 如何在 Excel 工作表中移除重複行