Line bot範例

大家如果有仔細觀察,可以發現當使用者對店家的LINE Bot發出訊息時,有些除了回應文字訊息外,還會回應貼圖、影片或樣板訊息等,來提高使用者體驗,這些就是LINE Messaging API所提供的訊息型態(Message types),讓開發人員可以依據需求,來客製化回應的訊息。

按鈕樣板訊息(Buttons template message)就是一個樣板類型的訊息,其中可以包含圖片、標題、文字及多顆按鈕,讓使用者可以進行點選,如下圖:

Line bot範例

取自LINE Developers Documents

為了想要讓LINE Bot的回覆訊息擁有這樣的效果,所以開啟Django應用程式(foodlinebot)下的views.py檔案,如下圖:

callback(檢視函式)中,假設當使用者輸入「哈囉」時,想要LINE Bot回覆選擇地區的按鈕樣板訊息(Buttons template message),就需引用TemplateSendMessage、ButtonsTemplateMessageTemplateAction,如下範例第11~13行

from django.shortcuts import render
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings

from linebot import LineBotApi, WebhookParser
from linebot.exceptions import InvalidSignatureError, LineBotApiError
from linebot.models import (
    MessageEvent,
    TextSendMessage,
    TemplateSendMessage,
    ButtonsTemplate,
    MessageTemplateAction
)

from .scraper import IFoodie

line_bot_api = LineBotApi(settings.LINE_CHANNEL_ACCESS_TOKEN)
parser = WebhookParser(settings.LINE_CHANNEL_SECRET)


@csrf_exempt
def callback(request):

    if request.method == 'POST':
        signature = request.META['HTTP_X_LINE_SIGNATURE']
        body = request.body.decode('utf-8')

        try:
            events = parser.parse(body, signature)  # 傳入的事件
        except InvalidSignatureError:
            return HttpResponseForbidden()
        except LineBotApiError:
            return HttpResponseBadRequest()

        for event in events:
            if isinstance(event, MessageEvent):  # 如果有訊息事件

                if event.message.text == "哈囉":

                    line_bot_api.reply_message(  # 回復傳入的訊息文字
                        event.reply_token,
                        TemplateSendMessage(
                            alt_text='Buttons template',
                            template=ButtonsTemplate(
                                title='Menu',
                                text='請選擇地區',
                                actions=[
                                    MessageTemplateAction(
                                        label='台北市',
                                        text='台北市'
                                    ),
                                    MessageTemplateAction(
                                        label='台中市',
                                        text='台中市'
                                    ),
                                    MessageTemplateAction(
                                        label='高雄市',
                                        text='高雄市'
                                    )
                                ]
                            )
                        )
                    )
                else:
                    food = IFoodie(event.message.text)

                    line_bot_api.reply_message(  # 回應前五間最高人氣且營業中的餐廳訊息文字
                        event.reply_token,
                        TextSendMessage(text=food.scrape())
                    )

        return HttpResponse()
    else:
        return HttpResponseBadRequest()

執行結果

Line bot範例

範例中39行,LINE Bot判斷使用者發送「哈囉」訊息時,在replay_message(回覆訊息)的API中,使用TemplateSendMessage(樣板傳送訊息),並且指定為Buttons Template(按鈕樣板),如第45行。其中就可以自訂標題、文字及按鈕。

當使用者選擇地區後,39行判斷使用者發送的訊息不是「哈囉」,所以就會執行第66行將地區傳入Python網頁爬蟲中取得資料。

這時候,如果想要再增加一個步驟,當使用者選擇地區後,LINE Bot能夠接著回覆按鈕樣板訊息(Buttons template message),讓使用者選擇想吃的美食分類,像是火鍋、早午餐或約會餐廳等,完成後再呼叫Python網頁爬蟲進行資料的取得,該怎麼做呢?

想必大家最直覺的做法是,再增加一個if判斷式吧?這樣雖然能夠解決問題,但是使用者第二次選擇餐廳分類時,LINE Bot所收到的訊息將會是餐廳分類,那該如何知道使用者在第一次選擇地區時,是選擇什麼?

二、LINE Bot按鈕樣板訊息(Buttons template message)回傳值

要解決這樣的問題,就需要在使用者進行選擇前,在按鈕樣板訊息(Buttons template message)的每個選項背後夾帶自訂的資料,當使用者選擇後,就能夠將其中夾帶的資料發送給LINE Bot

舉例來說,在使用者發送「哈囉」訊息給LINE Bot時,為了要標記接下來回覆的選擇地區按鈕樣板訊息(Buttons template message)為第一步驟,就能夠引用「PostbackEvent」及PostbackTemplateAction,如下範例第14~15行,在每個選項中增加一個回傳值(data),其中夾帶自訂的資料A(代表第一步驟)以及該選項的資料,如下範例第53~67

