2021年8月11日 星期三

Python 學習筆記 : 用字典來做字母頻率統計排序

今天接到學姊來電說女兒在學 Python, 想問了我幾個問題, 其中一個是跟計算語言學有關的, 要統計一段文本中用到的字母出現之字數 (不分大小寫), Python 的字典資料類型正好可以派上用場, 例如下面這個字串 : 

"Hello World"

字母 L 出現三次, O 出現 2 次, W 出現一次,  H, E, D, R 都只出現 1 次, 可以用 {'h': 1, 'e': 1, 'l': 3, 'o': 2, 'd': 1, 'r': 1, 'w': 1} 來表示統計結果. 

首先定義一個空字典來儲存統計結果, 因為不分大小寫, 因此可先呼叫字串的 lower() 方法將所有字元都轉成小寫 (也可轉成大寫來處理), 然後用迴圈走訪字串中的每一個字元, 並判斷此字元是否已在字典中, 不是的話就以此字元為鍵, 1 為值於字典中新增項目, 是的話就將該項目的值增量 1, 程式如下 :

s="Hello World"
d={}
for c in s.lower():
    if c not in d:
        d[c]=1
    else:
        d[c] += 1
print(d)

最後印出字典內容如下 :

{'h': 1, 'e': 1, 'l': 3, 'o': 2, ' ': 1, 'w': 1, 'r': 1, 'd': 1}

不過美中不足的是, 空白字元也被列入統計, 因為它也是一個字元. 其實, 如果是更複雜的文本, 則包含標點與跳行符號都會列入統計, 例如 :

s="""Hi, my name is Ruby.
Welcome to Taiwan!"""
d={}
for c in s.lower():
    if c not in d:
        d[c]=1
    else:
        d[c] += 1
print(d)

結果如下 :

{'h': 1, 'i': 3, ',': 1, ' ': 6, 'm': 3, 'y': 2, 'n': 2, 'a': 3, 'e': 3, 's': 1, 'r': 1, 'u': 1, 'b': 1, '.': 1, '\n': 1, 'w': 2, 'l': 1, 'c': 1, 'o': 2, 't': 2, '!': 1}

可見字典中包含了空格, 句點, 逗號, 驚嘆號, 以及跳行符號, 這些可用字串的 replace() 方法來去除, 例如 : 

s="""Hi, my name is Ruby.
Welcome to Taiwan!"""
s=s.replace(" ", "")
s=s.replace(",", "")
s=s.replace(".", "")
s=s.replace("\n", "")
s=s.replace("!", "")
s=s.replace("?", "")
d={}
for c in s.lower():
    if c not in d:
        d[c]=1
    else:
        d[c] += 1
print(d)

執行結果 :

{'h': 1, 'i': 3, 'm': 3, 'y': 2, 'n': 2, 'a': 3, 'e': 3, 's': 1, 'r': 1, 'u': 1, 'b': 1, 'w': 2, 'l': 1, 'c': 1, 'o': 2, 't': 2}

但用 replace() 實在太麻煩, 比較俐落的方法是使用正規式模組 re, 利用正規式 "\W" 可以濾掉非英文字母, 例如 : 

import re
s="""Hi, my name is Ruby.
Welcome to Taiwan!"""
s=re.sub("\W", "", s)
d={}
for c in s.lower():
    if c not in d:
        d[c]=1
    else:
        d[c] += 1
print(d)

結果如下 : 

{'h': 1, 'i': 3, 'm': 3, 'y': 2, 'n': 2, 'a': 3, 'e': 3, 's': 1, 'r': 1, 'u': 1, 'b': 1, 'w': 2, 'l': 1, 'c': 1, 'o': 2, 't': 2}

下面是較長文本的測試範例 : 

import re
s="""Fires have raged in Greece, Northern Macedonia,
Turkey, Algeria, Italy and Cyprus in recent days.
Firefighters in the US, too, have been battling blazes
across 15 states as the Dixie Fire, California's second
largest ever, continues to grow. Last month, parts of
Siberia and British Columbia both experienced the largest
fires in years."""
s=re.sub("\W", "", s)
d={}
for c in s.lower():
    if c not in d:
        d[c]=1
    else:
        d[c] += 1
