2021年8月8日 星期日

實作 Flask 框架連結資料庫的網站

學習目標:
  • 了解 Flask 框架連結資料庫的用法!
  • 實作網站CRUD功能!

Flask 框架連結資料庫
  1. 修改 app/router.py 內容,在檔尾增加下列程式內容:
    (前方略過....)
    # 導向查詢菜單內容的網頁
    @app.route("/showmenu")
    def showmenu():
        menulists = connectDB.showallMenu()
        return render_template("menu.html", menulist=menulists)
    
  2. 新增 app/templates/menu.html 檔案,內容如下:
    {% extends "base.html" %}
    {% block title %}大學麵店菜單系統{% endblock %}

    {% block main %}
    <div class="container my-5 py-5">
    <div class="row">
    <div class="rol">
    <h1>菜單列表</h1>
    </div>
    </div>

    {% for menu in menulist %}
    <div class="row">
    {% for item in menu %}
    <div class="col">
    <p>{{ item }}</p>
    </div>
    {% endfor %}
    </div>
    {% endfor %}
    </div>
    {% endblock %}
  3. 推送上 Heroku 專案!利用瀏覽器查看站台: https://你的app.herokuapp.com/showmenu
  4. 修改 app/templates/menu.html 檔案,增加巨集內容(macro)如下:
    {% extends "base.html" %}
    {% block title %}大學麵店菜單系統{% endblock %}

    {% block main %}

    <!-- 宣告巨集 -->
    {% macro show_row(data, tag) -%}
    {% for menu in data %}
    <div class="row">
    {% for item in menu %}
    <div class="col">
    <{{ tag }}>{{ item }}</{{ tag }}>
    </div>
    {% endfor %}
    </div>
    {% endfor %}
    {%- endmacro %}
    <!-- 宣告結束 -->

    <div class="container my-5 py-5">
    <div class="row">
    <div class="rol">
    <h1>菜單列表</h1>
    </div>
    </div>
    <!-- 顯示菜單用 -->
    {% set cols = (("菜單項次","菜單名稱","價錢/每碗"),)%}

    {{ show_row(cols, "h2") }}
    <hr class="my-4">
    {{ show_row(menulist,"p") }}
    </div>

    {% endblock %}
  5. 推送上 Heroku 專案!利用瀏覽器查看站台: https://你的app.herokuapp.com/showmenu

實作 CRUD 連結資料庫
  1. 新增 app/templates/addmenu.html 檔案,內容如下:
    <form action="/addmenu" method="post">
    <label for="menuname">菜單名稱</label>
    <input type="text" id="menuname" placeholder="請輸入菜單名稱" name="menuname">
    <br>
    <label for="menuprize">菜單價錢</label>
    <input type="text" id="menuprize" placeholder="請輸入價格" name="menuprize">
    <br>
    <input type="submit" value="送出">
    </form>
  2. 修改 app/router.py 內容,在檔尾增加下列程式內容:
    (前方略過....)
    # 新增菜單內容的網頁
    @app.route("/addmenu", methods=['GET', 'POST'])
    def addmenu():
        if request.method == 'POST':
            data = ""
            for key, value in request.form.items():
                print(value)
                data += value
                data += ','
            data = data[:-1]
            connectDB.addMenu(data)
            print(data)
            menulists = connectDB.showallMenu()
            return render_template("menu.html", menulist=menulists)
        else:
            return render_template("addmenu.html")
    
  3. 推送上 Heroku 專案!利用瀏覽器查看站台: https://你的app.herokuapp.com/showmenu
  4. 修改 app/router.py 內容,在檔尾增加下列程式內容:
    (前方略過....)
    # 刪除菜單內容的網頁
    @app.route("/delmenu", methods=['GET', 'POST'])
    def delmenu():
        if request.method == 'POST':
            print(request.form)
            data = ""
            data1 = ""
            for key, value in request.form.items():
                data = eval(''.join(value))
                data1 = str(data[0])+','+str(data[1])
                print(data1)
                connectDB.deleteMenu(data1)
    
        menulists = connectDB.showallMenu()
        return render_template("menu.html", menulist=menulists)
    
  5. 修改 app/templates/menu.html 檔案,內容如下:
    {% extends "base.html" %}
    {% block title %}大學麵店菜單系統{% endblock %}

    {% block main %}

    <!-- 宣告巨集 -->
    {% macro show_row(data, tag) -%}
    {% for menu in data %}
    <div class="row">
    {% if menu[3] == 'Action' %}
    {% for item in menu %}
    <div class="col">
    <{{ tag }}>{{ item }}</{{ tag }}>
    </div>
    {% endfor %}
    {% else %}
    {% for item in menu %}
    <div class="col">
    <{{ tag }}>{{ item }}</{{ tag }}>
    </div>
    {% endfor %}
    <div class="col">
    <button type="submit" value="{{ menu[1],menu[2] }}" name="deldata">刪除</button>
    </div>
    {% endif %}
    </div>
    {% endfor %}
    {%- endmacro %}
    <!-- 宣告結束 -->

    <div class="container my-5 py-5">
    <div class="row">
    <div class="rol">
    <h1>菜單列表</h1>
    </div>
    </div>
    <!-- 顯示菜單用 -->
    {% set cols = (("菜單項次","菜單名稱","價錢/每碗","Action"),)%}

    {{ show_row(cols, "h2") }}
    <hr class="my-4">
    <form action="/delmenu" method="post">
    {{ show_row(menulist,"p") }}
    </form>
    <form action="/addmenu" method="get">
    <input type="submit" value="新增菜單">
    </form>
    </div>

    {% endblock %}
  6. 推送上 Heroku 專案!利用瀏覽器查看站台: https://你的app.herokuapp.com/showmenu

