全網最適合入門的物件導向程式設計教程:56 Python字串與序列化-正規表示式和re模組應用

FreakStudio發表於2024-10-07

全網最適合入門的物件導向程式設計教程:56 Python 字串與序列化-正規表示式和 re 模組應用

image

摘要:

Python 的 re 模組提供了強大的正規表示式操作功能,用於在字串中搜尋、匹配、替換等,正規表示式是一種匹配字串的模式。透過正規表示式,可以輕鬆地查詢特定模式的字串片段,如匹配電子郵件地址、手機號、特定格式的日期等。

原文連結:

FreakStudio的部落格

往期推薦:

學嵌入式的你,還不會物件導向??!

全網最適合入門的物件導向程式設計教程:00 物件導向設計方法導論

全網最適合入門的物件導向程式設計教程:01 物件導向程式設計的基本概念

全網最適合入門的物件導向程式設計教程:02 類和物件的 Python 實現-使用 Python 建立類

全網最適合入門的物件導向程式設計教程:03 類和物件的 Python 實現-為自定義類新增屬性

全網最適合入門的物件導向程式設計教程:04 類和物件的Python實現-為自定義類新增方法

全網最適合入門的物件導向程式設計教程:05 類和物件的Python實現-PyCharm程式碼標籤

全網最適合入門的物件導向程式設計教程:06 類和物件的Python實現-自定義類的資料封裝

全網最適合入門的物件導向程式設計教程:07 類和物件的Python實現-型別註解

全網最適合入門的物件導向程式設計教程:08 類和物件的Python實現-@property裝飾器

全網最適合入門的物件導向程式設計教程:09 類和物件的Python實現-類之間的關係

全網最適合入門的物件導向程式設計教程:10 類和物件的Python實現-類的繼承和里氏替換原則

全網最適合入門的物件導向程式設計教程:11 類和物件的Python實現-子類呼叫父類方法

全網最適合入門的物件導向程式設計教程:12 類和物件的Python實現-Python使用logging模組輸出程式執行日誌

全網最適合入門的物件導向程式設計教程:13 類和物件的Python實現-視覺化閱讀程式碼神器Sourcetrail的安裝使用

全網最適合入門的物件導向程式設計教程:全網最適合入門的物件導向程式設計教程:14 類和物件的Python實現-類的靜態方法和類方法

全網最適合入門的物件導向程式設計教程:15 類和物件的 Python 實現-__slots__魔法方法

全網最適合入門的物件導向程式設計教程:16 類和物件的Python實現-多型、方法重寫與開閉原則

全網最適合入門的物件導向程式設計教程:17 類和物件的Python實現-鴨子型別與“file-like object“

全網最適合入門的物件導向程式設計教程:18 類和物件的Python實現-多重繼承與PyQtGraph串列埠資料繪製曲線圖

全網最適合入門的物件導向程式設計教程:19 類和物件的 Python 實現-使用 PyCharm 自動生成檔案註釋和函式註釋

全網最適合入門的物件導向程式設計教程:20 類和物件的Python實現-組合關係的實現與CSV檔案儲存

全網最適合入門的物件導向程式設計教程:21 類和物件的Python實現-多檔案的組織:模組module和包package

全網最適合入門的物件導向程式設計教程:22 類和物件的Python實現-異常和語法錯誤

全網最適合入門的物件導向程式設計教程:23 類和物件的Python實現-丟擲異常

全網最適合入門的物件導向程式設計教程:24 類和物件的Python實現-異常的捕獲與處理

全網最適合入門的物件導向程式設計教程:25 類和物件的Python實現-Python判斷輸入資料型別

全網最適合入門的物件導向程式設計教程:26 類和物件的Python實現-上下文管理器和with語句

全網最適合入門的物件導向程式設計教程:27 類和物件的Python實現-Python中異常層級與自定義異常類的實現

全網最適合入門的物件導向程式設計教程:28 類和物件的Python實現-Python程式設計原則、哲學和規範大彙總

全網最適合入門的物件導向程式設計教程:29 類和物件的Python實現-斷言與防禦性程式設計和help函式的使用

全網最適合入門的物件導向程式設計教程:30 Python的內建資料型別-object根類

全網最適合入門的物件導向程式設計教程:31 Python的內建資料型別-物件Object和型別Type

全網最適合入門的物件導向程式設計教程:32 Python的內建資料型別-類Class和例項Instance

全網最適合入門的物件導向程式設計教程:33 Python的內建資料型別-物件Object和型別Type的關係

全網最適合入門的物件導向程式設計教程:34 Python的內建資料型別-Python常用複合資料型別:元組和命名元組