print(d)

結果如下 : 

{'f': 7, 'i': 24, 'r': 24, 'e': 37, 's': 22, 'h': 10, 'a': 25, 'v': 3, 'g': 8, 'd': 8, 'n': 18, 'c': 10, 'o': 14, 't': 22, 'm': 3, 'u': 5, 'k': 1, 'y': 5, 'l': 9, 'p': 3, 'b': 7, 'z': 1, '1': 1, '5': 1, 'x': 2, 'w': 1}

如果要從字典統計結果中找出出現最多次的字母 (鍵) 及其次數 (值) 該怎麼做? 使用 Python 內建函數並傳入字典將會傳回 key 中最大的, 而非值最大的, 所以只要文本中含有字母 z, 則 max(d) 一定傳回 'z', 因為其 ASCII 值是英文字母中最大的. 例如 : 

import re
s="""Many web developers are unaware of how SQL queries can be tampered with, and assume that an SQL query is a trusted command. It means that SQL queries are able to circumvent access controls, thereby bypassing standard authentication and authorization checks, and sometimes SQL queries even may allow access to host operating system level commands.

Direct SQL Command Injection is a technique where an attacker creates or alters existing SQL commands to expose hidden data, or to override valuable ones, or even to execute dangerous system level commands on the database host. This is accomplished by the application taking user input and combining it with static parameters to build an SQL query. The following examples are based on true stories, unfortunately."""
s=re.sub("\W", "", s)
d={}
for c in s.lower():
    if c not in d:
        d[c]=1
    else:
        d[c] += 1
print(d)
print("出現最多次的字母=", max(d), d[max(d)], "次")   # max(d) 傳回的是最大的鍵

結果如下 : 

{'f': 7, 'i': 24, 'r': 24, 'e': 37, 's': 22, 'h': 10, 'a': 25, 'v': 3, 'g': 8, 'd': 8, 'n': 18, 'c': 10, 'o': 14, 't': 22, 'm': 3, 'u': 5, 'k': 1, 'y': 5, 'l': 9, 'p': 3, 'b': 7, 'z': 1, '1': 1, '5': 1, 'x': 2, 'w': 1}
出現最多次的字母= z 1 次

正確的做法是在呼叫 max() 時必須傳入 key 參數, 其值為呼叫字典的 get() 方法, 這樣 max() 就會傳回值最大的鍵了, 例如 :

import re
s="""Fires have raged in Greece, Northern Macedonia,
Turkey, Algeria, Italy and Cyprus in recent days.
Firefighters in the US, too, have been battling blazes
across 15 states as the Dixie Fire, California's second
largest ever, continues to grow. Last month, parts of
Siberia and British Columbia both experienced the largest
fires in years."""
s=re.sub("\W", "", s)
d={}
for c in s.lower():
    if c not in d:
        d[c]=1
    else:
        d[c] += 1
print(d)
max_key=max(d, key=d.get)     
print("出現最多次的字母=", max_key, d[max_key], "次")

結果如下 :

{'f': 7, 'i': 24, 'r': 24, 'e': 37, 's': 22, 'h': 10, 'a': 25, 'v': 3, 'g': 8, 'd': 8, 'n': 18, 'c': 10, 'o': 14, 't': 22, 'm': 3, 'u': 5, 'k': 1, 'y': 5, 'l': 9, 'p': 3, 'b': 7, 'z': 1, '1': 1, '5': 1, 'x': 2, 'w': 1}
出現最多次的字母= e 37 次

字典的項目或元素本質是無序的, 要如何讓它們排序呢?  作法將其轉成可排序類型, 可先呼叫字典的 items() 方法, 它會傳回鍵值對元組的串列, 這樣就變成可排序類型了, 將此串列傳給內建函數 sorted() 並以 key 參數呼叫一個匿名函數來指定依照  key 還是 value 來排序, sorted() 函數的 API 如下 :