2021年8月3日 星期二

利用 Flask 建立支援 Line Bot 的後勤網站

學習目標:
  • 了解 Flask 的用法!
  • 建立支援 Line Bot 機器人的網站!

利用 Bootstrap 製作 Flask Web 站台網頁
  1. 開啟 templates/home.html 檔案,導入 Bootstrap 連結:
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>MySite</title>
    </head>
    <body>
    Hello , Hacker !!
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
    </body>
    </html>
  2. 使用導航列!
    (前方略過....)
    <body>
    <nav class="navbar navbar-expand-sm navbar-dark bg-primary">
    <ul class="navbar-nav">
    <li class="nav-item">
    <a class="nav-link" href="#">Home</a>
    </li>
    <li class="nav-item">
    <a class="nav-link" href="#">Link</a>
    </li>
    </ul>
    </nav> (後方略過....)
  3. 使用拆疊式按鍵,方便做成 RWD 網頁
    (前方略過....)
    <body>
    <nav class="navbar navbar-expand-sm navbar-dark bg-primary">

    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#collapsibleNavBar">
    <span class="navbar-toggler-icon"></span>
    </button>

    <div class="collapse navbar-collapse" id="collapsibleNavBar">
    <ul class="navbar-nav"> (後方略過....)
  4. 使用 Jumbotron
    (前方略過....)
    </nav>
    <div class="jumbotron">
    <h1>Hello , Hacker !!</h1>
    <p>這是測試頁...
    </div> (後方略過....)
  5. 使用 Google fonts :
    (前方略過....)
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300&display=swap" rel="stylesheet">

    <title>MySite</title>
    <style> body {font-family: 'Noto Sans TC', sans-serif;}</style>
    </head> (後方略過....)
利用 Jinja2 模板引擎打造網頁
  1. 將 home.html 檔案,複製到 base.html 檔案
  2. 修改 base.html 檔案中,可能異動的部份:
    (前方略過....)
    <title>{% block title %}{% endblock %}</title>
    (中間略過....) 
    <!--取代 jumbotron 那一段-->
    <main>
    {% block main %}{% endblock %}
    </main>

    <footer>
    </footer> (中間略過....) {% block script %}{% endblock %}
    </body>
  3. 修改 home.html 檔案,去掉重複的部份,並加入 base.html 樣板檔:
    {% extends "base.html" %}
    {% block title %}My Web Site{% endblock %}
    {% block main %}
    <div class="jumbotron">
    <h1>Hello , Hacker !!</h1>
    <p>這是測試頁...</p>
    </div>
    <div class="container my-5 py-5">
    <div class="row">
    <div class="col">
    <h2>左側</h2>
    </div>
    <div class="col">
    <h2>中間</h2>
    </div>
    <div class="col">
    <h2>右側</h2>
    </div>
    </div>
    </div>
    {% endblock %}
  4. 送上 Heroku 主機,測試看看內容!
  5. 修改 app/router.py 檔案內容,加入導向查詢菜單的網頁位置:
    # 導向查詢菜單內容的網頁
    app.route("/showmenu")
    def showmenu():
        return render_template("menu.html")
    
  6. 新增菜單的網頁 app/templates/menu.html:
    {% extends "base.html" %}
    {% block title %}大學麵店菜單系統{% endblock %}

    {% block main %}
    <div class="container my-5 py-5">
    <div class="row">
    <div class="rol">
    <h1>菜單列表</h1>
    </div>
    </div>

    {% for menu in menulist %}
    <div class="row">
    <div class="col">
    <p>{{ menu[0] }}</p>
    </div>
    <div class="col">
    <p>{{ menu[1] }}</p>
    </div>
    <div class="col">
    <p>{{ menu[2] }}</p>
    </div>
    </div>
    {% endfor %}
    </div>
    {% endblock %}
  7. 送上 Heroku 主機,測試看看內容!

2021年7月30日 星期五

Line Bot 上的 FlexMessage 用法

學習目標:
  • 了解 Line Bot 上的 FlexMessage 用法!
  • 設定 FlexMessage 連結 Line Bot 機器人資料庫!

