2026年8月31日 星期一

Python一下:用正規表示式整理地方故事、電話與感測器編號

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

用正規表示式整理地方故事、電話與感測器編號

用可讀的文字規則,從逐字稿與巡查紀錄中搜尋、驗證、擷取及遮罩資料。

CH9 正規表示式re資料清理
學習目標
完成本篇後,你能讀懂常用正規表示式符號,使用 fullmatch()search()findall()finditer()split()sub(),並以群組整理感測器編號、日期及逐字稿。

一、什麼時候需要正規表示式?

一般字串方法適合搜尋固定文字;當資料具有「形式固定、內容會變」的特徵,例如 TEMP-01PH-07 或日期,就可使用正規表示式描述格式。

重要界線:格式符合不代表資料真實。電話格式正確不代表已獲聯絡同意;日期格式正確不代表確有該場活動;感測器編號正確也不代表設備在線。程式驗證後仍須進行來源、授權與場域確認。

二、先認識 re 與原始字串

import re

text = "今日巡查 TEMP-01 與 PH-07。"
match = re.search(r"TEMP-\d{2}", text)

if match:
    print("找到:", match.group())
    print("位置:", match.span())

模式前的 r 表示原始字串,可減少反斜線被 Python 字串先行解讀的問題。正規表示式通常建議寫成 r"..."

三、常用符號速查

符號意義例子
\d數字\d{2} 兩位數字
\s空白字元\s+ 一個以上空白
.除換行外的任意字元需小心範圍過大
[ABC]指定集合中的一個字元[A-Z] 大寫英文字母
* + ?0次以上、1次以上、0或1次-? 可有可無的連字號
{m,n}重複次數\d{2,3}
^ $字串開頭、結尾完整格式限制
()群組及擷取(TEMP|PH)

四、fullmatch:驗證完整感測器編號

import re

pattern = re.compile(r"(TEMP|PH|DO)-\d{2}")
sensor_ids = ["TEMP-01", "PH-07", "DO-12", "TEMP-1", "XPH-07"]

for sensor_id in sensor_ids:
    valid = pattern.fullmatch(sensor_id) is not None
    print(sensor_id, "格式正確" if valid else "格式錯誤")

fullmatch() 要求整個字串符合模式,適合欄位格式驗證;search() 只要在文字任一位置找到即可。

五、群組:把編號拆成種類與序號

import re

pattern = re.compile(
    r"(?P<kind>TEMP|PH|DO)-(?P<number>\d{2})"
)
match = pattern.fullmatch("TEMP-03")

if match:
    print("種類:", match.group("kind"))
    print("序號:", match.group("number"))
    print(match.groupdict())

命名群組 (?P<名稱>...) 比只用第 1、2 群組更容易閱讀,適合後續轉成字典或資料庫欄位。

六、findall 與 finditer:找出全部符合項目

import re

log = "TEMP-01 正常;PH-07 待確認;TEMP-02 正常。"
pattern = re.compile(r"(?:TEMP|PH|DO)-\d{2}")

print(pattern.findall(log))

for match in pattern.finditer(log):
    print(match.group(), match.start(), match.end())

findall() 快速取得結果;需要位置或群組細節時,使用 finditer()。模式中的 (?:...) 是不擷取群組,可避免 findall() 只回傳群組內容。

七、擷取巡查日期

import re
from datetime import date

pattern = re.compile(
    r"(?P<year>\d{4})[-/]"
    r"(?P<month>\d{1,2})[-/]"
    r"(?P<day>\d{1,2})"
)

text = "巡查日期:2026/08/30"
match = pattern.search(text)

if match:
    try:
        patrol_date = date(
            int(match.group("year")),
            int(match.group("month")),
            int(match.group("day")),
        )
    except ValueError as error:
        print("日期不存在:", error)
    else:
        print(patrol_date)

正規表示式只能確認外觀。像 2026/02/31 也可能符合模式,所以還要交給 date 檢查日期是否存在。

八、整理逐字稿空白與重複標點

import re
import unicodedata


def clean_transcript(text):
    text = unicodedata.normalize("NFC", text)
    text = re.sub(r"[ \t]+", " ", text)
    text = re.sub(r"。{2,}", "。", text)
    text = re.sub(r"\n{3,}", "\n\n", text)
    return text.strip()


