LINE Bot機器人在日常生活中,相信都可以看到它的身影,像是線上購物平台的優惠活動或最新消息推播等,藉此來和使用者進行互動。所以筆者也開發了一個美食餐廳機器人(FoodLineBot),來幫助使用者找到理想的餐廳。
本文延續[Python+LINE Bot教學]6步驟快速上手LINE Bot機器人文章,在瞭解LINE Bot的執行架構,建置了一個基本的答話功能後,接下來就要來教各位如何整合Python網頁爬蟲,將取得的訊息,透過LINE Bot自動回覆給使用者。
本文的重點包含:
- LINE Bot專案架構
- Python網頁爬蟲
- LINE Bot整合Python網頁爬蟲
一、LINE Bot專案架構
首先,來複習一下[Python+LINE Bot教學]6步驟快速上手LINE Bot機器人文章中的專案架構,如下圖:
也就是在Django專案中,新增一個Python檔案,在其中撰寫網頁爬蟲的程式碼,最後將取得的資料透過文字訊息回應給使用者。
為了簡化教學內容,讓讀者易於學習,所以本文只會爬取一個美食網站的資訊作為範例,當讀者熟悉後,即可依照需求爬取更多的網站。
二、Python網頁爬蟲
本文將以知名美食網站愛食記為例,爬取其中的餐廳資訊,首頁如下圖:
接下來,利用以下的指令安裝Requests及BeautifulSoup套件,分別用來請求存取與解析網頁的HTML原始碼:
$ pip install requests $ pip install beautifulsoup4
開啟Django專案,在應用程式(foodlinebot)下,新增一個scraper.py檔案,如下圖:
在scraper.py檔案中,會使用Python物件導向的多型(Polymorphism)概念來進行設計,可以參考[Python物件導向]Python多型(Polymorphism)實用教學文章,目的是除了保有每一個美食網頁爬蟲都擁有共同的介面外,未來如果需要爬取更多的美食網站時,也能夠易於擴充,其概念如下圖:
首先,引用剛剛所安裝的BeautifulSoup與Requests套件,以及抽象類別(ABC),如下範例:
from bs4 import BeautifulSoup from abc import ABC, abstractmethod import requests
接著,定義一個美食抽象類別,其中包含地區(area)屬性及爬蟲抽象方法(scrape),如下範例:
from bs4 import BeautifulSoup from abc import ABC, abstractmethod import requests # 美食抽象類別 class Food(ABC): def __init__(self, area): self.area = area # 地區 @abstractmethod def scrape(self): pass
第13行的抽象方法(abstractmethod)就是共同的介面,未來新增的美食網頁爬蟲,就可以依據各自的邏輯來實作這個介面。
回到愛食記網站,假設在首頁「搜尋地點」的地方選擇台北市時,可以看到網址如下圖:
開啟Python網頁爬蟲scraper.py檔案,新增IFoodie類別(Class),並且繼承自Food抽象類別,在scrape方法(Method)中,利用requests模組發送GET請求到剛剛所看到的網址結構,如下範例:
from bs4 import BeautifulSoup from abc import ABC, abstractmethod import requests # 美食抽象類別 class Food(ABC): def __init__(self, area): self.area = area # 地區 @abstractmethod def scrape(self): pass # 愛食記爬蟲 class IFoodie(Food): def scrape(self): response = requests.get( "https://ifoodie.tw/explore/" + self.area + "/list?sortby=popular&opening=true")
有了response(回應結果)的物件後,就可以利用BeautifulSoup套件來解析網頁中的HTML原始碼,如下範例:
from bs4 import BeautifulSoup from abc import ABC, abstractmethod import requests # 美食抽象類別 class Food(ABC): def __init__(self, area): self.area = area # 地區 @abstractmethod def scrape(self): pass # 愛食記爬蟲 class IFoodie(Food): def scrape(self): response = requests.get( "https://ifoodie.tw/explore/" + self.area + "/list?sortby=popular&opening=true") soup = BeautifulSoup(response.content, "html.parser")
接下來,要爬取每個餐廳卡片(Card)中的資料,所以在網站中任一間的餐廳名稱上,點擊右鍵,選擇「檢查」,在「Elements(元素)」的頁籤下,來觀察餐廳卡片(Card)中的HTML元素,如下圖:
from bs4 import BeautifulSoup from abc import ABC, abstractmethod import requests # 美食抽象類別 class Food(ABC): def __init__(self, area): self.area = area # 地區 @abstractmethod def scrape(self): pass # 愛食記爬蟲 class IFoodie(Food): def scrape(self): response = requests.get( "https://ifoodie.tw/explore/" + self.area + "/list?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變數 content += f"{title} \n{stars}顆星 \n{address} \n\n" return content
範例中第28行取得前五筆的餐廳卡片資料,而每個卡片中都包含各自的餐廳名稱、評價及地址,所以,第32行透過Python迴圈讀取每個餐廳的卡片資料,最後,將取得的資訊連結成一個字串,指派給content(最終結果)。
三、LINE Bot整合Python網頁爬蟲
Python網頁爬蟲的部分建置完成後,接下來開啟應用程式(foodlinebot)下的views.py檔案,延續[Python+LINE Bot教學]6步驟快速上手LINE Bot機器人文章,在上方引用的部分,增加scraper.py檔案中的IFoodie類別(Class),如下範例第10行:
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 from .scraper import IFoodie line_bot_api = LineBotApi(settings.LINE_CHANNEL_ACCESS_TOKEN) parser = WebhookParser(settings.LINE_CHANNEL_SECRET)
接下來,在callback檢視函式(View Function)中,利用print()函式來看一下當使用者發送訊息給FoodLineBot時,LINE Platform所傳遞過來的訊息格式,如下範例第10行:
@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) # 傳入的事件 print(events) except InvalidSignatureError: return HttpResponseForbidden() except LineBotApiError: return HttpResponseBadRequest() for event in events: if isinstance(event, MessageEvent): # 如果有訊息事件 line_bot_api.reply_message( # 回應傳入的訊息文字 event.reply_token, TextSendMessage(text=event.message.text) ) return HttpResponse() else: return HttpResponseBadRequest()
執行結果
[{ "message": { "id": "xxxxxxxxxxx", "text": "\u54c8\u56c9", #哈囉Unicode編碼 "type": "text" }, "mode": "active", "replyToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "source": { "type": "user", "userId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, "timestamp": 1593873903583, "type": "message" #訊息型態 }]
從執行結果可以看到,當使用者發送「哈囉」訊息時,FoodLineBot會收到這樣的訊息格式,其中第4行的text就是使用者發送的文字(哈囉)Unicode編碼,而第14行則是訊息的型態。
所以在範例程式中透過Python迴圈讀取傳入的訊息事件時,第18行判斷如果訊息型態為MessageEvent的物件,則利用Messaging API的reply_message(回覆訊息),指定回覆TextSendMessage(文字訊息)。
這時候,就可以利用這樣的觀念,將使用者所發送的地區訊息,傳入Python網頁爬蟲,取得該地區前五間最高人氣且營業中的餐廳資訊,回應給使用者,如下範例:
@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) # 傳入的事件 print(events) except InvalidSignatureError: return HttpResponseForbidden() except LineBotApiError: return HttpResponseBadRequest() for event in events: if isinstance(event, MessageEvent): # 如果有訊息事件 food = IFoodie(event.message.text) #使用者傳入的訊息文字 line_bot_api.reply_message( # 回應前五間最高人氣且營業中的餐廳訊息文字 event.reply_token, TextSendMessage(text=food.scrape()) ) return HttpResponse() else: return HttpResponseBadRequest()
執行結果
範例程式中,第20行利用event.message.text取得使用者傳入的地區訊息文字來建立IFoodie物件,接著在第24行,呼叫scrape()方法(Method)爬取該地區前五間最高人氣且營業中的餐廳資料。
四、小結
以上就是LINE Bot整合Python網頁爬蟲的實作方式,當然,為了提升使用者的互動體驗,還有以下兩點能夠進行改善:
- 如何提供選單讓使用者進行地區的選擇?
- 如何透過對談互動的方式,蒐集使用者的需求,像是美食分類及平均消費價格等,再進行網頁爬取的動作?
這些將於下一篇教學文章,來和大家分享,希望本文能夠幫助到您,歡迎分享給身邊對LINE Bot或Python網頁爬蟲有興趣的朋友。
有想要看的教學內容嗎?歡迎利用以下的Google表單讓我知道,將有機會成為教學文章,分享給大家😊
Python網頁爬蟲推薦課程
Python LINE Bot推薦書籍
你可能有興趣的文章
請問HTML那張圖,是點檢查-->Elements這裡找嗎?
回覆刪除因為我找不到版主貼的圖,所以想問一下,謝謝。
您好:
刪除沒錯,有稍微修改了一下文章中的說明,希望有解決您的問題 :)
您好,我在實作時有遇到這個錯誤,雖然訊息有成功回傳,但不知道是不是有什麼貓膩呢
回覆刪除----------------------------------------
Exception occurred during processing of request from ('127.0.0.1', 63656)
Traceback (most recent call last):
File "/Users/user/.pyenv/versions/3.9.0/lib/python3.9/socketserver.py", line 650, in process_request_thread
self.finish_request(request, client_address)
File "/Users/user/.pyenv/versions/3.9.0/lib/python3.9/socketserver.py", line 360, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/Users/user/.pyenv/versions/3.9.0/lib/python3.9/socketserver.py", line 720, in __init__
self.handle()
File "/Users/user/.pyenv/versions/3.9.0/lib/python3.9/site-packages/django/core/servers/basehttp.py", line 174, in handle
self.handle_one_request()
File "/Users/user/.pyenv/versions/3.9.0/lib/python3.9/site-packages/django/core/servers/basehttp.py", line 182, in handle_one_request
self.raw_requestline = self.rfile.readline(65537)
File "/Users/user/.pyenv/versions/3.9.0/lib/python3.9/socket.py", line 704, in readinto
return self._sock.recv_into(b)
ConnectionResetError: [Errno 54] Connection reset by peer
上網查了一下這個error好像是出現在前端表單以及ajax各自發送一個http request導致的塞車,如這篇文章https://www.programmersought.com/article/14364722080/,只是在我們的程式裡應該沒有前端跟script衝突的問題才是?找了好久不知道問題在哪裡,再請版主撥冗回覆,謝謝
想要詢問,我只導入了from bs4 import BeautifulSoup 這樣子機器人就會報錯,是甚麼原因呢?
回覆刪除你有pip install 嗎
刪除你好請問一下如出現HTTP/1.1" 500 90383是有什麼問題呢?
回覆刪除寫的真棒!
回覆刪除作者已經移除這則留言。
回覆刪除Requested setting LINE_CHANNEL_ACCESS_TOKEN, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.
回覆刪除找不到要設定什麼
後來把鑰匙跟密碼直接打上去就可以了
刪除但是測試跑完line沒有回傳東西
請問我在引進googlemapapi後 想要傳來自googlemap的照片,但一直跳出Broken pipe from ('127.0.0.1', 57577)請問如何解決
回覆刪除error=Error.new_from_json_dict(response.json)
回覆刪除linebot.exceptions.LineBotApiError: LineBotApiError: status_code=400, request_id=c1400241-7fb2-4381-8f74-40e6ea732932, error_response={"details": [{"message": "May not be empty", "property": "messages[0].text"}], "message": "The request body has 1 error(s)"}, headers={'Content-Type': 'application/json', 'Server': 'envoy', 'x-content-type-options': 'nosniff', 'x-frame-options': 'DENY', 'x-line-request-id': 'c1400241-7fb2-4381-8f74-40e6ea732932', 'x-xss-protection': '1; mode=block', 'Content-Length': '118', 'Expires': 'Tue, 28 Mar 2023 14:37:03 GMT', 'Cache-Control': 'max-age=0, no-cache, no-store', 'Pragma': 'no-cache', 'Date': 'Tue, 28 Mar 2023 14:37:03 GMT', 'Connection': 'close'}
請問這該怎麼辦