from django.shortcuts import render
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings

from linebot import LineBotApi, WebhookParser
from linebot.exceptions import InvalidSignatureError, LineBotApiError
from linebot.models import (
    MessageEvent,
    TextSendMessage,
    TemplateSendMessage,
    ButtonsTemplate,
    MessageTemplateAction,
    PostbackEvent,
    PostbackTemplateAction
)

from .scraper import IFoodie

line_bot_api = LineBotApi(settings.LINE_CHANNEL_ACCESS_TOKEN)
parser = WebhookParser(settings.LINE_CHANNEL_SECRET)


@csrf_exempt
def callback(request):

    if request.method == 'POST':
        signature = request.META['HTTP_X_LINE_SIGNATURE']
        body = request.body.decode('utf-8')

        try:
            events = parser.parse(body, signature)  # 傳入的事件

        except InvalidSignatureError:
            return HttpResponseForbidden()
        except LineBotApiError:
            return HttpResponseBadRequest()

        for event in events:

            if isinstance(event, MessageEvent):  # 如果有訊息事件

                if event.message.text == '哈囉':

                    line_bot_api.reply_message(  # 回復「選擇地區」按鈕樣板訊息
                        event.reply_token,
                        TemplateSendMessage(
                            alt_text='Buttons template',
                            template=ButtonsTemplate(
                                title='Menu',
                                text='請選擇地區',
                                actions=[
                                    PostbackTemplateAction(
                                        label='台北市',
                                        text='台北市',
                                        data='A&台北市'
                                    ),
                                    PostbackTemplateAction(
                                        label='台中市',
                                        text='台中市',
                                        data='A&台中市'
                                    ),
                                    PostbackTemplateAction(
                                        label='高雄市',
                                        text='高雄市',
                                        data='A&高雄市'
                                    )
                                ]
                            )
                        )
                    )
                else:
                    food = IFoodie(event.message.text)

                    line_bot_api.reply_message(  # 回復傳入的訊息文字
                        event.reply_token,
                        TextSendMessage(text=food.scrape())
                    )

        return HttpResponse()
    else:
        return HttpResponseBadRequest()

這時候,當使用者選擇地區後,LINE Bot就能夠收到傳值(data)中的資料,讓開發人員可以更有彈性的應用,其中一個就是可以再新增第二個步驟-「選擇美食分類」,而在其中的每個選項夾帶第一步驟中使用者所選擇的地區資料,如下範例:

from django.shortcuts import render
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings

from linebot import LineBotApi, WebhookParser
from linebot.exceptions import InvalidSignatureError, LineBotApiError
from linebot.models import (
    MessageEvent,
    TextSendMessage,
    TemplateSendMessage,
    ButtonsTemplate,
    MessageTemplateAction,
    PostbackEvent,
    PostbackTemplateAction
)

from .scraper import IFoodie

line_bot_api = LineBotApi(settings.LINE_CHANNEL_ACCESS_TOKEN)
parser = WebhookParser(settings.LINE_CHANNEL_SECRET)


@csrf_exempt
def callback(request):

    if request.method == 'POST':
        signature = request.META['HTTP_X_LINE_SIGNATURE']
        body = request.body.decode('utf-8')

        try:
            events = parser.parse(body, signature)  # 傳入的事件
        except InvalidSignatureError:
            return HttpResponseForbidden()
        except LineBotApiError:
            return HttpResponseBadRequest()

        for event in events:
            if isinstance(event, MessageEvent):  # 如果有訊息事件

                if event.message.text == "哈囉":

                    line_bot_api.reply_message(  # 回復傳入的訊息文字
                        event.reply_token,
                        TemplateSendMessage(
                            alt_text='Buttons template',
                            template=ButtonsTemplate(
                                title='Menu',
                                text='請選擇地區',
                                actions=[
                                    PostbackTemplateAction(
                                        label='台北市',
                                        text='台北市',
                                        data='A&台北市'
                                    ),
                                    PostbackTemplateAction(
                                        label='台中市',
                                        text='台中市',
                                        data='A&台中市'
                                    ),
                                    PostbackTemplateAction(
                                        label='高雄市',
                                        text='高雄市',
                                        data='A&高雄市'
                                    )
                                ]
                            )
                        )
                    )
            elif isinstance(event, PostbackEvent):  # 如果有回傳值事件

                if event.postback.data[0:1] == "A":  # 如果回傳值為「選擇地區」

                    area = event.postback.data[2:]  # 透過切割字串取得地區文字

                    line_bot_api.reply_message(   # 回復「選擇美食類別」按鈕樣板訊息
                        event.reply_token,
                        TemplateSendMessage(
                            alt_text='Buttons template',
                            template=ButtonsTemplate(
                                title='Menu',
                                text='請選擇美食類別',
                                actions=[
                                    PostbackTemplateAction(  # 將第一步驟選擇的地區,包含在第二步驟的資料中
                                        label='火鍋',
                                        text='火鍋',
                                        data='B&' + area + '&火鍋'
                                    ),
                                    PostbackTemplateAction(
                                        label='早午餐',
                                        text='早午餐',
                                        data='B&' + area + '&早午餐'
                                    ),
                                    PostbackTemplateAction(
                                        label='約會餐廳',
                                        text='約會餐廳',
                                        data='B&' + area + '&約會餐廳'
                                    )
                                ]
                            )
                        )
                    )

                elif event.postback.data[0:1] == "B":  # 如果回傳值為「選擇美食類別」

                    result = event.postback.data[2:].split('&')  # 回傳值的字串切割

                    food = IFoodie(
                        result[0],  # 地區
                        result[1]  # 美食類別
                    )

                    line_bot_api.reply_message(  # 回復訊息文字
                        event.reply_token,
                        # 爬取該地區正在營業,且符合所選擇的美食類別的前五大最高人氣餐廳
                        TextSendMessage(text=food.scrape())
                    )

        return HttpResponse()
    else:
        return HttpResponseBadRequest()

