此作業完整的程式碼有放在我的Github上。

一、作業內容

以下為老師作業的題目:

製作一個Line機器人,用戶上傳照片,會將照片搜集回本地的專案資料夾內

老師上課有示範如何以docker-compose建立Line機器人,其建構的容器共有3個,分別為MariaDB、Jupyter及Ngrok,此示範的檔案有放在Github:aeon33system/docker_Advanced_ngrok

這份作業參考老師提供的範例架構進行修改,在資料庫部分改為MySQL,Flask從Jupyter改為直接以Python執行,Ngrok則維持和範例相同的做法。

Line機器人在原本的範例中只會做訊息回應,此作業將加上照片蒐集功能。除此之外,也會撰寫程式碼,將使用者傳送的訊息或照片檔案位置紀錄在資料庫內。

二、作業執行環境說明

  • 採用VMware Workstation 16 Pro建立虛擬機器
  • 主體作業系統:Windows 10
  • 客體作業系統:CentOS7

Docker部署在虛擬機CentOS7作業系統上,部署前資料夾架構如下:

.
├── app.py
├── docker-compose.yml
├── dockerfile-flask
├── get_ngrok_url.sh
├── image
├── mysql-init.sql
├── requirements.txt
└── secretFile.txt
  • 檔案簡介:
    • app.py:為Flask程式碼,主要負責Line機器人對使用者傳的訊息及照片進行對應處理
    • docker-compose.yml:為Docker部署的yaml檔案
    • dockerfile-flask:部署Python容器時要使用的Dockerfile,在docker-compose.yml檔案內會引用到
    • get_ngrok_url.sh:此為bash檔案,在Ngrok部署好後執行,可取得Ngrok容器內的公開網址
    • image:此為資料夾,主要存放使用者傳給Line機器人的照片檔案
    • mysql-init.sql:部署MySQL容器時初始化資料庫所要執行的語句,主要為建立資料庫和資料表,用於儲存使用者傳的訊息和照片
    • requirements.txt:此為app.py所需要的套件,在dockerfile-flask檔案內會用到
    • secretFile.txt:儲存Line機器人的token和SQL資料庫的連線資訊,在app.py內會讀取使用

三、編寫docker-compose.yml

1. MySQL

MySQL的yaml寫法主要參考Docker Hub官方說明頁面,並另外撰寫mysql-init.sql檔案,讓Docker在起始部署時,能夠建立待會要儲存文字內容和圖片存放位置的資料庫和資料表。

  • docker-compose.yml檔案中的MySQL內容如下:
version: "3.1"
services:

  db:
    image: mysql
    container_name: chatbot_db
    restart: always
    ports:
      - "3306:3306"
    environment:
      - MYSQL_ROOT_PASSWORD=123456
    volumes:
      - ./mysql-init.sql:/docker-entrypoint-initdb.d/mysql-init.sql
      - ./mysql_data:/var/lib/mysql
  • 說明:

    • MYSQL_ROOT_PASSWORD=123456為資料庫root帳號的密碼,此處可自行修改。
  • mysql-init.sql檔案如下,為MySQL程式碼:

# 建立資料庫
CREATE SCHEMA  if not exists `linebot`;

# 建立儲存圖片的資料表
CREATE TABLE if not exists `linebot`.`upload_fig` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `time` DATETIME NULL,
  `file_path` TEXT NULL COMMENT '圖片存放路徑',
  PRIMARY KEY (`id`))
COMMENT = '上傳圖片資料表';

# 建立儲存訊息的資料表
CREATE TABLE if not exists `linebot`.`msg` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `time` DATETIME NULL,
  `msg` TEXT NULL COMMENT '使用者輸入訊息',
  PRIMARY KEY (`id`))
COMMENT = '使用者輸入訊息資料表';

2. Python(Flask)

Python部署的部分參考Docker官方文件範例,此部分除了撰寫docker-compose.yml外,另外撰寫dockerfile。

  • docker-compose.yml檔案中的Python內容如下:
