對其他人來說也許沒什麼,但對他而言這可真是不容易。因為這個男人認為,打從他有記憶以來就這麼相信,某部分的他是用快速回覆按鈕 (QuickReplyButton) 做出來的。他唯恐一個錯誤的回發動作 (PostbackAction) 被觸發,然後他將會在她面前分崩離析。
~節錄自《聊天機器人的歷史:我媽的憂傷》
延伸自系列文 《從LINE BOT到資料視覺化:賴田捕手》
《賴田捕手:追加篇》:
根據可靠的?科學研究,草泥馬是一群視覺系的動物。草泥馬心理分析權威是這麼說的:「良好而適當的視覺刺激具有穩定草泥馬情緒波動,促進正向思考的能力,是培育積極且強韌的草泥馬的不二法門」。這應該是個生理影響心理,而後心理又影響生理的最佳例子。那麼這就讓人好奇了,到底什麼是良好而適當的視覺刺激呢?答案是,草泥馬們相當享受觀賞雙色打光影像的時刻。
所謂的雙色打光,顧名思義,就是在一個主要觀賞目標的兩側,分別打上兩種不同的單色光,如圖一。
圖一、雙色打光!
身為一個專業的草泥馬訓練師,我當然義不容辭要為我所飼養的草泥馬們打造出最舒適的環境,也就是將所有的圖片轉為雙色打光的圖片,供草泥馬們愜意的欣賞。這件事說難不難,說簡單,好像也沒那麼容易。要將所有的圖片轉為雙色打光的圖片?那我豈不是一整天忙著修圖就飽了,這樣根本沒時間照顧草泥馬啊。幸好我是一個懂得寫 LINE BOT 的草泥馬訓練師。是的,只要寫出一個專門將一般圖片轉為雙色打光圖片的 LINE BOT,那所有的任務都交給 LINE BOT 就搞定了。是不是很吸引人呢?那麼,接下來請容我娓娓道來,我是如何建構出一個擅長雙色打光的 LINE BOT。
關於雙色打光的概念,鼓勵各位讀者參考 Logos By Nick 的影片分享。相當感謝他的熱情教學,才有我的雙色打光 LINE BOT。
工欲善其事,必先利其器。為了完成我們了不起的雙色打光 LINE BOT,將程式碼好好的分門別類是一件相當重要的工作。在第 31 天當中,我們已經將程式碼做了一個大略的分類,把初始化 LINE BOT 的程式碼、處理handler
相關的程式碼、處理route
相關的程式碼拆開變成了三個檔案。檔案結構如下:
D:\appendix>tree /f
Folder PATH listing
Volume serial number is 9C33-6XXD
D:.
│ runtime.txt
│ requirements.txt
│ Procfile
│ Alma.py
│
└───app
__init__.py
models_for_line.py
routes.py
為了要做出功能強大的 LINE BOT,我們會在處理handler
相關的任務中寫下更多更長更繁瑣的程式碼。為了保持乾淨的程式碼,便於繼續擴充和維護,我打算把models_for_line.py
這個檔案中的詳細功能拉出來,另外創立幾個檔案,因此資料夾的檔案結構會變成這樣:
D:\appendix>tree /f
Folder PATH listing
Volume serial number is 9C33-6XXD
D:.
│ runtime.txt
│ requirements.txt
│ Procfile
│ Alma.py
│
└───app
│ __init__.py
│ models_for_line.py
│ routes.py
│
└───custom_models
AlmaTalks.py
詳細回覆使用者的方法被放到app/custom_models/AlmaTalks.py
:
app/custom_models/AlmaTalks.py
from app import line_bot_api
from linebot.models import TextSendMessage
def default_reply(event):
name = line_bot_api.get_profile(event.source.user_id).display_name
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text=f"Hello {name}!")
)
而原本的models_for_line.py
則剩下:
app/models_for_line.py
from app import handler
from app.custom_models import AlmaTalks
from linebot.models import MessageEvent, TextMessage
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
AlmaTalks.default_reply(event)
為什麼選擇這麼做呢?因為我希望models_for_line.py
這個檔案簡單一點,讓人一目了然。而真正困難而仔細的各種任務就放到app/custom_models
底下的不同檔案裡。
那麼,實際上最核心的雙色打光程式碼可以怎麼做呢?概念上來說,應該也不難:
兩步驟完成!
而 Python 在影像處理以及數據處理方面有非常豐富的資源庫,我們可以簡單的用 PIL 和 numpy 這兩個資源庫來完成這個任務。
import numpy as np
from PIL import Image
from PIL import ImageDraw
def sigmoid(x, alpha):
return 1 /(1 + np.exp(-x * alpha))
def create_gradient_layer(layer_im, first_tone, second_tone, gradient_factor):
layer_gradient = Image.new('RGB', layer_im.size)
draw = ImageDraw.Draw(layer_gradient)
for i in range(layer_im.size[0]):
value = sigmoid(i - layer_im.size[0] / 2, gradient_factor / layer_im.size[0])
fill_color = np.array(first_tone) * value + np.array(second_tone) * (1 - value)
draw.line([(i, 0), (i, layer_im.size[1]-1)], fill=tuple(fill_color.astype('int')))
return layer_gradient
所謂的漸層,即是色彩由深而淺的變化,於是我們可以先創造一個sigmoid
函式來模擬這種變化,並且設計兩個參數,x
和alpha
,x
與位置有關,根據位置來做出不同顏色的深淺變化。而第二個參數alpha
則讓我們可以控制漸層變化的幅度。以我們設計的sigmoid
函式來說,alpha
越小,梯度變化越平緩,而alpha
越大,梯度變化越急遽,具體結果可以用matplotlib
這個用來作圖的資源庫來檢視 (圖二):
import matplotlib.pyplot as plt
fig, axes = plt.subplots(3, 1, figsize=(10, 10))
x = np.linspace(0, 10, 101)
for ax, g in zip(axes.flatten(), [1, 10, 100]):
ax.plot(x, sigmoid(x-5, g), label=f'gradient={g}')
ax.legend(fontsize='x-large')
圖二、sigmoid
函式運作方式
在create_gradient_layer
這個函式裡,我們先用Image.new()
來產生一張新的圖像,接著再用ImageDraw.Draw()
為這張新的圖像上色。
layer_gradient = Image.new('RGB', layer_im.size)
:
產生一張以'RGB'
做為資料儲存格式的圖像layer_gradient
,圖像大小則參考原始圖像layer_im.size
。
draw = ImageDraw.Draw(layer_gradient)
:
準備在新的圖像layer_gradient
上面作畫。
draw.line([(i, 0), (i, layer_im.size[1]-1)], fill=tuple(fill_color.astype('int')))
:
從座標(i, 0)
開始到座標(i, layer_im.size[1]-1)
為止,畫一條線,該線條的顏色則由fill=tuple(fill_color.astype('int'))
來定義。由於我們的layer_gradient
這個新圖像是用'RGB'
做為資料儲存的格式,因此fill
也要用相同的形式來表示,如紅色應該表示為(255, 0, 0)
,綠色是(0, 255, 0)
,諸如此類。
這邊假定有一張demo_image.jpg
,那麼我們執行create_ gradient_layer
:
create_gradient_layer(Image.open('demo_image.jpg'), (0, 255, 0), (0, 0, 255), 1)
圖三、gradient_factor=1
創造出來的雙色漸層圖像
create_gradient_layer(Image.open('demo_image.jpg'), (0, 255, 0), (0, 0, 255), 10)
圖四、gradient_factor=10
創造出來的雙色漸層圖像
create_gradient_layer(Image.open('demo_image.jpg'), (0, 255, 0), (0, 0, 255), 100)
圖五、gradient_factor=100
創造出來的雙色漸層圖像
blend
或是composite
兩種方式將雙色漸層圖像與原始圖像進行疊圖。Image.blend
:Image.blend(layer_im, layer_gradient, alpha=0.5)
這邊的alpha
可以調整兩張影像是用何種比例進行疊圖的。若希望第一張影像layer_im
所佔的比例高一些,則alpha
要小一點,若希望第二張影像layer_gradient
所佔的比例高一些,則alpha
就設定大一點。極端一點來說,若alpha=0
則會直接得到第一張影像layer_im
,而alpha=1
會直接得到第二張影像layer_gradient
。
圖六、Image.blend
疊圖
Image.composite
:# 用layer_im當濾鏡,深色的地方雙色打光效果明顯
Image.composite(layer_im, layer_gradient, layer_im.convert('L'))
# 用ImageOps.invert(layer_im)當濾鏡,淺色的地方雙色打光效果明顯
Image.composite(layer_im, layer_gradient, ImageOps.invert(layer_im).convert('L'))
第二種方式composite
則提供了有趣的疊圖選擇。一樣是將兩張影像layer_im
和layer_gradient
疊在一起,不過第二張影像要透過一個帶有透明訊息的濾鏡來疊圖。而最直覺的濾鏡,就是layer_im.convert('L')
,這樣可以做出如圖七的影像,深色的地方會有較強的雙色打光效果。
圖七、Image.composite
疊圖
另外我們也可以用ImageOps.invert
,將影像黑白反轉,這樣的濾鏡可以做出如圖八的影像,淺色的地方會有較強的雙色打光效果。
圖八、Image.composite
加上ImageOps.invert
疊圖
完成之後,我們可以把所有程式碼串在一起了:
import numpy as np
from PIL import Image
from PIL import ImageDraw
from PIL import ImageOps
def sigmoid(x, alpha):
return 1 /(1 + np.exp(-x * alpha))
def create_gradient_layer(layer_im, first_tone, second_tone, gradient_factor):
layer_gradient = Image.new('RGB', layer_im.size)
draw = ImageDraw.Draw(layer_gradient)
for i in range(layer_im.size[0]):
value = sigmoid(i - layer_im.size[0] / 2, gradient_factor / layer_im.size[0])
fill_color = np.array(first_tone) * value + np.array(second_tone) * (1 - value)
draw.line([(i, 0), (i, layer_im.size[1]-1)], fill=tuple(fill_color.astype('int')))
return layer_gradient
def dual_tone_run(file, mode, gradient_factor, first_tone, second_tone):
layer_im = Image.open(file).convert('RGBA')
layer_gradient = create_gradient_layer(layer_im, first_tone, second_tone, gradient_factor).convert('RGBA')
if mode == 'blend':
dual_tone = Image.blend(layer_im, layer_gradient, 0.5)
elif mode == 'composite':
dual_tone = Image.composite(layer_im, layer_gradient, layer_im.convert('L'))
elif mode == 'composite_invert':
dual_tone = Image.composite(layer_im, layer_gradient, ImageOps.invert(layer_im.convert('L')).convert('L'))
return dual_tone
完成了雙色打光的運作之後,就可以開始來設計我們的 LINE BOT 了。大家在實作雙色打光的程式碼時,應該注意到這段程式碼其實擁有許多參數,而這些參數都可以大大的影響最後產生出來的圖像。因此我們在建構這個影像處理 LINE BOT 的時候,其實是可以給出不少選項供使用者依照個人偏好或是情境做出選擇的。而在這方面,LINE 相當貼心的推出了各種功能,讓使用者可以更容易的與 LINE BOT 進行互動,像我們今天要介紹的快速回覆 (QuickReply
) 就是其中一種。
快速回覆有點像 LINE BOT 丟給使用者的選擇題。LINE BOT 提供選項,而使用者從選擇其中一個作為回答,回傳給 LINE BOT,完成一次互動,操作起來簡單明瞭。不過,快速回覆最大的缺點是只會顯示在手機介面上。因此目前沒辦法在電腦版的介面上透過快速回覆與 LINT BOT 進行互動。
要使用快速回覆也不難,先看一段 LINE 官方給出的使用範例:
text_message = TextSendMessage(
text='Hello, world',
quick_reply=QuickReply(items=[
QuickReplyButton(action=MessageAction(label="label", text="text"))
])
)
這段操作的結果是,LINE BOT 會傳送文字訊息'Hello, world'
給使用者,而使用者會看到一個快速回覆按鈕 (QuickReplyButton
)。如果我們想要提供更多的快速回覆按鈕,可以這麼做:
quick_reply=QuickReply(
items=[
QuickReplyButton(),
QuickReplyButton(),
QuickReplyButton(),
…
]
)
只要在代表選項的items
這個清單當中放入更多的QuickReplyButton
就可以了。
回到我們的範例。這邊所提供的快速回覆按鈕的文字顯示為"label"
。使用者可以點擊這個按鈕,當作對 LINE BOT 的回答。點下去的時候,該按鈕所設定的動作被觸發,也就是訊息動作MessageAction(label="label", text="text")
,這時候系統會為使用者傳送文字訊息"text"
給 LINE BOT。換句話說,點擊帶有MessageAction(label="label", text="text")
動作的按鈕,就好像使用者親自發送文字訊息"text"
給 LINE BOT 一樣。
常用的幾種動作包括MessageAction
、PostbackAction
、URIAction
等。回發動作PostbackAction
可以看做進階的MessageAction
,除了可以發送文字訊息之外,還會多傳送隱藏的資料 (不會顯示在 LINE 的對話視窗當中),方便 LINE BOT 根據這些資料做出適當的回應。而URIAction
則是為使用者開啟指定連結的動作。
這邊我打算用回發動作PostbackAction
,使用範例如下:
action=PostbackAction(
label='postback',
display_text='postback text',
data='action=buy&itemid=1'
)
label='postback'
:
用來設定代表觸發該動作的按鈕的顯示文字。
display_text='postback text'
:
觸發該動作之後,系統為使用者傳送的文字訊息。也就是說,點擊此按鈕,就好像使用者向 LINE BOT 傳送了'postback text'
這樣的文字訊息。但跟MessageAction
不同的是,LINE BOT 並不會因此收到MessageEvent
。相對的,LINE BOT 會收到的是回發事件PostbackEvent
,因為這可是PostbackAction
啊。所以說,跟MessageAction
不同,這邊的文字訊息只是假的,是顯示文字 (display_text
) 而已。
data='action=buy&itemid=1'
:
這個才是PostbackAction
真正傳送到 LINE BOT 的資訊。LINE BOT 會收到PostbackEvent
,而我們可以藉由event.postback.data
來拿到這些資訊。
好的,既然我們已經知道怎麼做出雙色打光,也了解怎麼使用QuickReply
,那現在就可以開始來規劃一下整個 LINE BOT 跟使用者的互動流程,看看我們的 LINE BOT 怎麼替使用者客製化作出雙色打光影像處理。
這部分大家當然可以自由發揮,我就提一個簡單的流程來說明我會如何架構:
圖九、LINE BOT 與使用者互動流程
圖九是我打算採用的流程概念圖,整個互動從使用者向 LINE BOT 傳送圖像開始,也就是當 LINE BOT 接收到ImageEvent
,整段流程就開始了。使用者依序設定好模式 (mode)、梯度 (gradient_factor)、第一種顏色 (first_tone)、以及第二種顏色 (second_tone),接著 LINE BOT 就根據這些使用者給出的條件,去對使用者一開始傳送過來的圖像做影像處理。按照這個規劃,我們就得為models_for_line.py
這個檔案添加幾段程式碼:
app/models_for_line.py
from app import handler
from app.custom_models import AlmaTalks
from linebot.models import ImageMessage, PostbackEvent
@handler.add(MessageEvent, message=ImageMessage)
def handle_image(event):
AlmaTalks.phase_start(event)
@handler.add(PostbackEvent)
def handle_postback(event):
if not event.postback.data.startswith('second_tone='):
AlmaTalks.phase_intermediate(event)
else:
AlmaTalks.phase_finish(event)
按照這樣的設計,當使用者向 LINE BOT 傳送圖片 (ImageMessage
),任務開始,啟動AlmaTalks.phase_start
這個函式。接著 LINE BOT 會依序接收到使用者透過QuickReplyButton
傳送過來的PostbackEvent
,而這些就交給AlmaTalks.phase_intermediate
和AlmaTalks.phase_finish
來處理。所以現在我們就要來著手撰寫這幾個函式。
app/custom_models/AlmaTalks.py
def phase_start(event):
# 初始化表格
CallDatabase.init_table()
# 檢查使用者資料是否存在
if CallDatabase.check_record(event.source.user_id):
_ = CallDatabase.update_record(event.source.user_id, 'message_id', event.message.id)
else:
_ = CallDatabase.init_record(event.source.user_id, event.message.id)
mode_dict = {'blend': '線性疊圖', 'composite': '濾鏡疊圖', 'composite_invert': '反式濾鏡疊圖'}
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(
text=f"[1/4] 今晚,我想來點雙色打光!\n請選擇雙色打光模式:",
quick_reply=QuickReply(
items=[QuickReplyButton(action=PostbackAction(
label=v,
display_text=f'打光模式:{v}',
data=f'mode={k}')) for k, v in mode_dict.items()
]
)
)
)
為了讓 LINE BOT 能夠記得使用者選擇的設定,我們在第 31 天添加了一個擴充元件 Heroku Postgres 當作資料庫,將我們需要保留的資料,也就是使用者的設定,儲存起來。所有與資料庫的互動,包括初始化表格、檢查資料、放入資料、更新資料等等,我都打算放進另一個檔案裡,也就是app/custom_models/CallDatabase.py
,等等會再詳細介紹。
根據這段程式碼,函式phase_start
要做的事就是當接收到使用者傳來的圖片時,為使用者在資料庫中的表格初始化一筆資料 (或是更新資料),接著透過QuickReplyButton
提供不同的打光模式選擇給使用者。
app/custom_models/AlmaTalks.py
def phase_intermediate(event):
color_dict = {
'red': '紅',
'orange': '橙',
'yellow': '黃',
'green': '綠',
'blue': '藍',
'purple': '紫'
}
reply_dict = {
'mode': '[2/4] 今晚,繼續來點雙色打光!\n請選擇色彩變化梯度:',
'gradient_factor': '[3/4] 今晚,還想來點雙色打光!\n請選擇第一道色彩:',
'first_tone': '[4/4] 今晚,最後來點雙色打光!\n請選擇第二道色彩:'
}
quick_button_dict = {
'mode':
[QuickReplyButton(
action=PostbackAction(
label=i,
display_text=f'變化梯度:{i}',
data=f'gradient_factor={i}')) for i in (5, 10, 50, 100)
],
'gradient_factor':
[QuickReplyButton(
action=PostbackAction(
label=j,
display_text=f'第一道色彩:{j}',
data=f'first_tone={i}')) for i, j in color_dict.items()
],
'first_tone':
[QuickReplyButton(
action=PostbackAction(
label=j,
display_text=f'第二道色彩:{j}',
data=f'second_tone={i}')) for i, j in color_dict.items()
]
}
user_id = event.source.user_id
postback_data = event.postback.data
current_phase = postback_data.split('=')[0]
# 依照使用者的選擇更新資料
CallDatabase.update_record(user_id, current_phase, postback_data.split('=')[1])
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(
text=reply_dict[current_phase],
quick_reply=QuickReply(
items=quick_button_dict[current_phase]))
)
這邊應該也難不倒大家。先建構好reply_dict
和quick_button_dict
,按照流程準備好不同階段相對應的回答跟快速回覆按鈕。這邊我在QuickReplyButton
的PostbackAction
裡藏了不同階段的暗示,讓 LINE BOT 在收到PostbackEvent
時,可以藉由event.postback.data
來判斷這一連串的互動是進行到哪一階段了。
app/custom_models/AlmaTalks.py
def phase_finish(event):
user_id = event.source.user_id
postback_data = event.postback.data
current_phase = postback_data.split('=')[0]
# 更新資料並取得最後的完整設定
record = CallDatabase.update_record(user_id, current_phase, postback_data.split('=')[1])
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text=str(record))
)
最後一個階段,在使用者選擇好第二道色彩的同時,我們讓 LINE BOT 更新這一筆設定,並從資料庫中提取該名使用者先前的所有設定,包括打光模式、變化梯度、以及選擇的兩種色彩。我們可以簡單的用TextSendMessage
來將設定的內容傳回給使用者,檢查 LINE BOT 是否真的記下了這些內容。
在前面我們設計的互動過程中,LINE BOT 是利用app/custom_models/CallDatabase.py
來操作 Heroku Postgres,紀錄、更新、提取使用者的設定。在互動流程大致底定之後,現在該是時候把CallDatabase
給生出來了。
app/custom_models/CallDatabase.py
import os
import psycopg2
def access_database():
DATABASE_URL = os.environ['DATABASE_URL']
conn = psycopg2.connect(DATABASE_URL, sslmode='require')
cursor = conn.cursor()
return conn, cursor
def init_table():
conn, cursor = access_database()
postgres_table_query = "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema'"
cursor.execute(postgres_table_query)
table_records = cursor.fetchall()
table_records = [i[0] for i in table_records]
if 'user_dualtone_settings' not in table_records:
create_table_query = """CREATE TABLE user_dualtone_settings (
user_id VARCHAR ( 50 ) PRIMARY KEY,
message_id VARCHAR ( 50 ) NOT NULL,
mode VARCHAR ( 20 ) NOT NULL,
gradient_factor VARCHAR ( 20 ) NOT NULL,
first_tone VARCHAR ( 20 ) NOT NULL,
second_tone VARCHAR ( 20 ) NOT NULL
);"""
cursor.execute(create_table_query)
conn.commit()
return True
先寫好兩個函式:連接 Heroku Postgres 資料庫的起手式access_database
,以及在資料庫中初始化表格init_table
。先跟大家道個歉。在第 31 天當中,我也寫了一個init_table
這個函式,用來創造我們需要用的表格'user_dualtone_settings'
。不過經過一個星期的修訂之後,我想要更改表格的欄位和資料類型,改動如下:
CREATE TABLE user_dualtone_settings (
user_id VARCHAR ( 50 ) PRIMARY KEY,
message_id VARCHAR ( 50 ) NOT NULL,
mode VARCHAR ( 20 ) NOT NULL,
gradient_factor VARCHAR ( 20 ) NOT NULL,
first_tone VARCHAR ( 20 ) NOT NULL,
second_tone VARCHAR ( 20 ) NOT NULL
);
所以說,如果有人已經根據上星期的內容在資料庫裡新增了一個表格,那較簡單的方法可能是把 Heroku Postgres 這個擴充元件給刪了,再重新新增一個。當然,如果對 SQL 語法以及psycopg2
熟悉的朋友,也可以用刪掉表格 (DROP TABLE
,詳細內容可以參考第 15 天)、改動表格 (ALTER TABLE
) 等等方式來做修改。對於造成的不便,再次向大家道歉。
接著我們需要一個檢查使用者資料是否存在的函式check_record
:
app/custom_models/CallDatabase.py
def check_record(user_id):
conn, cursor = access_database()
postgres_select_query = f"SELECT * FROM user_dualtone_settings WHERE user_id = '{user_id}';"
cursor.execute(postgres_select_query)
user_settings = cursor.fetchone()
return user_settings
如果沒有紀錄,那就先初始化一筆暫時的紀錄:
app/custom_models/CallDatabase.py
def init_record(user_id, message_id):
conn, cursor = access_database()
table_columns = '(user_id, message_id, mode, gradient_factor, first_tone, second_tone)'
postgres_insert_query = f"INSERT INTO user_dualtone_settings {table_columns} VALUES (%s,%s,%s,%s,%s,%s)"
record = (user_id, message_id, 'blend', '50', 'red', 'blue')
cursor.execute(postgres_insert_query, record)
conn.commit()
cursor.close()
conn.close()
return record
以及更新紀錄的方法:
app/custom_models/CallDatabase.py
def update_record(user_id, col, value):
conn, cursor = access_database()
postgres_update_query = f"UPDATE user_dualtone_settings SET {col} = %s WHERE user_id = %s"
cursor.execute(postgres_update_query, (value, user_id))
conn.commit()
postgres_select_query = f"SELECT * FROM user_dualtone_settings WHERE user_id = '{user_id}';"
cursor.execute(postgres_select_query)
user_settings = cursor.fetchone()
cursor.close()
conn.close()
return user_settings
全部寫好之後,我們的檔案架構看起來會像這樣:
D:\appendix>tree /f
Folder PATH listing
Volume serial number is 9C33-6XXD
D:.
│ runtime.txt
│ requirements.txt
│ Procfile
│ Alma.py
│
└───app
│ __init__.py
│ models_for_line.py
│ routes.py
│
└───custom_models
AlmaTalks.py
CallDatabase.py
好了,那麼把完成的資料夾丟到 Heroku 上面吧!
棒!是不是真的記住了我們的設定呢。
圖十、記住所有設定的 LINE BOT
等等,說好今晚的雙色打光呢?
抱歉,今晚已經有點晚了。這個我們留到下星期,討論如何利用 Heroku 暫存空間的同時,再一起把所有東西補上。有興趣的讀者,也可以試著利用上面我們討論出來的雙色打光程式碼,裝備到 LINE BOT 上,看看 Heroku 是否跑得動這個雙色打光的傑出操作 (當然是跑得動,不然這個系列文就?)。
好的,相信大家知道接下來又要進入最重要的工商時間了。是的,感謝 iT邦幫忙 和 博碩文化,LINE Bot by Python 全攻略 集結成書了,歡迎有興趣的大家前往預購喔。