產生 FlexMessage 程式碼
  1. 登入 Line Developers ,查看 Flex Message Simulator
  2. 利用「Showcase」選擇自己想要的範例!利用「View as JSON」可以查看檔案內容!
  3. 新增 app/flexmodules 子目錄,方便存放Flex Message 程式!
  4. 利用 Python 程式,產生所需要的 JSON 檔案格式,傳送給 Line 平台!例:flexmessages.py
    def fleximage(url):
        return { "type": "image",
                 "url": url,
                 "size": "full",
                 "aspectRatio": "20:13",
                 "aspectMode": "cover"}
                 
    def flextext(text,size,color,weight='regular',wrap=False):
        return { "type": "text",
                 "text": text,
                 "size": size,
                 "color": color,
                 "weight": weight,
                 "wrap": wrap }
                 
    def flexlogo(text='大學麵店'):
        return flextext(text, size='md',color='#066BAF', weight='bold')
    
    def flextitle(text):
        return flextext(text, size='xl',color='#066BAF', weight='bold')
        
    def flexhead(text):
        return flextext(text, size='xl',color='#555555')
        
    def flexnote(text):
        return flextext(text, size='md',color='#AAAAAA', wrap=True)
        
    def flexseparator(margin='xl'):
        return { "type": "separator", "margin": margin }
        
    def flexbutton(label, data, display_text):
        return { "type": "button",
                 "action": {
                     "type": "postback",
                     "label": label,
                     "data": data,
                     "display_text": display_text },
                     "style": "link",
                     "color": "#066BAF",
                     "height": "sm" }
                     
    def flex_index():
        hero_url = "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_2_restaurant.png"
        bodys = [flexlogo(),
        		 flextitle('歡迎光臨'),
                 flexseparator(),
                 flexhead('菜單列表'),
                 flexnote('# 查詢所有資料'),
                 flexseparator()]
        footers = [flexbutton('菜單列表','菜單','菜單列表'),
                   flexbutton('單項查詢','單項','單項菜單查詢') ]
    
        FlexMessage = {'type': 'bubble',
                        'hero': fleximage(hero_url),
                        'body': {
                            'type': 'box',
                            'layout': 'vertical',
                            'spacing': 'md',
                            'contents': bodys},
                        'footer': {
                            'type': 'box',
                            'layout': 'vertical',
                            'contents': footers}}
        return FlexMessage
    
  5. 新增程式 app/flexmodules/flextalks.py ,回應 line bot 平台的訊息!
    from app.flexmodules import flexmessages
    from linebot.models import FlexSendMessage
    from app import line_bot_api, handler
    
    def query_menu(event):
        if '菜單查詢' in event.message.text:
            line_bot_api.reply_message(
                event.reply_token,
                FlexSendMessage(alt_text='query record: index',contents=flexmessages.flex_index())
            )
            return True
        else:
            return False
    
  6. 修改 app/linebotmodules.py 裡的程式,加上呼叫 flextalks.py 的程式!
    (前面略過....)
            replay = False
    
            if not replay:
                reply = flextalks.query_menu(event)
    (後面略過....)
    
  7. 上傳至 Heroky 專案內!進行測試!
製作 PostbackEvent 程式碼
  1. 修改 app/linebotmodules.py 裡的程式!在最後一行加上下列程式碼:
    (前面略過....)
    # line 回應的訊息
    @handler.add(PostbackEvent)
    def handle_postback(event):
        print(event)
        flextalks.query_menu_back(event)
    
  2. 修改程式 app/flexmodules/flextalks.py ,import 資料庫連結程式,並加入回應 line bot 平台的訊息!
    (前面略過....)
    from app import line_bot_api, handler
    from app.dataSQL import connectDB
    (中間略過....)
    def query_menu_back(event):
        query = event.postback.data
        print(query)
        if '=' in query:
            print(query.split('=')[1])
            data = connectDB.queryItem(query.split('=')[1])
            menu_name = [i[2] for i in data]
            line_bot_api.reply_message(
                event.reply_token,
                FlexSendMessage(
                    alt_text=f"query record: column {query}",
                    contents= flexmessages.flex_menu_prize(query,menu_name)
                )
            )
            return True
        elif '菜單' in query:
            data = connectDB.showallMenu()
            menu_name = [i[1] for i in data]
            line_bot_api.reply_message(
                event.reply_token,
                FlexSendMessage(
                    alt_text=f"query record: column {query}",
                    contents= flexmessages.flex_menu(query,menu_name)
                )
            )
            return True
        else:
            return False
    
  3. 修改程式 app/flexmodules/fflexmessages.py,在檔案最後面,加上下列程式:
    (前面略過....)
    def flex_menu(keywords, data):
        bodys = [flexlogo(),
        		 flextitle(f'{keywords}'),
                 flexseparator()]
    
        footers = [flexbutton(f"{i}",f"{keywords}={i}",f"查詢 {i}") for i in data]
    
        FlexMessage = {'type': 'bubble',
                        'body': {
                            'type': 'box',
                            'layout': 'vertical',
                            'spacing': 'md',
                            'contents': bodys},
                        'footer': {
                            'type': 'box',
                            'layout': 'vertical',
                            'contents': footers}}
        return FlexMessage
    
    def flex_menu_prize(keywords, data):
        keyword = keywords.split('=')[1]
        bodys = [flexlogo(),
        		 flextitle(f'{keyword}'),
                 flexseparator(),
                 flexnote(f'價格:{data}'),
                 flexseparator()]
    
        footers = [flexbutton("菜單查詢","菜單查詢","菜單列表")]
    
        FlexMessage = {'type': 'bubble',
                        'body': {
                            'type': 'box',
                            'layout': 'vertical',
                            'spacing': 'md',
                            'contents': bodys},
                        'footer': {
                            'type': 'box',
                            'layout': 'vertical',
                            'contents': footers}}
        return FlexMessage
    
  4. 修改 app/dataSQL/connectDB.py,加入單項查詢的資料庫語法:
    (前面略過....)
    def queryItem(keyword):
        DATABASE_URL = os.environ['DATABASE_URL']
        connection_db = psycopg2.connect(DATABASE_URL,sslmode='require')
        cursor = connection_db.cursor()
        SQL_cmd = f"""SELECT * FROM menu WHERE menuname = %s;"""
        cursor.execute(SQL_cmd,[keyword])
        connection_db.commit()
        raws = cursor.fetchall()
        data = []
        for raw in raws:
            data.append(raw)
        
        cursor.close()
        connection_db.close()
        return data
    (後面略過....)
    
  5. 上傳 Heroku 專案後,進行測試!