一般在傳送文字訊息時,都是MessageEvent(訊息事件),如第16行,而如果有回傳值(data),就會是PostbackEvent(回傳值事件),如第48行。

所以第一次使用者發送「哈囉」訊息時,沒有回傳值,第39行判斷為MessageEvent,回覆選擇地區的按鈕樣板訊息(Buttons template message),第二次當使用者選擇後,由於選項中有回傳值(data),因此第70判斷PostbackEvent就會成立。

接著,利用Python的字串切割,第72行判斷為A(第一步驟),所以將回傳值(data)中使用者選擇的地區附加到B(第二步驟)的回傳值(data)中,並且回覆選擇餐廳分類的鈕樣板訊息(Buttons template message)

使用者在選擇餐廳分類後,同樣為PostbackEvent,所以LINE Bot就可以從回傳值(data)中取得使用者在第一及第二步驟所選擇的資料了。

三、LINE Bot重構Python網頁爬蟲

由於我們增加了一個美食分類,所以在scraper.py網頁爬蟲檔案中,Food抽象類別的建構式,需增加category屬性,如下範例

# 美食抽象類別
class Food(ABC):

    def __init__(self, area, category):
        self.area = area  # 地區
        self.category = category  # 美食類別

    @abstractmethod
    def scrape(self):
        pass

開啟愛食記網站,選擇搜尋地點後,可以看到網址結構為:

接著,選擇美食分類,可以看到網址結構為:

從上圖可以知道,餐廳分類就是接在網址結構中的list後面,所以,在scraper.py檔案的IFoodie類別中,在網址的地方修改為如下範例第5

# 愛食記爬蟲
class IFoodie(Food):

    def scrape(self):
        response = requests.get(
            "https://ifoodie.tw/explore/" + self.area +
            "/list/" + self.category +
            "?sortby=popular&opening=true")

        soup = BeautifulSoup(response.content, "html.parser")

        # 爬取前五筆餐廳卡片資料
        cards = soup.find_all(
            'div', {'class': 'jsx-1776651079 restaurant-info'}, limit=5)

        content = ""
        for card in cards:

            title = card.find(  # 餐廳名稱
                "a", {"class": "jsx-1776651079 title-text"}).getText()

            stars = card.find(  # 餐廳評價
                "div", {"class": "jsx-1207467136 text"}).getText()

            address = card.find(  # 餐廳地址
                "div", {"class": "jsx-1776651079 address-row"}).getText()

            content += f"{title} \n{stars}顆星 \n{address} \n\n"

        return content

最後,就可以執行LINE Bot和它對話了,如下範例:

Line bot範例

Line bot範例

四、小結

以上就是延續[Python+LINE Bot教學]建構具網頁爬蟲功能的LINE Bot機器人文章,使用鈕樣板訊息(Buttons template message)來提升使用者的互動體驗,大家可以依循這樣的邏輯,練習再增加一個步驟,讓使用者選擇平均消費價格,再利用Python網頁爬蟲取得餐廳資料。希望本文有幫助到您,歡迎分享給身邊對LINE Bot有興趣的朋友。

如果您喜歡我的文章,請幫我按五下Like(使用GoogleFacebook帳號免費註冊),支持我創作教學文章,回饋由LikeCoin基金會出資,完全不會花到錢,感謝大家。