sorted(d.items() [, key=lambda d: d[0], reverse=False])    # 依 key 排序
sorted(d.items() [, key=lambda d: d[1], reverse=False])    # 依 value 排序

其中 key 與 reverse 均為選用參數, key 用來指定呼叫甚麼函數來排序, 此處是傳入字典 d, 並傳回 d[0] 或 d[1] 做為排序的對象. reverse 用來指定是否要用遞減方式排序, 預設是遞增排序.

參考 : 


例如 : 

import re
s="""Fires have raged in Greece, Northern Macedonia,
Turkey, Algeria, Italy and Cyprus in recent days.
Firefighters in the US, too, have been battling blazes
across 15 states as the Dixie Fire, California's second
largest ever, continues to grow. Last month, parts of
Siberia and British Columbia both experienced the largest
fires in years."""
s=re.sub("\W", "", s)
d={}
for c in s.lower():
    if c not in d:
        d[c]=1
    else:
        d[c] += 1
print(d)
print("依 key 遞增排序 :")
print(sorted(d.items(), key=lambda d: d[0]))
print("依 key 遞減排序 :")
print(sorted(d.items(), key=lambda d: d[0], reverse=True))
print("依 value 遞增排序 :")
print(sorted(d.items(), key=lambda d: d[1]))
print("依 value 遞減排序 :")
ds=sorted(d.items(), key=lambda d: d[1], reverse=True)
print(ds)
print("出現最多次的字母=", ds[0][0], ds[0][1], "次")

結果如下 : 

{'f': 7, 'i': 24, 'r': 24, 'e': 37, 's': 22, 'h': 10, 'a': 25, 'v': 3, 'g': 8, 'd': 8, 'n': 18, 'c': 10, 'o': 14, 't': 22, 'm': 3, 'u': 5, 'k': 1, 'y': 5, 'l': 9, 'p': 3, 'b': 7, 'z': 1, '1': 1, '5': 1, 'x': 2, 'w': 1}
依 key 遞增排序 :
[('1', 1), ('5', 1), ('a', 25), ('b', 7), ('c', 10), ('d', 8), ('e', 37), ('f', 7), ('g', 8), ('h', 10), ('i', 24), ('k', 1), ('l', 9), ('m', 3), ('n', 18), ('o', 14), ('p', 3), ('r', 24), ('s', 22), ('t', 22), ('u', 5), ('v', 3), ('w', 1), ('x', 2), ('y', 5), ('z', 1)]
依 key 遞減排序 :
[('z', 1), ('y', 5), ('x', 2), ('w', 1), ('v', 3), ('u', 5), ('t', 22), ('s', 22), ('r', 24), ('p', 3), ('o', 14), ('n', 18), ('m', 3), ('l', 9), ('k', 1), ('i', 24), ('h', 10), ('g', 8), ('f', 7), ('e', 37), ('d', 8), ('c', 10), ('b', 7), ('a', 25), ('5', 1), ('1', 1)]
依 value 遞增排序 :
[('k', 1), ('z', 1), ('1', 1), ('5', 1), ('w', 1), ('x', 2), ('v', 3), ('m', 3), ('p', 3), ('u', 5), ('y', 5), ('f', 7), ('b', 7), ('g', 8), ('d', 8), ('l', 9), ('h', 10), ('c', 10), ('o', 14), ('n', 18), ('s', 22), ('t', 22), ('i', 24), ('r', 24), ('a', 25), ('e', 37)]
依 value 遞減排序 :
[('e', 37), ('a', 25), ('i', 24), ('r', 24), ('s', 22), ('t', 22), ('n', 18), ('o', 14), ('h', 10), ('c', 10), ('l', 9), ('g', 8), ('d', 8), ('f', 7), ('b', 7), ('u', 5), ('y', 5), ('v', 3), ('m', 3), ('p', 3), ('x', 2), ('k', 1), ('z', 1), ('1', 1), ('5', 1), ('w', 1)]
出現最多次的字母= e 37 次

可見先轉成鍵值對元組之串列, 經過按值遞減排序, 取出第一個元素也可以得到出現率最高的字母與其次數. 

參考 :


沒有留言 :