全網最適合入門的物件導向程式設計教程:35 Python的內建資料型別-文件字串和__doc__屬性

全網最適合入門的物件導向程式設計教程:36 Python的內建資料型別-字典

全網最適合入門的物件導向程式設計教程:37 Python常用複合資料型別-列表和列表推導式

全網最適合入門的物件導向程式設計教程:38 Python常用複合資料型別-使用列表實現堆疊、佇列和雙端佇列

全網最適合入門的物件導向程式設計教程:39 Python常用複合資料型別-集合

全網最適合入門的物件導向程式設計教程:40 Python常用複合資料型別-列舉和enum模組的使用

全網最適合入門的物件導向程式設計教程:41 Python常用複合資料型別-佇列(FIFO、LIFO、優先順序佇列、雙端佇列和環形佇列)

全網最適合入門的物件導向程式設計教程:42 Python常用複合資料型別-collections容器資料型別

全網最適合入門的物件導向程式設計教程:43 Python常用複合資料型別-擴充套件內建資料型別

全網最適合入門的物件導向程式設計教程:44 Python內建函式與魔法方法-重寫內建型別的魔法方法

全網最適合入門的物件導向程式設計教程:45 Python實現常見資料結構-連結串列、樹、雜湊表、圖和堆

全網最適合入門的物件導向程式設計教程:46 Python函式方法與介面-函式與事件驅動框架

全網最適合入門的物件導向程式設計教程:47 Python函式方法與介面-回撥函式Callback

全網最適合入門的物件導向程式設計教程:48 Python函式方法與介面-位置引數、預設引數、可變引數和關鍵字引數

全網最適合入門的物件導向程式設計教程:49 Python函式方法與介面-函式與方法的區別和lamda匿名函式

全網最適合入門的物件導向程式設計教程:50 Python函式方法與介面-介面和抽象基類

全網最適合入門的物件導向程式設計教程:51 Python函式方法與介面-使用Zope實現介面

全網最適合入門的物件導向程式設計教程:52 Python函式方法與介面-Protocol協議與介面

全網最適合入門的物件導向程式設計教程:53 Python字串與序列化-字串與字元編碼

全網最適合入門的物件導向程式設計教程:54 Python字串與序列化-字串格式化與format方法

全網最適合入門的物件導向程式設計教程:55 Python字串與序列化-位元組序列型別和可變位元組字串

更多精彩內容可看:

給你的 Python 加加速:一文速通 Python 平行計算

一文搞懂 CM3 微控制器除錯原理

肝了半個月,嵌入式技術棧大彙總出爐

電子計算機類比賽的“武林秘籍”

一個MicroPython的開源專案集錦:awesome-micropython,包含各個方面的Micropython工具庫

Avnet ZUBoard 1CG開發板—深度學習新選擇

SenseCraft 部署模型到Grove Vision AI V2影像處理模組

文件和程式碼獲取:

可訪問如下連結進行對文件下載:

https://github.com/leezisheng/Doc

image

本文件主要介紹如何使用 Python 進行物件導向程式設計,需要讀者對 Python 語法和微控制器開發具有基本瞭解。相比其他講解 Python 物件導向程式設計的部落格或書籍而言,本文件更加詳細、側重於嵌入式上位機應用,以上位機和下位機的常見串列埠資料收發、資料處理、動態圖繪製等為應用例項,同時使用 Sourcetrail 程式碼軟體對程式碼進行視覺化閱讀便於讀者理解。

相關示例程式碼獲取連結如下:https://github.com/leezisheng/Python-OOP-Demo

正文

正規表示式簡介

我們常常需要判斷一個給定字串的合法性,比如一串數字是否是電話號碼;一串字元是否是合法的 URL、Email 地址;使用者輸入的密碼是否滿足複雜度要求等等。

如果我們為每一種格式都定義一個判定函式,首先這種定義可能很複雜,比如電話號碼可以為座機時表示為 010-12345678 ,也可以表示為 0510-12345678, 還可以是手機號 13800000000。這樣程式碼的邏輯複雜度就線性增加。其次我們定義的函式功能很難重用,匹配 A 的不能匹配 B。能否有一個萬能的函式,只要我們傳入特定的引數就能實現我們特定的字元匹配需求呢?答案是肯定的。

在現實世界中,大部分程式語言透過正規表示式來處理字串解析。正規表示式是一種被用於從文字中檢索符合某些特定模式的文字,它們依賴特殊符號來匹配未知字串。它可用於解決一個常見的問題:給定一個字串,確定它是否能夠匹配某個給定的模式,以及可以收集包含相關資訊的子字串。