raw = "阿嬤講:  姻緣花開矣。。。\n\n\n這是庄頭的記持。"
print(clean_transcript(raw))
口述資料原則:清理空白與排版前先保留原始逐字稿。不要用正規表示式刪除語助詞、重複語句或台語用字,因為它們可能承載語氣、記憶與地方語言特徵。

九、用 split 切分多種標點

import re

text = "白馬、烏龜;姻緣花,柴井車"
items = re.split(r"[、;,]\s*", text)

for item in items:
    print(item)

方括號表示其中任一字元。若分隔規則只是單一固定字串,優先使用一般的 str.split(),會更簡單易懂。

十、電話遮罩:公開前降低識別風險

以下僅示範常見手機形式的遮罩,不宣稱涵蓋所有合法電話格式。

import re


def mask_mobile(text):
    pattern = re.compile(r"(?<!\d)(09\d{2})-?(\d{3})-?(\d{3})(?!\d)")
    return pattern.sub(r"\1-***-\3", text)


note = "聯絡電話 0912-345-678,僅供教學示範。"
print(mask_mobile(note))

遮罩不是匿名化保證;姓名、地址、職稱與事件內容交叉比對後仍可能辨識個人。公開社區資料前應取得同意並做整體風險檢查。

十一、批次解析巡查紀錄

import re

pattern = re.compile(
    r"^(?P<sensor>(?:TEMP|PH|DO)-\d{2})"
    r"\s*,\s*(?P<value>-?\d+(?:\.\d+)?)"
    r"\s*,\s*(?P<unit>[^,]+)$"
)

lines = [
    "TEMP-01, 27.2, °C",
    "PH-07, 7.4, pH",
    "TEMP-2, 離線, °C",
]

records = []
errors = []

for line_number, line in enumerate(lines, start=1):
    match = pattern.fullmatch(line.strip())
    if match is None:
        errors.append(f"第 {line_number} 行格式錯誤:{line}")
        continue

    record = match.groupdict()
    record["value"] = float(record["value"])
    records.append(record)

print(records)
print(errors)

十二、貪婪與非貪婪

import re

text = "【白馬】故事一【烏龜】故事二"

greedy = re.findall(r"【.*】", text)
non_greedy = re.findall(r"【.*?】", text)

print("貪婪:", greedy)
print("非貪婪:", non_greedy)

* 預設會盡量多吃字元,*? 則盡量少。更穩定的寫法通常是明確排除結束符號,例如標題可寫成 r"【[^】]+】"

十三、常見錯誤檢查表

現象原因修正
明明格式多字卻驗證成功使用 search 而非 fullmatch完整欄位改用 fullmatch
反斜線規則失效忘記原始字串模式使用 r"..."
findall 只回傳部分內容模式含擷取群組改用非擷取群組或 finditer
一次抓到兩個標題量詞太貪婪使用非貪婪或排除字元
模式可讀性很差一條正規式承擔太多工作拆成步驟並用 Python 驗證語意

十四、USR 實作挑戰

挑戰 A|設備編號盤點
為教學用水溫、pH、溶氧設備定義編號規格,找出重複、缺號與格式錯誤項目。
挑戰 B|逐字稿主題標記
在已授權逐字稿中搜尋「白馬、烏龜、姻緣花」出現位置,建立人工複核清單,不自動改寫內容。
挑戰 C|公開資料去識別檢查
以規則找出可能的手機號碼並遮罩,再由人工檢查姓名、住址與情境交叉識別風險。

十五、與生成式 AI 協作

請擔任 Python 正規表示式助教。
請為我的地方故事與感測紀錄提出正規表示式:
1. 先用自然語言說明格式;
2. 模式使用 Python 原始字串;
3. 完整欄位使用 fullmatch;
4. 提供至少五個應成功與五個應失敗案例;
5. 日期須另用 datetime 驗證真實性;
6. 個資遮罩後仍提醒人工檢查;
7. 不得擅自刪除台語逐字稿內容。
請說明每個群組與量詞的作用。
本篇小結
正規表示式擅長辨識「格式」,但不負責判斷「真實、合理與已授權」。把模式保持精簡,搭配一般 Python 驗證與人工確認,才能可靠整理場域資料。

沒有留言:

張貼留言