2021年7月24日 星期六

連結與使用資料庫

學習目標:
  • 了解 Python 與資料庫連結方式!
  • 設定 Line Bot 機器人讀寫資料庫!

Python 與資料庫連結
  1. 在 Heroku 上,新增一個資料庫軟體:
    • 使用「Configure Add-ons」選項!
    • 選擇「Heroku Postgres」項目!
    • 「Plan Name」選用 Free 項目!再按下「Submit Order Form」!
    • 按下「Heroku Postgres」可查看使用情形!
  2. 使用文字介面視窗,安裝 psycopg2 套件,並進行資料庫的建立!
    C:\workspace\LineBot> heroku login -i
    C:\workspace\LineBot> pip install psycopg2
    C:\workspace\LineBot> python
    >>> import os
    >>> import psycopg2
    >>> heroku_pgCLI = 'heroku config:get DATABASE_URL -a 你的APP名稱'
    >>> DATABASE_URL = os.popen(heroku_pgCLI).read()[:-1]
    >>> print(DATABASE_URL)
    >>> connection_db=psycopg2.connect(DATABASE_URL, sslmode='require')
    >>> cursor = connection_db.cursor()
    >>>
    (要注意一下網址!)
    
  3. 建立一張表格,可以放入麵店的點餐單資料!
    >>> SQL_cmd = '''CREATE TABLE menu(
    ... menu_id serial PRIMARY KEY,
    ... menuname VARCHAR (150) UNIQUE NOT NULL,
    ... menuprize Integer NOT NULL
    ... );'''
    >>> cursor.execute(SQL_cmd)
    >>> connection_db.commit()
    >>> cursor.close()
    >>> connection_db.close()
    >>>
    
  4. 查看一下 Heroku 內的資料!
  5. 利用指令,查看資料庫表格相關訊息!
    >>> import os
    >>> import psycopg2
    >>> heroku_pgCLI = 'heroku config:get DATABASE_URL -a 你的APP名稱'
    >>> DATABASE_URL = os.popen(heroku_pgCLI).read()[:-1]
    >>> connection_db=psycopg2.connect(DATABASE_URL, sslmode='require')
    >>> cursor = connection_db.cursor()
    >>> SQL_cmd = '''SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'menu';'''
    >>> cursor.execute(SQL_cmd)
    >>> connection_db.commit()
    >>> data = []
    >>> while True:
    ...   temp = cursor.fetchone()
    ...   if temp:
    ...      data.append(temp)
    ...   else:
    ...      break
    ... 
    >>> print(data)
    [('menu_id', 'integer'), ('menuname', 'character varying'), ('menuprize', 'integer')]
    >>> cursor.close()
    >>> connection_db.close()
    
    PS:刪除資料的方式:SQL_cmd = '''DROP TABLE IF EXISTS menu;''' ,再重複執行上列工作即可!
  6. 將一筆資料輸入資料表內!
    >>> import os
    >>> import psycopg2
    >>> heroku_pgCLI = 'heroku config:get DATABASE_URL -a 你的APP名稱'
    >>> DATABASE_URL = os.popen(heroku_pgCLI).read()[:-1]
    >>> connection_db=psycopg2.connect(DATABASE_URL, sslmode='require')
    >>> cursor = connection_db.cursor()
    >>> record = ('牛肉麵',170)
    >>> table_columns = '(menuname,menuprize)'
    >>> SQL_cmd = f"""INSERT INTO menu {table_columns} VALUES(%s,%s);"""
    >>> cursor.execute(SQL_cmd,record)
    >>> connection_db.commit()
    >>> cursor.close()
    >>> connection_db.close()
    
  7. 將多筆資料輸入資料表內!
    >>> import os
    >>> import psycopg2
    >>> heroku_pgCLI = 'heroku config:get DATABASE_URL -a 你的APP名稱'
    >>> DATABASE_URL = os.popen(heroku_pgCLI).read()[:-1]
    >>> connection_db=psycopg2.connect(DATABASE_URL, sslmode='require')
    >>> cursor = connection_db.cursor()
    >>> record = [('餛飩麵',100),('湯麵',80)]
    >>> table_columns = '(menuname,menuprize)'
    >>> SQL_cmd = f"""INSERT INTO menu {table_columns} VALUES(%s,%s);"""
    >>> cursor.executemany(SQL_cmd,record)
    >>> connection_db.commit()
    >>> count = cursor.rowcount
    >>> print(count," records have inserted!!")
    >>> cursor.close()
    >>> connection_db.close()
    
  8. 列出資料庫的資料表內容
    >>> import os
    >>> import psycopg2
    >>> heroku_pgCLI = 'heroku config:get DATABASE_URL -a 你的APP名稱'
    >>> DATABASE_URL = os.popen(heroku_pgCLI).read()[:-1]
    >>> connection_db=psycopg2.connect(DATABASE_URL, sslmode='require')
    >>> cursor = connection_db.cursor()
    >>> SQL_cmd = f"""SELECT * FROM menu ;"""
    >>> cursor.execute(SQL_cmd)
    >>> connection_db.commit()
    >>> temp = cursor.fetchall()
    >>> print(temp)
    >>> count = cursor.rowcount
    >>> print(count," records have geted!!")
    >>> cursor.close()
    >>> connection_db.close()
    
  9. 有條件的列出資料庫的資料表內容:
    >>> import os
    >>> import psycopg2
    >>> heroku_pgCLI = 'heroku config:get DATABASE_URL -a 你的APP名稱'
    >>> DATABASE_URL = os.popen(heroku_pgCLI).read()[:-1]
    >>> connection_db=psycopg2.connect(DATABASE_URL, sslmode='require')
    >>> cursor = connection_db.cursor()
    >>> prize = 100
    >>> SQL_cmd = f"""SELECT * FROM menu WHERE menuprize < %s;"""
    >>> cursor.execute(SQL_cmd,[prize])
    >>> connection_db.commit()
    >>> temp = cursor.fetchall()
    >>> print(temp)
    >>> count = cursor.rowcount
    >>> print(count," records have geted!!")
    >>> cursor.close()
    >>> connection_db.close()
    
  10. 更新資料庫的資料表內容:
    >>> import os
    >>> import psycopg2
    >>> heroku_pgCLI = 'heroku config:get DATABASE_URL -a 你的APP名稱'
    >>> DATABASE_URL = os.popen(heroku_pgCLI).read()[:-1]
    >>> connection_db=psycopg2.connect(DATABASE_URL, sslmode='require')
    >>> cursor = connection_db.cursor()
    >>> name = "湯麵"
    >>> prize = 50
    >>> SQL_cmd = f"""UPDATE menu SET menuprize = %s WHERE menuname = %s;"""
    >>> cursor.execute(SQL_cmd,(prize,name))
    >>> connection_db.commit()
    >>> count = cursor.rowcount
    >>> print(count," records have updated!!")
    >>> cursor.close()
    >>> connection_db.close()
    
  11. 刪除資料庫內的資料表內容:"DELETE FROM menu WHERE menuname = '';"