正規表示式中有兩個概念,一個字串包含若干個字元,每個字元在記憶體中都有對應的二進位制編碼,以及字元先後關係構成的位置,比如字串開始位置和結束位置如圖所示表示為 ps 和 pe。包含 N 個字元的字串有 N+1 個位置,位置不佔用記憶體,僅用於匹配定位。

image

正規表示式使用一些特殊字元(通常以\開頭,\是跳脫字元)來表示特定的一類字符集(比如數字 0-9)和字元位置(比如字串開始位置)。它們被稱為元字元(metacharacter)。元字元和其他控制字元構成的表示式被稱為匹配模式(pattern)。匹配過程中有一個位置指標,開始總是指向位置 ps,根據匹配模式每匹配一次,就將指標移動到匹配字元的後序位置,並嘗試在每一個位置上進行模式匹配,直至嘗試過 pe 位置後匹配過程結束。‘.’在正規表示式中表示匹配除換行符\n 外的所有字元,如果要匹配‘.’自身,就要使用‘\ .’的形式。由於 Python 字串本身也採用\作為轉義符,所以正規表示式字串前要加 r,表示原始輸入,以防轉義衝突。

在正規表示式中,如果直接給出字元,就是精確匹配。用’\d’可以匹配一個數字,’\w’可以匹配一個字母或數字,所以:

'00\d'可以匹配'007',但無法匹配'00A';
'\d\d\d'可以匹配'010';
'\w\w\d'可以匹配'py3';

'.'可以匹配任意字元,所以:

'py.'可以匹配'pyc'、'pyo'、'py!'等等。

要匹配變長的字元,在正規表示式中,用'*'表示任意個字元(包括 0 個),用'+'表示至少一個字元,用'?'表示 0 個或 1 個字元,用'{n}'表示 n 個字元,用'{n,m}'表示 n-m 個字元:

\d{3}\s+\d{3,8}
1. \d{3}表示匹配3個數字,例如'010';
2. \s可以匹配一個空格(也包括Tab等空白符),所以\s+表示至少有一個空格,例如匹配' ','  '等;
3. \d{3,8}表示3-8個數字,例如'1234567'。
綜合起來,上面的正規表示式可以匹配以任意個空格隔開的帶區號的電話號碼。

如果要匹配'010-12345'這樣的號碼呢?由於'-'是特殊字元,在正規表示式中,要用''轉義,所以,上面的正則是\d{3}-\d{3,8}。

要做更精確地匹配,可以用‘[]’表示範圍,比如:

[0-9a-zA-Z\_]可以匹配一個數字、字母或者下劃線;
[0-9a-zA-Z\__]+可以匹配至少由一個數字、字母或者下劃線組成的字串,比如'a100','0__Z','Py3000'等等;
[a-zA-Z\_][0-9a-zA-Z\_]*可以匹配由字母或下劃線開頭,後接任意個由一個數字、字母或者下劃線組成的字串,也就是Python合法的變數;
[a-zA-Z\_][0-9a-zA-Z\_]{0, 19}更精確地限制了變數的長度是1-20個字元(前面1個字元+後面最多19個字元)。

’A|B‘可以匹配 A 或 B:

(P|p)ython可以匹配'Python'或者'python'。

其他特殊符號還有:

^表示行的開頭,^\d表示必須以數字開頭。
$表示行的結束,\d$表示必須以數字結束。
?表示對它前面的正則式匹配0到1次重複,ab? 會匹配 'a' 或者 'ab'。

更多相關表示式使用方法可以檢視如下連結:

https://tool.oschina.net/uploads/apidocs/jquery/regexp.html

https://docs.python.org/zh-cn/3/howto/regex.html

https://pythonhowto.readthedocs.io/zh-cn/latest/regular.html

https://www.runoob.com/regexp/regexp-syntax.html

image

正規表示式的應用與 re 模組

Python 標準庫中的正規表示式模組被稱為 re。我們匯入它之後建立一個搜尋字串和需要搜尋的模式。

在下面的例子中,我們使用 re.compile()建立一個已編譯正規表示式物件 re.Pattern,並使用 Pattern.match(string[, pos[, endpos]])方法:如果字串開頭的零個或多個字元與此正規表示式匹配,則返回相應的 Match。如果字串與模式不匹配則返回 None。

image

image

image

由於要搜尋的字串和模式是相匹配的,條件判斷會透過並且 print 語句會執行。示例程式碼如下:

import re
search_string = "hello world"
pattern = re.compile("hello world")
match = re.match(pattern, search_string)
if match:
    print("regex matches")
    print(match)
    print(match.__doc__)