version: "3.1"
services:

  web:
    build:
      context: .
      dockerfile: dockerfile-flask
    container_name: chatbot_web
    restart: always
    ports:
      - "5000:5000"
    depends_on:
      - db
    volumes:
      - .:/code
    environment:
      FLASK_ENV: development
  • dockerfile-flask內容如下:
FROM python:3.7-alpine
WORKDIR /code
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
EXPOSE 5000
COPY . .
CMD ["flask", "run"]
  • 說明:

    • FROM python:3.7-alpine:要使用的Docker Image
    • WORKDIR /code:容器內執行Python的路徑
    • ENV FLASK_APP=app.py:代表說待會要執行的flask主程式檔名是app.py檔案
    • ENV FLASK_RUN_HOST=0.0.0.0:代表FLASK要讓其他人都可以連入
    • COPY requirements.txt requirements.txt:將requirement.txt檔案放到容器內,requirement.txt檔案是執行app.py所需要的套件
    • RUN pip install -r requirements.txt:指依據requirement.txt內的套件名稱進行安裝
  • requirement.txt檔案內容,為Python所需安裝套件的清單:

flask
requests 
line-bot-sdk 
mysql-connector-python

3. Ngrok

Ngrok直接沿用老師的範例寫法。

  • docker-compose.yml檔案中的Ngrok內容如下:
version: "3.1"
services:

  ngrok:
    image: wernight/ngrok
    container_name: chatbot_ngrok
    tty: true
    stdin_open: true
    restart: always
    ports:
      - "4040:4040"
    depends_on:
      - web
    command: ngrok http chatbot_web:5000 -region ap

四、撰寫Flask檔案

app.py檔案為Python架構Flask的程式碼,以下將針對此檔案程式碼依序說明:

首先載入相關套件:

# 載入相關套件
from flask import Flask, request, abort
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage, ImageMessage
from datetime import datetime
import json
import mysql.connector

接下來利用json.load函數將secretFile.txt讀進來。secretFile.txt裡面放置Line機器人需要的channelAccessToken和channelSecret,以及MySQL的相關連線資訊。

# 讀取linebot和mysql連線資訊
secretFile = json.load(open('secretFile.txt', 'r'))

secretFile.txt的檔案內容如下:

{
  "channelAccessToken":"[您的channelAccessToken]",
  "channelSecret":"[您的channelSecret]",
  "host":"chatbot_db",
  "port":3306, 
  "user":"root",
  "passwd":"123456"
}

其中,Line機器人的channelAccessToken和channelSecret需要從Line Developer網站申請取得。

接下來為建立Flask:

# 建立Flask
app = Flask(__name__)

以下開始撰寫Line機器人的處理邏輯,此處參考Github:line /line-bot-sdk-python說明。

取得Line機器人驗證資訊:

# 讀取LineBot驗證資訊
line_bot_api = LineBotApi(secretFile['channelAccessToken'])
handler = WebhookHandler(secretFile['channelSecret'])

Line機器人接收使用者訊息部分:

# linebot接收訊息
@app.route("/", methods=['POST'])
def callback():
    # get X-Line-Signature header value: 驗證訊息來源
    signature = request.headers['X-Line-Signature']

    # get request body as text: 讀取訊息內容
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        print("Invalid signature. Please check your channel access token/channel secret.")
        abort(400)

    return 'OK'

Line機器人接收使用者文字訊息後對應動作,此處在接受文字訊息後,除了回應「收到您的訊息囉!」外,也會將使用者的訊息記錄到資料庫中:

# linebot處理文字訊息
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    # linebot回傳訊息
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text='收到您的訊息囉!'))

    # 將文字資訊紀錄至資料庫
    conn = mysql.connector.connect(
        host=secretFile['host'],  # 連線主機名稱
        user=secretFile['user'],  # 登入帳號
        password=secretFile['passwd'])  # 登入密碼
    cursor = conn.cursor()
    query = 'INSERT INTO linebot.msg (time, msg) VALUES (%s, %s)'
    value = (datetime.now().strftime("%Y-%m-%d %H:%M:%S"), event.message.text)
    cursor.execute(query, value)
    conn.commit()
    conn.close()