Python 與資料庫連結
  1. 修改 LineBot 專案的 requirements.txt:
    Flask>=1.1.1
    gunicorn>=20.0.4
    line-bot-sdk>=1.15.0
    APScheduler>=3.6.1
    psycopg2>=2.0.0
    
  2. 修改 LineBot 專案的程式碼:app/linebotmodules.py
    import app.dataSQL import callData
    (中間略過....)
    # 靈活展現文字
    @handler.add(MessageEvent, message=TextMessage)
    def replyText(event):
        if event.source.user_id == "Uf4a596a6eb65eabf52c003ffe325a21d":
           reply = False
           if not reply:
               reply = callData.showData(event)
    (接下來的程式碼,請先註解....)
    
  3. 在 app 資料夾新增 dataSQL 資料夾!
  4. 在資料夾 dataSQL 中,新增檔案 connectDB.py:
    import os
    import psycopg2
    
    def showallMenu():
        DATABASE_URL = os.environ['DATABASE_URL']
        connection_db = psycopg2.connect(DATABASE_URL,sslmode='require')
        cursor = connection_db.cursor()
        SQL_cmd = f"""SELECT * FROM menu ;"""
        cursor.execute(SQL_cmd)
        raws = cursor.fetchall()
        data = []
        for raw in raws:
            data.append(raw)
    
        return data
    
  5. 在資料夾 dataSQL 中,新增檔案 callData.py:
    from app import line_bot_api
    from linebot.models import TextSendMessage, ImageSendMessage, TemplateSendMessage, messages
    from linebot.models import ImageCarouselTemplate, ImageCarouselColumn, URIAction
    from app.dataSQL import connectDB
    
    import random
    
    def showData(event):
        if '菜單' in event.message.text:
            data = connectDB.showallMenu()
            print_text = ""
            for i in data:
                print_text += str(i[1])
                print_text += str(i[2])
                print_text += "\n"
    
            line_bot_api.reply_message(
                event.reply_token,
                TextSendMessage(text=print_text)
            )
    
            return True
        else:
            return False
    

2021年7月22日 星期四

機器人主動推送通知

學習目標:
  • 在 Flask 上,寫一個網頁,防止 Heroku 主機休眠!
  • 寫一個定時推送通知的 Line Bot 機器人程式!