執行結果如下:

image

由於 match 函式是從字串的開頭開始匹配模式,因此,如果模式改為"ello world",將無法匹配。

import re
search_string = "hello world"
pattern = re.compile("ello world")
match = re.match(pattern, search_string)
if match:
    print("regex matches")
    print(match)
    print(match.__doc__)
else:
    print("no match")

執行結果如下:

image

不同的是,一旦發現匹配,解析器將會立即停止搜尋,因此模式"hello wo"也會成功匹配。如果我們只想要幾個特定的字元被匹配可以將幾個字元放到一個方括號中,以匹配其中的任何一個字元。因此,如果遇到一個[abc]的正規表示式模式字串,我們就知道這 5 個字元(包括兩個方括號)只會匹配搜尋字串中的一個字元,而且,這個字元只能是 a、b、c 中的一個。示例程式碼如下:

import re

search_string = "hello world"
pattern = re.compile("hell[lpo] world")
match = re.match(pattern, search_string)

if match:
    print("regex matches")
    print(match)

執行結果如下,實際上,這些方括號應該被命名為字符集合,不過它們更常指代字元類。

image

通常,我們想要用更多字元,但逐個輸入既單調又容易出錯。幸運的是,正規表示式的設計者考慮到了這一點,並提供了簡寫方式。短畫線符號可以代表一個範圍。如果你想要匹配“所有小寫字母”或“所有數字”,這就非常有用了,例如:

import re
search_string = "hello world"
pattern = re.compile("hell[a-z] world")
match = re.match(pattern, search_string)
if match:
    print("regex matches")
    print(match)
    print(match.__doc__)
else:
    print("no match")

執行結果如下:

image

