iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 8
0
Modern Web

從LINE BOT到資料視覺化:賴田捕手系列 第 8

第 08 天:Python:回頭再看看字串物件

第 08 天:Python:回頭再看看字串物件

我不喜歡這個想法,仔細思考一下的話,這根本就糟糕透了。如果你不喜歡一首歌,那你就不應該一直去聽那首歌。如果你喜歡一首歌,那你就應該要喜歡它的歌詞,應該要牢牢地記住它的歌詞,而不是胡亂的去惡搞它。

~節錄自《賴田捕手》第九章

更多關於字串(function)物件的操作

  之前有先介紹過字串物件的基本操作了。其實字串在程式中的應用相當多,更別提我們想做的可是接收跟發送字串的聊天機器人,對於字串的各種使用方式跟相關的套件,我們要有更多了解才行。因此在我們把 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的方法。
  1. split(),把字串切開的方式,用法是要切開的字串.split(切口處)。切完了之後,會拿到由片段字串組成的清單。來用用看吧?
In [15]: 要切開的字串 = '煞☆氣☆的☆中☆二'
         切口處 = '☆'
         要切開的字串.split(切口處)
Out[15]: ['煞', '氣', '的', '中', '二'] 
  1. join(),顧名思義,把字串連在一起的方式,用法是膠水.join(要黏合的字串清單)。所謂要黏合的字串清單,指的是由一個一個字串組成的清單,透過膠水,黏在一起後傳回一個完整的字串。
In [16]: 膠水 = ' ㊣ '
         要黏合的字串清單 = ['煞', '氣', '的', '中', '二']
         膠水.join(要黏合的字串清單)
Out[16]: '煞 ㊣ 氣 ㊣ 的 ㊣ 中 ㊣ 二'
  1. 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

  而該方法會回傳一個屬於numpyndarray物件。我懂,雖然長得很像清單(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()隨機打亂,最後再用空白鍵將打亂的字串連接再一起。耶,成功剝開洋蔥囉!

正常表達(Regular Expressions, re)

  上面的程式碼寫了那麼多,我們可以用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 上了(Githubnbviewer),有興趣的可以下載下來看看,或是參考 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 按鈕


上一篇
第 07 天:Python:函數物件
下一篇
第 09 天:LINE BOT SDK:註冊!註冊!註冊!
系列文
從LINE BOT到資料視覺化:賴田捕手30

尚未有邦友留言

立即登入留言