寫個簡易的 Flask 小網頁
  1. 修改 app/router.py 的內容
    (前方省略....)
    from flask import render_template
    
    # 設定預設網頁
    @app.route("/")
    def home():
        return render_template("home.html")
    (後方省略....)
    
  2. 在專案目錄 LineBot 下,建立 app/templates/home.html 的 HTML 檔案:
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <title>MyLineBot</title>
    </head>
    <body>
    <h1>Hello, World</h1>
    </body>
    </html>
  3. 將專案推向 Heroku 主機....
  4. 打開瀏覽器,輸入http://你的專案.herokuapp.com/

定時推送通知的機器人程式
  1. 安裝 APScheduler
    C:\> pip install APScheduler
    
  2. 在專案目錄中,寫一個 timer.py 的時鐘程式
    from apscheduler.schedulers.blocking import BlockingScheduler
    import urllib.request
    
    scheduleEvent = BlockingScheduler()
    
    @scheduleEvent.scheduled_job('cron',day_of_week='mon-fri',minute='*/20')
    def scheduled_job():
        website = "https://mylinebothellotux.herokuapp.com/"
        connection = urllib.request.urlopen(website)
    
        for i, value in connection.getheaders():
            print(i,value)
    
    scheduleEvent.start()
    
  3. 修改 Procfile 檔案內容
    web: gunicorn main:app --preload
    clock: python timer.py
    
  4. 修改 requirements.txt 檔案內容
    Flask>=1.1.1
    gunicorn>=20.0.4
    line-bot-sdk>=1.15.0
    APScheduler>=3.6.1
    
  5. 上傳專案到 heroku
  6. 修改 Heroku 的設定值
  7. 建立主動推送通知的方式:
    line_bot_api.push_message(to, TextSendMessage(text='Morning Sir'))
    
  8. 修改成需要的樣子:linebotmodules.py
     (前方省略....)
     except:
              #hello_text = echo(event.message.text)
              line_bot_api.push_message(
                '你的 User ID',
                TextSendMessage(text='沒找到')
              )
    
  9. 推送上 Heroku !
  10. 在手機 line 上,輸入任何一個句子!

2021年7月20日 星期二

到網路上爬圖片吧!

學習目標:
  • 寫出網路爬蟲程式,將 Google 上的圖檔回傳!