正如前面我們說到的,我們也可以用反斜槓跳脫字元來匹配一些特殊符號,如‘.’、‘(’等,示例程式碼如下:

import re

search_string = "0.05"
pattern = re.compile("0\\.[0-9][0-9]")
match = re.match(pattern, search_string)
if match:
    print("regex matches")
    print(match)
    print(match.__doc__)

執行結果如下:

image

這裡,需要注意的是我們傳遞給 re.compile() 的結果字串必須是轉義兩個反斜槓“\section”,如果要匹配兩個反斜槓的相關字元”\section“,則需要用四個反斜槓'\\'。也可以在 Python 正規表示式前加入 r 表示原生字串,r 字元宣告瞭引號中的內容表示該內容的原始含義,避免了多次轉義造成的反斜槓困擾。

re 模組的 re.Pattern 類還具有如下方法,我們接下來進行嘗試:

image

我們可以用 search 方法對字串中的正規表示式進行搜尋,它將返回與正規表示式產生匹配的第一個位置,具體用法如下:

image

示例程式碼如下:

import re

pattern = re.compile("o")
locate = pattern.search("dog")
print(locate)

image

在正規表示式的第一個例子中,我們將整個正規表示式與字串相匹配,實際上我們應該使用 fullmatch 函式而非 match 函式:

image

image

image

import re

pattern = re.compile("o[gh]")
_# Pattern.fullmatch(string[, pos[, endpos]])_
_# 第二個引數 pos 給出了字串中開始搜尋的位置索引_
_# 第三個引數endpos限定了字串搜尋的結束_
_# 從 pos 到 endpos - 1 的字元會被匹配,在本例中,為‘ogg’_
match = pattern.fullmatch("doggie", 1, 3)

print(match)
_# 列印匹配到的子串_
print(match.group())
_# 列印匹配到的字元的起始位置_
print(match.pos)

image

你可以看到在這個例子中,我們使用 group 方法提取了子串。group(0)是與整個正規表示式相匹配的字串,group(1)、group(2)……表示第 1、2、……個子串。

我們也可以利用正規表示式來切分字串,該方法比用固定的字元更靈活。

image

示例程式如下:

import re

str       = 'a b   c'
_# 普通split方法拆分字串_
str_split = str.split(' ')
_# 輸出['a', 'b', '', '', 'c']_
_# 無法識別連續的空格_
print(str_split)

_# 使用re庫中的split方法_
str_re_split = re.split(r'\s+', str)
print(str_re_split)

執行結果如下:

image

其中,\s 表示匹配任意的空白字元,+ 表示匹配前面的字元一次或多次,這個正規表示式可以匹配一個或多個空白字元,包括空格、製表符、換行符等。

至今為止,我們已經成功匹配了大部分已知長度的字串。然而,在多數實際場景中,我們並不清楚需要匹配的確切字元數量。這時,正規表示式便能發揮巨大作用。我們可以透過對模式進行微調,新增一個或多個標點符號,從而實現對多個字元的有效匹配。這種靈活性使得正規表示式在處理複雜字串匹配問題時具有顯著優勢。

星號()意味著前一種模式可以出現零次或多次。將星號和其他匹配多個字元的符號組合起來就會得到更有趣的結果,例如,‘.’將會匹配任何字串,而’[a-z]*‘將會匹配任意數量的小寫字母,包括空字串;加號(+)和星號的行為類似,只不過它要求之前一種模式出現的次數必須是一次或多次,而不像星號一樣是可選的;問號(?)要求前一種模式只能出現零次或一次,不能更多。

常見示例如下:

'0.4' matches pattern '\d+\.\d+' 
'1.002' matches pattern '\d+\.\d+' 
'1.' does not match pattern '\d+\.\d+' 
'1%' matches pattern '\d?\d%' 
'99%' matches pattern '\d?\d%' 
'999%' does not match pattern '\d?\d%'

接下來,我們用一個示例更加深入地應用前面說明的相關知識點。一般來說,郵箱的使用者名稱和域名都必須至少包含兩個字元,並且只能使用字母、數字、點、下劃線、百分號、加號或減號作為字元。使用者名稱和域名中間有 @ 符號,域名格式為:xxx.com。

在下例中,我們來編寫一個正規表示式,用於匹配有效的郵箱地址:

^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$

其中:

  • (1) ^ 表示字串的開頭;
  • (2) [a-zA-Z0-9._%+-]+ 匹配一個或多個字母、數字、點、下劃線、百分號、加號或減號,這些字元通常出現在電子郵件地址的使用者名稱中;
  • (3) @ 匹配 @ 符號;
  • (4) [a-zA-Z0-9.-]+ 匹配一個或多個字母、數字、點或減號,這些字元通常出現在電子郵件地址的域名中;
  • (5) . 匹配一個點符號;
  • (6) [a-zA-Z]{2,} 匹配兩個或更多個字母,這些字元通常出現在電子郵件地址的頂級域名中(如 .com、.org 等);
  • (7) $ 表示字串的結尾。

該正規表示式可以匹配類似於“example@example.com”這樣的電子郵件地址。

在下面程式碼中,我們定義了一個複雜的正規表示式,用於匹配有效的郵箱地址。然後,我們定義了一個列表 emails,其中包含了一些郵箱地址。使用 search()方法逐個匹配郵箱地址,並輸出結果。示例程式碼如下:

import re

_# 定義正規表示式_
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'

_# 定義目標字串_
emails = [
    "user@example.com",
    "user-1@example.co.uk",
    "user.name@example.com",
    "user@sub.example.co.in",
    "invalid_email"
]


_# 使用search()方法匹配有效的郵箱地址_
for email in emails:
    match = re.search(pattern, email)
    if match:
        print("有效的郵箱地址:", match.group())
    else:
        print("無效的郵箱地址:",email)

執行結果如下:

image

我們也可以用 findall 函式查詢匹配模式的所有非重疊部分的結果,而不是像 search 函式一樣只有第一個。基本上它首先找到第一個匹配,然後從該結果的結尾處重新設定字串,再進行下一個搜尋。

image

它不返回匹配物件,而是返回一個匹配字串或元組的列表。返回結果的型別依賴於正規表示式中括號組合的數量:如果模式中沒有組合,findall 將會返回字串列表,其中每個值都是源字串中與模式匹配的子字串;如果模式中只有一個組合,findall 將會返回一個字串列表,其中每個值都是該組中的內容;如果模式中存在多個組合,findall 將會返回一個元組列表,其中按照順序每個元組包含的是一個組合中匹配到的結果。

import re
_# \b 表示單詞邊界,[a-z] 表示任意一個小寫字母_
_# 可以匹配以f開頭,後面跟著零個或多個小寫字母的字串。_
print(re.findall(r'\bf[a-z]*', 'which foot or hand fell fastest'))

執行結果如下:

image

最後需要特別指出的是,正則匹配預設是貪婪匹配,也就是匹配儘可能多的字元。舉例如下,匹配出數字後面的 0:

import re
_# 由於\d+採用貪婪匹配,直接把後面的0全部匹配了,結果0*只能匹配空字串了_
print(re.match(r'^(\d+)(0*)$', '102300').groups())
_# 加個?就可以讓\d+採用非貪婪匹配:_
print(re.match(r'^(\d+?)(0*)$', '102300').groups())

執行結果如下:

image

image

相關文章