我不喜歡這個想法,仔細思考一下的話,這根本就糟糕透了。如果你不喜歡一首歌,那你就不應該一直去聽那首歌。如果你喜歡一首歌,那你就應該要喜歡它的歌詞,應該要牢牢地記住它的歌詞,而不是胡亂的去惡搞它。
~節錄自《賴田捕手》第九章
之前有先介紹過字串物件的基本操作了。其實字串在程式中的應用相當多,更別提我們想做的可是接收跟發送字串的聊天機器人,對於字串的各種使用方式跟相關的套件,我們要有更多了解才行。因此在我們把 Python 的基本物件都介紹完了以後,今天就回過頭來再仔細端詳一下字串物件。
幾年前有一首歌,短短的,像個繞口令一樣,才用沒幾個字,也沒什麼內容,卻紅遍了大街小巷。是的,我說的就是 PPAP➀。PPAP 之所以能夠這麼成功,快速的攻佔各大社群媒體的版面,成為大家嚷嚷上口的洗腦神曲,有人認為是因為表演者讓人噴飯的肢體語言、表演技巧、或是刻意走庸俗華麗路線的服裝設計。但我想,影響一首歌的命運,最重要的還是旋律、節奏跟歌詞吧。今天我們不講草泥馬,就來談談 PPAP 的歌詞吧。
In [1]: import numpy as np
import re
首先,載入兩個等等要用到的套件。re
是 Python 的標準套件,所以這次我們不需要再幫環境安裝。
In [2]: PPAP = '''I have a pen
I have an apple
Ah
Apple pen
I have a pen
I have pineapple
Ah
Pineapple pen
Apple pen
Pineapple pen
Ah
Pen Pie Pineapple Apple Pen
Pen Pie Pineapple Apple Pen'''
我們用'''可以換行的字串'''
做出可以換行的字串,把 PPAP 的歌詞填上去,然後貼上PPAP
的標籤。好了,我們來看看 PPAP 的歌詞裡用了哪些關鍵的字眼:
In [3]: 'pen' in PPAP
Out[3]: True
In [4]: 'pea' in PPAP
Out[4]: False
原來沒有用到'pea'
啊。
pea /pi/ (noun) 豌豆
~節錄自《民明書房大字典》
那'pen'
放在PPAP的哪裡呢?
In [5]: PPAP.index('pen')
Out[5]: 9
In [6]: PPAP.index('pea')
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-8-9a91766deac0> in <module>
----> 1 PPAP.index('pea')
ValueError: substring not found
不好,都忘了沒有'pea'
啊。
到這為止的操作都很熟悉,可以來點不一樣的嗎?好的,馬上就到。剛才的例子裡,用index()
在字串中尋找特定目標,會找到第一個相符合的目標,並且回傳位置。但如果整個字串都沒有相符合的目標,就會產生錯誤。在執行程式的過程中,有些時候我們不希望程式出錯。那麼還有其他方法嗎?
In [7]: PPAP.find('pen')
Out[7]: 9
In [8]: PPAP.find('pea')
Out[8]: -1
屬於字串的方法find()
,會在字串中尋找你指定的目標,找到第一個符合的目標後,回傳該目標的位置。不過,當字串中不存在指定目標時,會回傳-1
,而不會造成程式出錯。如果要找最後一個呢?
In [9]: PPAP.rfind('pen')
Out[9]: 109
說真的,PPAP 到底唱了幾次 pen 啊?我聽起來像是 10 次,Python 聽起來呢?
In [10]: PPAP.count('pen')
Out[10]: 6
我們的 Python 朋友只聽到 6 次,怎麼回事呢。檢視一下歌詞,原來我們的歌詞'pen'
、'Pen'
並存,有'pen'
也有'Pen'
。Python 很正直的,大寫就是大寫,小寫就是小寫,豈容胡亂混淆。怎麼辦呢?這邊介紹一個方法,叫做lower()
,不囉嗦,看程式碼:
In [11]: print(PPAP.lower())
Out[11]: i have a pen
i have an apple
ah
apple pen
i have a pen
i have pineapple
ah
pineapple pen
apple pen
pineapple pen
ah
pen pie pineapple apple pen
pen pie pineapple apple pen
喔,我們利用lower()
顯示出全部都是小寫的 PPAP 了!為什麼我說顯示呢?讓我們再回頭看看 PPAP 吧:
In [12]: PPAP[0]
Out[12]: I have a pen
I have an apple
Ah
Apple pen
I have a pen
I have pineapple
Ah
Pineapple pen
Apple pen
Pineapple pen
Ah
Pen Pie Pineapple Apple Pen
Pen Pie Pineapple Apple Pen
還是Pen
啊。原來如此,原來是這樣。lower()
的作用是回傳一個小寫版本的PPAP
,並不是改變PPAP
本身。如果後續的運算還有需要用到的話,要嘛不是再呼叫一次lower()
,不然就是要記得把PPAP.lower()
用標籤貼起來。
In [13]: PPAP.lower().count('pen')
Out[13]: 10
這次 Python 聽到的次數跟我聽到的次數一樣了!短短一首歌裡面就唱了 10 次'pen'
,這種像是繞口令一般的節奏,更別說是最後一句了,那根本是經典。乍看之下像是亂念一通,其實是經過精心安排的呢。不然你亂念一通給我看?
In [14]: ' '.join(np.random.permutation(PPAP.split('\n')[-1].split(' ')))
Out[14]: 'Pie Pen Pen Pineapple Apple'
念起來很彆扭吧?改成這樣大概就沒機會紅了。等等等等,沒人問你念起來怎麼樣,我比較想知道 Python 程式碼到底在寫些什麼。
' '.join(np.random.permutation(PPAP.split('\n')[-1].split(' ')))
numpy
的方法。split()
,把字串切開的方式,用法是要切開的字串.split(切口處)
。切完了之後,會拿到由片段字串組成的清單。來用用看吧?In [15]: 要切開的字串 = '煞☆氣☆的☆中☆二'
切口處 = '☆'
要切開的字串.split(切口處)
Out[15]: ['煞', '氣', '的', '中', '二']
join()
,顧名思義,把字串連在一起的方式,用法是膠水.join(要黏合的字串清單)
。所謂要黏合的字串清單
,指的是由一個一個字串組成的清單,透過膠水
,黏在一起後傳回一個完整的字串。In [16]: 膠水 = ' ㊣ '
要黏合的字串清單 = ['煞', '氣', '的', '中', '二']
膠水.join(要黏合的字串清單)
Out[16]: '煞 ㊣ 氣 ㊣ 的 ㊣ 中 ㊣ 二'
np.random.permutation()
則是有順序的可屬數物件打亂、隨機排列的方法。In [17]: def 洗牌():
一條龍 = [i for i in range(1, 14)]
return np.random.permutation(一條龍)
In [18]: print(洗牌())
type(洗牌())
[ 5 11 7 10 12 2 1 3 13 4 6 9 8]
Out[18]: numpy.ndarray
而該方法會回傳一個屬於numpy
的ndarray
物件。我懂,雖然長得很像清單(list),功能方面也蠻像清單的,但兩個是稍微不同的物件。既然都學會join()
了,那就來現學現賣吧?
In [19]: ' ♠ '.join(洗牌())
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-18-f01480c44660> in <module>
----> 1 ' ♠ '.join(洗牌())
TypeError: sequence item 0: expected str instance, numpy.int32 found
不行。因為洗牌()
傳回來的,不是由字串組成的清單,而是由整數物件組成的ndarray
,大意了。那改成這樣呢?
In [20]: ' ♠ '.join(洗牌().astype(str))
Out[20]: '7 ♠ 13 ♠ 8 ♠ 2 ♠ 10 ♠ 5 ♠ 12 ♠ 1 ♠ 4 ♠ 11 ♠ 9 ♠ 6 ♠ 3'
可以了!
好啦,重新再剝一次洋蔥囉。
' '.join(np.random.permutation(PPAP.split('\n')[-1].split(' ')))
PPAP.split('\n')[-1].split(' ')))
我把PPAP
用換行符號(\n
)切開之後,選擇最後一行文字,再用空白鍵切開,得到['Pen', 'Pie', 'Pineapple', 'Apple', 'Pen']
這樣的清單。下一步,用np.random.permutation()
隨機打亂,最後再用空白鍵將打亂的字串連接再一起。耶,成功剝開洋蔥囉! 上面的程式碼寫了那麼多,我們可以用count()
知道有 10 個'pen'
,也知道第一個'pen'
跟最後一個'pen'
所在的位置。那其他 8 個呢?
In [21]: re.findall('pen', PPAP.lower())
Out[21]: ['pen', 'pen', 'pen', 'pen', 'pen', 'pen', 'pen', 'pen', 'pen', 'pen']
喔!全都列出來給我了。不過到底在哪裡呢?先不急,我們來挑戰個難一點的。我們這次來找'apple'
。我們聽到'apple'
有 5 個,Python 聽到幾個呢?
In [22]: re.findall('apple', PPAP.lower())
Out[22]: ['apple', 'apple', 'apple', 'apple', 'apple', 'apple', 'apple', 'apple', 'apple', 'apple']
居然有 10 個,一定是跟'pineapple'
搞混了。那我們在前面加一個空白鍵' apple'
這樣呢?
In [23]: re.findall(' apple', PPAP.lower())
Out[23]: ['apple', 'apple', 'apple']
只剩下 3 個。因為其中有一個'apple'
是接在換行符號後面。怎麼辦呢。這就是為什麼我要在這個地方介紹正常表達(Regular Expressions)的原因。正常表達提供了對字串更精確的控制,除了一般的'\n'
、'\t'
用來表示換行符號跟表格鍵之外,還有如表一所列更多的選擇。
表一、特殊字元
程式碼 | 代表 |
---|---|
'\d' |
數字 (a single digit) |
'\D' |
非數字 (a single non-digit) |
'\w' |
字母或數字 (an alphanumeric character) |
'\W' |
非字母非數字 (a non-alphanumeric character) |
'\s' |
空白鍵 (a whitespace character) |
'\S' |
非空白鍵 (a non-whitespace character) |
再來試一次看看吧:
In [24]: re.findall('\Wapple', PPAP.lower())
Out[24]: [' apple', '\napple', '\napple', ' apple', ' apple']
找到了 5 個!來看看這 5 個分別都躲在哪裡吧!
In [25]: for match in re.finditer('\Wapple', PPAP.lower()):
print(match)
Out[25]: <re.Match object; span=(22, 28), match=' apple'>
<re.Match object; span=(31, 37), match='\napple'>
<re.Match object; span=(88, 94), match='\napple'>
<re.Match object; span=(133, 139), match=' apple'>
<re.Match object; span=(161, 167), match=' apple'>
In [26]: for match in re.finditer('\Wapple', PPAP.lower()):
print(match.span())
Out[26]: (22, 28)
(31, 37)
(88, 94)
(133, 139)
(161, 167)
In [27]: for n, match in enumerate(re.finditer('\Wapple', PPAP.lower()), start=1):
print(f'{n}: start: {match.start()},\tend: {match.end()}')
Out[27]: 1: start: 22, end: 28
2: start: 31, end: 37
3: start: 88, end: 94
4: start: 133, end: 139
5: start: 161, end: 167
可以正常表達(Regular Expressions)是不是很棒呢!
最後我們也試著照樣照句,學PPAP來創造一首歌吧,看看能不能跟PPAP一樣紅。先從第一句開始,不過要填些什麼呢?
In [28]: 先試試看這個 = 'peanut'
'I have a %s' % 先試試看這個
Out[28]: 'I have a peanut'
peanut /ˈpiˌnət/ (noun) 花生
~節錄自《民明書房大字典》
好像哪裡怪怪的。
In [29]: 再試試看這個 = 'pain'
'I have a %s' % 再試試看這個
Out[29]: 'I have a pain'
pain /peɪn/ (noun) 疼痛
~節錄自《民明書房大字典》
不錯,那就來唱一段試試:
In [30]: P開頭的 = 'pain'
A開頭的 = 'alpaca'
我的PPAP = 'I have a %s\nI have an %s\nAh\n%s %s' % (P開頭的, A開頭的, A開頭的, P開頭的)
print(我的PPAP)
Out[30]: I have a pain
I have an alpaca
Ah
alpaca pain
看懂了嗎?這是第一種編排字串格式的方式,我們姑且把它稱之為 % 字串。先用在字串中預定的位置放入%s
,代表等一下要放進這個位置的是字串物件。接著在字串結束後面,用%
宣告要放入字串的標籤,如果有超過一個標籤,那麼就要用()
框起來,這些標籤就會按照位置順序,一個一個填入字串。
接下來示範第二種編排字串格式的方式:
In [31]: P開頭的 = 'pain'
A開頭的 = 'alpaca'
我的PPAP = f'I have a {P開頭的}\nI have an {A開頭的}\nAh\n{A開頭的} {P開頭的}'
print(我的PPAP)
Out[31]: I have a pain
I have an alpaca
Ah
alpaca pain
這種叫做 f 字串(f-string),是比較新的編排字串格式方式。用f'我是 f 字串'
來宣告字串,並在當中需要用到變數的地方以{}
插入變數。是不是相當直覺呢?但我認為兩種方式都學比較好,因為有些情況是 f 字串做不到而還是需要依靠 % 字串來幫忙的。
什麼情況呢?
In [32]: 整數 = len(PPAP); 浮點數 = np.pi; 字串 = 'alpaca'
'%10d, %10f, %10s' % (整數, 浮點數, 字串)
Out[32]: ' 171, 3.141593, alpaca'
上面的程式碼,我們先用%d
、%f
、%s
來宣告等等要放入的請採用十進位整數格式(decimal)、浮點數格式(float)、跟字串格式(string)。接著再寫下10,代表希望可以有 10 個字元這樣的空間。用 f 字串的話呢?
In [33]: 整數 = len(PPAP); 浮點數 = np.pi; 字串 = 'alpaca'
f'{整數:10d}, {浮點數:10f}, {字串:10s}'
Out[33]: ' 171, 3.141593, alpaca '
咦?'alpaca'
從右邊換到左邊了。沒關係,再試試:
In [34]: 整數 = len(PPAP); 浮點數 = np.pi; 字串 = 'alpaca'
f'{整數:10d}, {浮點數:10f}, {字串:>10s}'
Out[34]: ' 171, 3.141593, alpaca'
親愛的,我用>
幫'alpaca'
搬家了。再來看點其他的:
In [35]: 整數 = len(PPAP); 浮點數 = np.pi; 字串 = 'alpaca'
'%.4d, %.4f, %.4s' % (整數, 浮點數, 字串)
Out[35]: '0171, 3.1416, alpa'
那 f 字串呢?
In [36]: 整數 = len(PPAP); 浮點數 = np.pi; 字串 = 'alpaca'
f'{整數:.4d}, {浮點數:.4f}, {字串:.4s}'
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-55-1dc1048a532a> in <module>
1 整數 = len(PPAP); 浮點數 = np.pi; 字串 = 'alpaca'
----> 2 f'{整數:.4d}, {浮點數:.4f}, {字串:.4s}'
ValueError: Precision not allowed in integer format specifier
不允許了。事實上,只有 f'{整數:.4d}'
是不允許的,其他都還行。既然這樣,為什麼還要用 f 字串呢?第一個,它蠻好寫的,寫出來也清楚好懂。第二個:
In [37]: 歡呼一下吧 = 'Bravo'
f'{歡呼一下吧:!<14}, {歡呼一下吧:!^14}, {歡呼一下吧:!>14}'
Out[37]: 'Bravo!!!!!!!!!, !!!!Bravo!!!!!, !!!!!!!!!Bravo'
好啦,今天就講到這囉,謝謝大家的收看。程式碼我放在 Github 上了(Github 或 nbviewer),有興趣的可以下載下來看看,或是參考 Introducing Python 的讀書筆記第七章➁。另外告訴大家一個好消息,我們終於把 Python 的部分說完了。當然不是說 Python 就這些而已,Python 的世界可是博大精深(X),不過經過這幾天的陶冶,我們應該有能力看懂並自行摸索、學習新的 Python 套件,甚至運用這些 Python 套件創造出屬於自己程式囉!那麼明天就要開始講 LINE 聊天機器人囉!
➀ PPAP youtube
➁ Introducing Python 讀書筆記第七章
註:對於此系列文有興趣的讀者,歡迎參考由此系列文擴編成書的 LINE Bot by Python,以及最新的系列文《賴田捕手:追加篇》
第 31 天 初始化 LINE BOT on Heroku
第 32 天 快速回覆 QuickReply 介紹
第 33 天 妥善運用 Heroku APP 暫存空間
第 34 天 妥善運用 LINE Notify 免費推播
第 35 天 製造 Deploy to Heroku 按鈕