寫出網路爬蟲程式
  1. 開啟瀏覽器,利用 Google 進行查詢!按下「F12」進行網址的觀察!
  2. 在 python 文字介面中,進行分析與測試:
    C:\workspace\LineBot> python
    >>>
    
  3. 導入 urllib ,對 google 進行網站的連結:
    >>> import urllib.request
    >>> url = "https://www.google.com/"
    >>> conn = urllib.request.urlopen(url)
    >>> print(conn)
    <http.client.HTTPResponse object at 0x7f0d76035550>
    
  4. 將接收的物件,轉成資料印出來:
    >>> data = conn.read()
    >>> print(data)
    (印出的資料太多了,省略一下...)
    
  5. 修正一下,將 headers 的參數加上,限制資料印出的數量:
    >>> header = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0' }
    >>> req = urllib.request.Request(url,headers=header)
    >>> conn = urllib.request.urlopen(req)
    >>> data = conn.read()
    >>> print(data)
    (印出的資料太多了,省略一下...)
    
    PS: 你有個網頁好幫手「F12」!
  6. 回瀏覽器,在 google 查詢一下某家書商的名稱,再切換成圖片,觀察一下網址變化
    PS: 注意其網址的組成!
  7. 上圖中,按下滑鼠右鍵,可以「複製網址」
  8. 將網址複製後,貼至 python 的文字介面視窗內,進行分析!
    PS:觀察之後,可以猜測:
    • q=%E5%8D%9A%E7%A2%A9: 代表查詢字串
    • tbm=isch :指的是查詢圖片
  9. 使用分析函式,進行相關網址分析:
    >>> u = urllib.request.urlparse(search_url)
    >>> print(u)
    
  10. 進行下一步的分析!
    >>> u[4]
    >>> urllib.parse.parse_qs(u[4])
    
  11. URL 分析列表,有助於組合回原來的查詢字串:
     Attribute   Index   Value   Value if not present 
    scheme 0 URL scheme specifier scheme parameter
    netloc 1 Network location part empty string
    path 2 Hierarchical path empty string
    params 3 Parameters for last path element empty string
    query 4 Query component empty string
    fragment 5 Fragment identifier empty string
    username User name None
    password Password None
    hostname Host name (lower case) None
    port Port number as integer,if present None
  12. 大致上了解其組成結構後,可以進行測試:
    >>> test = {'tbm': 'isch', 'q': '博碩'}
    >>> urllib.parse.urlencode(test)
    'tbm=isch&q=%E5%8D%9A%E7%A2%A9'
    
  13. 將下列字串,放回瀏覽器的網址列,觀查結果是否相同:
    https://www.google.com/search?tbm=isch&q=%E5%8D%9A%E7%A2%A9
    
  14. 回到文字介面中,持續進行測試:
    >>> url = f"https://www.google.com/search?{urllib.parse.urlencode(test)}/"
    >>> req = urllib.request.Request(url, headers=header)
    >>> conn = urllib.request.urlopen(req)
    >>> data = conn.read()
    >>> print(data)
    (資料出現太多,省略過去....)
    
  15. 從瀏覽器中,分析圖片位於 HTML 語法中的何處!提示:在「檢視原始碼中」,查詢關鍵字詞:"img data-src"
  16. 切回文字介面,設定關鍵字詞的樣板:正規化設定
    >>> import re
    >>> template = '"(https://encrypted-tbn0.gstatic.com[\S]*)"'
    >>> image_list = []
    >>> for i in re.finditer(template,str(data,'utf-8')):
    ...     image_list.append(i.group(1))
    >>> image_list[:5]
    
    PS: 語法注意事項
    • [\S]: 空白字元除外
    • * : 任意字數的字元
    • .group(1): 只頡取 template 字串中的有()號的內容資料
    • [:5] : 取回前五行資料!
  17. 整理下過的指令,可容易形成程式檔案:
    import urllib.request
    import re
    import random
    
    search_key_word = {'tbm': 'isch', 'q': event.message.text}
    url = f"https://www.google.com/search?{urllib.parse.urlencode(search_key_word)}/"
    header = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0' }
    req = urllib.request.Request(url, headers=header)
    conn = urllib.request.urlopen(req)
    data = conn.read()
    template = '"(https://encrypted-tbn0.gstatic.com[\S]*)"'
    image_list = []
    for i in re.finditer(template,str(data,'utf-8')):
      image_list.append(i.group(1))
    
    random_image_url = image_list[random.randint(0, len(image_list)-1)]
    
    line_bot_api.reply_message(
      event.reply_token,
      ImageSendMessage(
        original_content_url=random_image_url,
        preview_image_url=random_image_url
      )
    )
    
  18. 利用 line-bot-sdk-python 提供的 TemplateSendMessage 可以一次取得多張圖片:
    TemplateSendMessage(
      alt_text=alt_text
      template=ImageCarouselTemplate(
        columns=[ImageCarouselColumn(
          image_url='https://website/image.jpg',
          action=URIAction(uri='https://website',label='label'))]
      )
    )
    
  19. 修改 LineBot/app/linebotmodules.py 檔案,將上面試過的指令,一一寫入檔案內!
    from linebot.models.send_messages import ImageSendMessage
    from app import line_bot_api, handler
    from linebot.models import MessageEvent, TextMessage, TextSendMessage
    
    import urllib.request
    import re
    import random
    
    # 查詢 google
    @handler.add(MessageEvent, message=TextMessage)
    def replyText(event):
        if event.source.user_id == "Uf4a596a6eb65eabf52c003ffe325a21d":
    
                search_key_word = {'tbm': 'isch', 'q': event.message.text}
                url = f"https://www.google.com/search?{urllib.parse.urlencode(search_key_word)}/"
                header = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0' }
                req = urllib.request.Request(url, headers=header)
                conn = urllib.request.urlopen(req)
                data = conn.read()
                template = '"(https://encrypted-tbn0.gstatic.com[\S]*)"'
                image_list = []
                
                for i in re.finditer(template,str(data,'utf-8')):
                    image_list.append(re.sub(r'\\u003d','=',i.group(1)))
    
                random_image_url = image_list[random.randint(0, len(image_list)+1)]
    
                line_bot_api.reply_message(
                    event.reply_token,
                    ImageSendMessage(
                        original_content_url=random_image_url,
                        preview_image_url=random_image_url
                    )
                )
    
  20. 將程式推上 Heroku 主機,並且進行測試!
  21. 修改 LineBot/app/linebotmodules.py 檔案,加入 TemplateSendMessage 模組!
    from linebot.models.send_messages import ImageSendMessage
    from app import line_bot_api, handler
    from linebot.models import MessageEvent, TextMessage, TextSendMessage
    
    import urllib.request
    import re
    import random
    
    # 查詢 google
    @handler.add(MessageEvent, message=TextMessage)
    def replyText(event):
        if event.source.user_id == "Uf4a596a6eb65eabf52c003ffe325a21d":
                search_key_word = {'tbm': 'isch', 'q': event.message.text, 'client': 'img'}
                url = f"https://www.google.com/search?{urllib.parse.urlencode(search_key_word)}/"
                header = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0' }
                req = urllib.request.Request(url, headers=header)
                conn = urllib.request.urlopen(req)
                data = conn.read()
                template = '"(https://encrypted-tbn0.gstatic.com[\S]*)"'
                image_list = []
                for i in re.finditer(template,str(data,'utf-8')):
                    image_list.append(re.sub(r'\\u003d','=',i.group(1)))
    
                #random_image_url = image_list[random.randint(0, len(image_list)-1)]
                random_image_list = random.sample(image_list,k=3)
    
                image_template = ImageCarouselTemplate(
                    columns=[ImageCarouselColumn(image_url=urx,action=URIAction(label=f'image{j}',
                    uri=urx)) for j,urx in enumerate(random_image_list)]
                )
    
                line_bot_api.reply_message(
                    event.reply_token,
                    TemplateSendMessage(
                        alt_text='Hello World',
                        template=image_template
                    )
                )
    
  22. PS: heroku 可能會當機,膽小者勿試!

重新設計應用程式

學習目標:
  • 將檔案分解,藏暱重要的 Line Bot 聊天機器人機密
  • 結構化開發的專案,以利將來的維護與發展