Line機器人接收使用者照片訊息後對應動作,在接受照片訊息後,將使用者傳的圖片放到image資料後,回應「收到您上傳的照片囉!」,並將圖片儲存的名稱紀錄至資料庫內:

# linebot處理照片訊息
@handler.add(MessageEvent, message=ImageMessage)
def handle_image_message(event):
    # 使用者傳送的照片
    message_content = line_bot_api.get_message_content(event.message.id)

    # 照片儲存名稱
    fileName = event.message.id + '.jpg'

    # 儲存照片
    with open('./image/' + fileName, 'wb')as f:
        for chunk in message_content.iter_content():
            f.write(chunk)

    # linebot回傳訊息
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text='收到您上傳的照片囉!'))

    # 將照片路徑資訊紀錄至資料庫
    conn = mysql.connector.connect(
        host=secretFile['host'],  # 連線主機名稱
        user=secretFile['user'],  # 登入帳號
        password=secretFile['passwd'])  # 登入密碼
    cursor = conn.cursor()
    query = 'INSERT INTO linebot.upload_fig (time, file_path) VALUES (%s, %s)'
    value = (datetime.now().strftime("%Y-%m-%d %H:%M:%S"), fileName)
    cursor.execute(query, value)
    conn.commit()
    conn.close()

開始啟動Flask:

# 開始運作Flask
if __name__ == "__main__":
    app.run(host='0.0.0.0')

五、執行部署

執行部署前資料夾內容如下:

.
├── app.py
├── docker-compose.yml
├── dockerfile-flask
├── get_ngrok_url.sh
├── image
├── mysql-init.sql
├── requirements.txt
└── secretFile.txt

執行請記得至secretFile.txt輸入自己Line機器人的channelAccessTokenchannelSecret,輸入位置如下:

在當前資料夾路徑上,輸入docker-compose指令開始部署:

docker-compose up -d

啟用後以docker指令檢查容器運作狀況:

docker-compose ps -a

接下來執行get_ngrok_url.sh檔案,此檔案是去Ngrok的容器內取得公開網址:

bash get_ngrok_url.sh

執行後畫面後會印出Ngrok的公開網址:

將此公開網址貼到Line Developer Messaging API的Webhook settings內:

(備註:如果公開網址不是https,可以自行修改為https,一樣可以連線。)

設定好後,接下來即可透過Line機器人進行測試:

為確認資料庫內是否有收到資料,可透過MySQL Workbench來連線查詢:

查詢輸入訊息:

查詢儲存的圖片資訊:

用戶上傳的照片部分,也存到本地端:

六、心得

由於老師提供Line機器人的範例是在Jupyter Notebook上手動執行Flask,我想說這部分應該可以改為Python容器,直接部署節省手動操作的部分。

首先我在Jupyter環境把Line機器人的程式碼撰寫完,並測試程式碼沒問題後,開始將Jupyter容器改為Python容器,看能不能直接在Python容器上執行Flask。

然而轉換到Python容器的過程並不是很順利,官網只提供docker rundocker build的寫法,關於docker-compose部署Python的文件並沒有說明,所以只能靠google搜尋並自己嘗試撰寫,但一直部署失敗。後來我發現Docker官網有提供部署Flask的簡單範例,先做一個簡單的Flask部署,慢慢增加所需要的設定與功能,最後成功測試出來。

會做這份作業是為自己未來的專題及工作先行準備,目前國內在Line機器人這塊市場需求還是很大,許多職缺都要求要會Line機器人。透過這份作業,可以了解Line機器人運作的框架以及架設方式,對於docker-compose的功能也更加熟悉。