藏暱重要的機密
  1. 在專案資料夾 LineBot下,新增 config.ini 檔案,寫人重要資訊
    [LineBot]
    channel_access_token = Channel access token
    channel_secret = Channel secret
    
  2. 修改主要程式檔:main.py
    (前面略過...)
    import configparser
    
    app = Flask(__name__)
    
    # 導入 config.ini 檔案
    config = configparser.ConfigParser()
    config.read('config.ini')
    
    # Line 聊天機器人的基本資料
    line_bot_api = LineBotApi(config.get('LineBot','channel_access_token'))
    handler = WebhookHandler(config.get('LineBot','channel_secret'))
    (以下略過...)
    
  3. 將程式碼送上 Heroku 進行測試,應不會出現錯誤訊息
    C:\LineBot>git add .
    C:\LineBot>git commit -m "Modify Main.py"
    C:\LineBot>git push heroku main
    
  4. MessageEvent主要分類:
    • MessageEvent:訊息事件
      • TextMessage:文字訊息
      • ImageMessage:圖片訊息
      • VideoMessage:影音訊息
      • StickerMessage:貼圖訊息
      • FileMessage:檔案訊息
    • FollowEvent:加入好友事件
    • UnfollowEvent:刪除好友事件
    • JoinEvent:加入聊天室事件
    • LeaveEvent:離開聊天室事件
    • MemberJoinedEvent:加入群組事件
    • MemberLeftEvent:離開群組事件
  5. 試著加上一些程式碼,例:main.py
    (前面的程式略過....)
    # 鸚鵡學說話
    @handler.add(MessageEvent, message=TextMessage)
    def echo(event):
        # user id 在 Line Developers / Basic Setting 下
        if event.source.user_id == "Uf4a596a6eb65eabf52c003ffe325a21d":
            line_bot_api.reply_message(
                event.reply_token,
                TextSendMessage(text=event.message.text)
            )
    
    if __name__ == "__main__":
        app.run()
    
    PS:將程式推上 Heroku ,再測看看!
學會查看重要記錄檔
  1. 在 main.py 檔案中,寫入一小斷程式碼:
    (前面略過....)
    print(body)
    try:
    (後面略過....)
    
  2. 利用 Heroku 的 app dashboard ,可查看 Server 上運作的訊息:
關閉自動訊息的回應
  1. 如果不喜歡這個自動回應的訊息,可以關閉:
  2. 靈活的改一段程式,顯現想要回應的訊息:main.py
    (前面略過....)
    # 靈活展現文字
    @handler.add(MessageEvent, message=TextMessage)
    def echo(event):
        # user id 在 Line Developers / Basic Setting 下
        if event.source.user_id == "Uf4a596a6eb65eabf52c003ffe325a21d":
    
            display_message = '你後面那位也想聽'
    
            if event.message.text == "tux來一個鬼故事":
                response_message = display_message
    
            line_bot_api.reply_message(
                event.reply_token,
                TextSendMessage(text=response_message)
            )
    
    if __name__ == "__main__":
        app.run()
    (後面略過....)
    
結構化開發專案的目錄設計
  1. 將目錄結構化成下列形式,儘量把程式分門別類分開:
    LineBot
      |-- Procfile
      |-- requirements.txt
      |-- runtime.txt
      |-- config.ini
      |-- main.py
      |-- app
           |-- __init__.py
           |-- router.py
           |-- linebotmodules.py
    
  2. 修改 main.py ,將內容儘量清空:
    from app import app
    
    if __name__ == "__main__":
       app.run()
    
  3. 新增 __init__.py 內容:
    from flask import Flask, request, abort
    from linebot import LineBotApi, WebhookHandler
    import configparser
    
    app = Flask(__name__)
    
    # 導入 config.ini 檔案
    config = configparser.ConfigParser()
    config.read('config.ini')
    
    # Line 聊天機器人的基本資料
    line_bot_api = LineBotApi(config.get('LineBot','channel_access_token'))
    handler = WebhookHandler(config.get('LineBot','channel_secret'))
    
    # 導入其他的程式模組
    from app import router, linebotmodules
    
  4. 新增 router.py 內容:
    from app import app, handler, request, abort
    from linebot.exceptions import InvalidSignatureError
    
    # 接收 Line 平台來的「通知」
    @app.route("/callback", methods=['POST'])
    def callback():
        signature = request.headers['X-Line-Signature']
        body = request.get_data(as_text=True)
        app.logger.info("Request body: " + body)
    
        print(body)
        try:
            handler.handle(body, signature)
        except InvalidSignatureError:
            abort(400)
    
        return 'OK'
    
  5. 新增 linebotmodules.py 內容:
    from app import line_bot_api, handler
    from linebot.models import MessageEvent, TextMessage, TextSendMessage
    
    # 靈活展現文字
    @handler.add(MessageEvent, message=TextMessage)
    def echo(event):
        # user id 在 Line Developers / Basic Setting 下
        if event.source.user_id == "Uf4a596a6eb65eabf52c003ffe325a21d":
    
            display_message = '你後面那位也想聽'
    
            if event.message.text == "tux來一個鬼故事":
                response_message = display_message
    
            line_bot_api.reply_message(
                event.reply_token,
                TextSendMessage(text=response_message)
            )
    
    
  6. 將程式推上 Heroku ,再測看看!