---
# System prepended metadata

title: 體溫機器人製作歷程

---

# 體溫機器人製作歷程
就像我在網站裡的"今年願景"的第一項寫的一樣，要做一個"daily routine reminder"，就是會提醒我每天例行公事的機器人，指的就是現在這個主題，OK，聽我娓娓道來

## 動機
我們學校有個規定，每天早上10:10分之前要上傳當天的體溫，逾時填報三次就要吃警告，然而我和很多很多同學真的都挺...健忘的，偶爾還是會忘了填，最後就會被健康中心打電話到班上催促，我們都稱之為通緝。所以為了讓同學能夠少被記一點警告，也讓護士阿姨輕鬆一點，我想寫一個會提醒各位填體溫的程式，並且挑戰自己網頁開發尚未觸及的主題，就是後端與資料庫。

至於呈現方式，會跟上次有所不同，真的逐周紀錄，畢竟這次應該不會有像個人網頁一樣挑主題色調就挑一星期的情況(我真的有點吹毛求雌)，然後應該會只打一篇，不會拆成不同的markdown，個人網站的歷程在那邊上一篇下一篇的，搞得像linked list一樣，弄出一堆分頁，有點酷但也有點煩，所以這次就循著左方的navigation(手機版左下方會有個小小的漢堡，點開就能看到)看下去就對了，立志要做出各方面都更優於上一篇的學習歷程( •̀ ω •́ )y

## 進度規劃

| 時間 | 工作內容 |
| -------- | -------- | 
| 第一周     | 基礎前後端跟整體構想     | 
| 第二周  | 使機器人能在本機進行爬蟲、寄信 |
| 第三周  | 學習MySQL，操作本地資料庫 |
| 第四周 | 部署至heroku、建立ClearDB雲端資料庫 |
| 第五周 | 部署至heroku、設定定時器 |
| 第六 ~ ?周 | 前端設計與後續維護 |

稍微解釋一下，第一周就是基礎的東西，二三周會有點挑戰，畢竟資料庫不知道要學多久，但那時候應該比較閒，兩周的時間應該可以，再來是部署，這邊感覺有點難且不確定性多，所以給自己多一點時間，再加上會撞上月考 + TOI初選，甚至有可能延長，最後我才要做前端設計，一定要先搞好變數多的後端、資料庫、heroku，再來處理前端，至於期限我打問號，主要是因為看我要設計得多精美，目前腦中有很多不錯的點子，再說吧~



## 2/11 ~ 2/18 基本前後端
我在這邊先說明一下這個企劃的前後端各自是如何運行的，一樣上示意圖

![](https://i.imgur.com/csIN616.png)

由上圖可知每個檔案的關係，我也盡我所能的做了一張流程圖

![](https://i.imgur.com/Wi3uriE.png)



左邊的流程圖是網頁表單取得使用者資料，有分兩種，衛生股長跟一般學生，分別插入MySQL資料庫中不同的table；右邊的是每天十點的程序，先從衛生股長的table取得每班衛生股長的帳密，再用其爬蟲取得班上學生跟體溫的表格，再對資料庫中的學生座號與gmail建表，若該學生的體溫顯示無資料且資料庫中有他的Email，則發信

然而就像我在個人網站學習歷程提到的，這是跟國中同學合作的企劃，因為我不會爬蟲，當然要學也是可以，只是我想盡快寫完然後去讀資料結構與演算法，所以決定合作，目前暫定網頁前後端以及跟資料庫有關的是我寫，爬蟲跟寄mail他寫

還有一點要注意，目前這都是暫定的，可能還會修改，最後成品預計會再做一份流程圖

接下來就講講我這週打了甚麼code，主要是非常基本的前後端，只有HTML跟Python比較值得講
```htmlmixed=
<form method="post" action="/">
        <h3>衛生股長註冊</h3>
        <div class="health-leader-class-box">
            <label for = "health-leader-class">班級</label>
            <input type="text" name="health-leader-class" class="health-leader-class" required pattern="[0-9]{3}" required="required">
        </div>
        <div class="health-leader-id-box">
            <label for = "health-leader-id">學號</label>
            <input type="text" name="health-leader-id" class="health-leader-id" required pattern="[0-9]{6}" required="required">
        </div>
        <div class="health-leader-password-box">
            <label for = "health-leader-password">密碼</label>
            <input type="password" name="health-leader-password" class="health-leader-password" required="required" maxlength="50">
        </div>
        <input type="submit" name="submit-btn" value="health-leader-btn">
    </form>

    <form method="post" action="/">
        <h3>學生註冊</h3>
        <div class="student-class-box">
            <label for="student-class">班級</label>
            <input type="text" name="student-class" class="student-class" required pattern="[0-9]{3}" required="required">
        </div>
        <div class="student-seat_number-box">
            <label for="student-seat_number">座號</label>
            <input type="number" name="student-seat_number" class="student-seat_number" min = "1" max = "40" required="required">
        </div>
        <div class="email-box">
            <label for="email">Email</label>
            <input type="email" name="email" class="email" required="required" maxlength="50">
        </div>
        <input type="submit" name="submit-btn" value="student-btn">
    </form>

```
```python=
app = Flask(__name__)

@app.route("/", methods = ["POST", "GET"])
def main_page():
    if request.method == "POST": # 按下submit鈕

        # 填報的是衛生股長
        if request.form.get("submit-btn") == "health-leader-btn":
            # do something

        # 瑱報的是一般學生
        elif request.form.get("submit-btn") == "student-btn":
            # do something

    return render_template("main.html") 

if __name__ == "__main__":
    app.debug = True
    app.run()
```

這樣就有非常基礎的表單

![](https://i.imgur.com/c9hARMj.png)


美觀那些之後再說，先做比較重要的後端跟資料庫

## 2/19 ~ 3/5 基礎後端、資料庫、爬蟲跟寄mail

先從我同學的爬蟲講起吧

### 爬蟲與環境架設

超感謝他的，禮拜三下午手把手教我怎麼用他的code，主要有三點

* 下載chromedriver並添加環境變數、放到同資料夾內
* pip install一堆東西
* 改PATH到chromedrive的相對路徑

感覺這些東西會讓我部署到heroku後的環境架設超麻煩XD

然後這是他的code

```python=
def crawl_data(account, password):
    # Chrome Driver 路徑
    PATH = "C:/Users/Weber Chang/OneDrive/桌面/temperature bot/chromedriver_win32/chromedriver.exe"
    driver = webdriver.Chrome(PATH)
    # 進入官網
    driver.get("https://webap1.kshs.kh.edu.tw/kshsSSO/publicWebAP/bodyTemp/bodyTempQuery.aspx")

    # 模擬登陸
    driver.find_element_by_id("ContentPlaceHolder1_txtId").send_keys(account)
    driver.find_element_by_id("ContentPlaceHolder1_txtPassword").send_keys(password)
    driver.find_element_by_id("ContentPlaceHolder1_btnId").click()
    time.sleep(1)
    driver.get("https://webap1.kshs.kh.edu.tw/kshsSSO/publicWebAP/bodyTemp/bodyTempQueryStudent.aspx")
    time.sleep(1)

    # 取得資料
    page = driver.page_source
    driver.quit()
    os.system("cls")
    # 建表
    tables = pd.read_html(page)
    tables = tables[4]
    tables.index += 1
    return tables
```
這樣就能拿到體溫、班級、座號等資料了，果然是爬蟲大老(只可惜學校的表格中沒有學號，不然會更方便)

### 後端與資料庫

再來是重頭戲，後端跟資料庫，但MySQL其實沒想像中難學，想看我的筆記可以點[連結](https://hackmd.io/@WeberChang/B1NuPU9Jq)飛過去

首先是資料庫長怎樣，我很努力地用我姊的iPad畫了張圖，這張圖有資料庫的布局跟爬蟲+寄信的流程(字有點醜，抱歉)

![](https://i.imgur.com/q5eBOiT.png)


然後是基本設定

```python=
db = mysql.connector.connect(
    host = "localhost",
    user = "root",
    password = "weber0518",
    database = "test_database"
)
myCursor = mysecondCursor = db.cursor() 
myCursor.execute("CREATE TABLE health_leader (class mediumint UNSIGNED, student_id VARCHAR(6), password VARCHAR(50))")
myCursor.execute("CREATE TABLE Student (class mediumint UNSIGNED, seat_number smallint UNSIGNED, gmail VARCHAR(50))")
```
這樣就能建立圖中的table了


再來講講後端，首先是如何抓取前端資料並插入資料

```python=
def insert_student_data(student_class, seat_number, gmail):
    # 將學生資料插入學生table
    myCursor.execute("INSERT INTO Student (class, seat_number, gmail) VALUES (%s, %s, %s)", (student_class, seat_number, gmail))
    db.commit()

def insert_health_leader_data(health_leader_class, student_id, password):
    # 將衛生股長資料插入衛生股長table
    myCursor.execute("INSERT INTO health_leader (class, student_id, password) VALUES (%s, %s, %s)", (health_leader_class, student_id, password))
    db.commit()
    
app = Flask(__name__)

@app.route("/", methods = ["POST", "GET"])
def main_page():
    if request.method == "POST": # 按下submit鈕

        # 填報的是衛生股長
        if request.form.get("submit-btn") == "health-leader-btn":
            insert_health_leader_data(int(request.form["health-leader-class"]), request.form["health-leader-id"], request.form["health-leader-password"])

        # 瑱報的是一般學生
        elif request.form.get("submit-btn") == "student-btn":
            insert_student_data(int(request.form["student-class"]), int(request.form["student-seat_number"]), request.form["email"])

    return render_template("main.html") 

if __name__ == "__main__":
    app.debug = True
    app.run()
```

這些都挺白話的，有註解後應該不須多做解釋

然後是更刺激的，啟動程式，結合資料庫、爬蟲跟寄信

```python=
def send_email(users):
    try:
        # 信件內文
        msg = email.message.EmailMessage()
        msg["From"] = "kshstemperaturebot@gmail.com"
        msg["To"] = ','.join(users) 
        msg["Subject"] = "你484還沒填體溫"
        msg.add_alternative("""
            <h3>快填體溫!</h3>
            <p>傳送門在這</p>
            <a href="https://webap1.kshs.kh.edu.tw/kshsSSO/publicWebAP/bodyTemp/index.aspx">點我</a>
            <p>有任何問題都可以連絡我喔<br>最後附上一張大黑的照片</p>
            <img src = "https://i.imgur.com/TMWDUEt.jpg" style = "width: 270px; height: 360px">
        """, subtype = "html")

        # 寄出
        server.send_message(msg)
        print("Email sent successfully")
    except:
        print("Got an error while sending email")


def crawl_data(account, password):
    try:
        # Chrome Driver 路徑
        PATH = "C:/Users/Weber Chang/OneDrive/桌面/temperature bot/chromedriver_win32/chromedriver.exe"
        driver = webdriver.Chrome(PATH)
        # 進入官網
        driver.get("https://webap1.kshs.kh.edu.tw/kshsSSO/publicWebAP/bodyTemp/bodyTempQuery.aspx")

        # 模擬登陸
        driver.find_element_by_id("ContentPlaceHolder1_txtId").send_keys(account)
        driver.find_element_by_id("ContentPlaceHolder1_txtPassword").send_keys(password)
        driver.find_element_by_id("ContentPlaceHolder1_btnId").click()
        time.sleep(1)
        driver.get("https://webap1.kshs.kh.edu.tw/kshsSSO/publicWebAP/bodyTemp/bodyTempQueryStudent.aspx")
        time.sleep(1)

        # 取得資料
        page = driver.page_source
        driver.quit()
        os.system("cls")
        # 建表
        tables = pd.read_html(page)
        tables = tables[4]
        tables.index += 1
        return tables
    except:
        print("Fail to crawl data")

def activate_bot():
    # 登入temperature bot 帳號
    server.login("kshstemperaturebot@gmail.com", "才不告訴你哩")
    # 遍歷每班的衛生股長
    myCursor.execute("SELECT * FROM health_leader")
    health_leader_list = myCursor.fetchall()
    for i in health_leader_list:
        # 爬蟲該班學生的座號與體溫並建表
        temp_table = crawl_data(i[1], i[2])

        
        # 建表 key是座號 value是Gmail
        mysecondCursor.execute("SELECT * FROM Student Where class = " + str(i[0]))
        email_table = { j[1]: j[2] for j in mysecondCursor }

        # 遍歷temp_table並發信 若該學生未上傳體溫(體溫呈現nan)且DB中有他的Gmail再發信
        send_list = \
        [ 
            email_table.get(temp_table["座號"][j]) \
            for j in range(1, len(temp_table) + 1) \
                if temp_table["溫度"][j] and email_table.get(temp_table["座號"][j], False) 
        ]
        send_email(send_list)

    server.close()
```

稍微說明一下好了，機器人啟動後，會先登入我特別註冊的Google帳號，本來不管是網路上的資料還是我同學寫給我的code，登入都是在send_email函式裡，我把它移出來，這樣只需要登入一次就好，不用每封信都重新登入，好笑的是，我一開始還忘了把server.close()移出來，所以一直出錯，還跑去stack overflow上問XD，至少這次沒有再被大神指責說問題不夠精確，看來我問問題的能力有一點點進步(就那麼一點點)

然後是把衛生股長的資料一一抓出來，對各班級的體溫資料爬蟲，再來是抓出該班學生的Gmail，用座號當key建表，這邊用了酷酷的陣列生成式，這樣之後就能快速讀取，萬事俱備後，就能遍歷體溫資料，若該學生未填體溫，內容會呈現nan，並且資料庫中有其Gmail，就用陣列生成式丟進list中，再根據整個list發信，最後遍歷完衛生股長的table(也就是處理完所有班級)，就能關閉smtplib server，結束一天的工作...目前預計是這樣

當時這邊遇到一些小問題，health_leader的迴圈我本來是直接迭代myCursor，但有時候會莫名中斷，跑完一班就不跑其他班，而且只有在建email表之後會發生，如果只是爬蟲則不會，最後我把health_leader迴圈改成fetchall()再迭代list就好了┌(。Д。)┐ 好奇怪的bug

### 資料安全性

接下來要處理一個很重要的東西，就是資料的篩選，必竟天曉得使用者會輸什麼進來，所以要先確保輸進來的資料可用，然後我突然發現我一開始忘了設Primary key XD

```python=
myCursor.execute("ALTER TABLE health_leader CHANGE class class mediumint UNSIGNED PRIMARY KEY")
myCursor.execute("ALTER TABLE Student ADD PRIMARY KEY (class, seat_number)")
```
衛生股長的班級設為primary key，學生的班級+座號設為primary key，這樣重複的資料進來就會出錯，所以要加try except將錯誤分流
```python=
def insert_student_data(student_class, seat_number, gmail):
    # 將學生資料插入學生table
    try:
        myCursor.execute("INSERT INTO Student (class, seat_number, gmail) VALUES (%s, %s, %s)", (student_class, seat_number, gmail))
        db.commit()
    except mysql.connector.Error:
        print("無法插入資料庫 該學生已有資料")
        
def insert_health_leader_data(health_leader_class, student_id, password):
    # 將衛生股長資料插入衛生股長table
    try:
        myCursor.execute("INSERT INTO health_leader (class, student_id, password) VALUES (%s, %s, %s)", (health_leader_class, student_id, password))
        db.commit()
    except mysql.connector.Error:
        print("無法插入資料庫 該班衛生已有資料")
```
這樣就不會有重複的資料了，但這樣的安全性還不足夠，畢竟雄中生都挺猴的，要檢查輸入的資料是否真的能用
先檢查班級，因為前端表單已經有限制只能輸入三位數，所以這邊從首位數跟末兩位下手就好，就能檢查班級是否位於101 ~ 124、201 ~ 224、301~324之間
```python=
def check_class(class_number):
    grade = int(class_number / 100)
    # 如果首位數不在1~3之間或後兩位不在1~24之間 回傳false
    if grade < 1 or grade > 3 or class_number - grade * 100 < 1 or class_number - grade * 100 > 24:
        return False
    return True

def insert_student_data(student_class, seat_number, gmail):
    # 將學生資料插入學生table
    try:
        if not check_class(student_class):
            raise ValueError("班級不存在啊")
        myCursor.execute("INSERT INTO Student (class, seat_number, gmail) VALUES (%s, %s, %s)", (student_class, seat_number, gmail))
        db.commit()
    except ValueError as ErrorMsg:
        print(ErrorMsg)
    except mysql.connector.Error:
        print("無法插入資料庫 該學生已有資料")

def insert_health_leader_data(health_leader_class, student_id, password):
    # 將衛生股長資料插入衛生股長table
    try:
        if not check_class(health_leader_class):
            raise ValueError("班級不存在啊")
        myCursor.execute("INSERT INTO health_leader (class, student_id, password) VALUES (%s, %s, %s)", (health_leader_class, student_id, password))
        db.commit()
    except ValueError as ErrorMsg:
        print(ErrorMsg)
    except mysql.connector.Error:
        print("無法插入資料庫 該班衛生已有資料")
```
再來是座號，我在前端是限制座號要在1~40之間，因為每班人數不一樣，可能36、37或更多，所以設得比較寬鬆，我沒辦法確認他們班到底有幾人（就算有，大概也是爬蟲），但老實說，這似乎不重要，因為就算該班只有36人，他輸入39，我在核對他填過體溫了沒的時候，是拿爬蟲下來“一定存在”的座號去尋找他的Gmail，所以若他輸了不存在的座號，根本不會影響程式的運行，頂多佔一點點空間而已

還有衛生股長的學號跟密碼，這個一定要確認，這關乎我能不能正確爬蟲，這邊預想到兩種狀況，一種是學號+密碼壓根得無法登入，另一種是更惡劣的，能登入但不是正確的班級(像是拿218的帳密但他班級輸入217，這樣機器人會按照218的體溫填報情況發送email給217的同學)
```python=
def check_health_leader(health_leader_class, account, password):
    # 外層try except分流帳密根本無法用於登入的狀況
    try:
        # Chrome Driver 路徑
        PATH = "C:/Users/Weber Chang/OneDrive/桌面/temperature bot/chromedriver_win32/chromedriver.exe"
        driver = webdriver.Chrome(PATH)
        # 進入官網
        driver.get("https://webap1.kshs.kh.edu.tw/kshsSSO/publicWebAP/bodyTemp/bodyTempQuery.aspx")

        # 模擬登陸
        driver.find_element_by_id("ContentPlaceHolder1_txtId").send_keys(account)
        driver.find_element_by_id("ContentPlaceHolder1_txtPassword").send_keys(password)
        driver.find_element_by_id("ContentPlaceHolder1_btnId").click()
        time.sleep(1)

        driver.get("https://webap1.kshs.kh.edu.tw/kshsSSO/publicWebAP/bodyTemp/bodyTempQueryStudent.aspx")
        time.sleep(1)
        grade_selection = Select(driver.find_element_by_id("ContentPlaceHolder1_ddlGra")).first_selected_option.get_attribute('value')
        class_selection = Select(driver.find_element_by_id("ContentPlaceHolder1_ddlCla")).first_selected_option.get_attribute('value')
        driver.quit()
        # 這邊判斷該帳號是否真的屬於該班的環保股長
        if str(health_leader_class) != str(grade_selection) + str(class_selection):
            return False
        return True
    except:
        return False
    
def insert_health_leader_data(health_leader_class, student_id, password):
    # 將衛生股長資料插入衛生股長table
    try:
        if not check_class(health_leader_class):
            raise ValueError("班級不存在啊")
        if not check_health_leader(health_leader_class, student_id, password):
            raise ValueError("帳密無法用於登入")
        myCursor.execute("INSERT INTO health_leader (class, student_id, password) VALUES (%s, %s, %s)", (health_leader_class, student_id, password))
        db.commit()
    except ValueError as ErrorMsg:
        print(ErrorMsg)
    except mysql.connector.Error:
        print("無法插入資料庫 該班衛生已有資料")
```
這邊借用爬蟲大佬的code，自己寫了確認的函式，好欸

再來是Gmail是否存在的問題，我們想了很久，最後決定乾脆寫一個Google登入的按鈕，捨棄一般輸入，首先是去Google cloud申請一組OAuth 2.0 Client ID

![](https://i.imgur.com/XwcDXBZ.png)

這邊相關網域只能先填本機，之後上heroku再改

![](https://i.imgur.com/SwwTVTX.png)

然後就是coding時間，意外的不難

```htmlembedded=
<!-- head裡面加 -->
<head>
    <meta name="google-signin-client_id" content="{{ google_oauth2_client_id }}">
    <script src="https://apis.google.com/js/platform.js" async defer></script>
    <script src="https://code.jquery.com/jquery-3.3.1.js" integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=" crossorigin="anonymous"></script>
</head>

<!-- 加入讀取資料的JS -->
<script>
    function onSignIn(googleUser) {
        const profile = googleUser.getBasicProfile();
        document.querySelector(".email").value = profile.getEmail();
    }
</script>

<!-- 加入按鈕 -->
<div class="g-signin2" data-onsuccess="onSignIn"></div>
```

head裡面的google_oauth2_client_id就是剛申請到的id，從flask server傳入

```python=
GOOGLE_OAUTH2_CLIENT_ID = '486654398403-3gvn0pg......'
# flask app 的渲染要傳入id
return render_template("main.html", google_oauth2_client_id = GOOGLE_OAUTH2_CLIENT_ID) 
```

其實我用了個小小的偷吃步，就是不是用ajax傳gmail資料到server，是填入原本gmail的格子，這樣有兩個好處，簡單跟能沿用input原本的限制，總之，這樣就有酷酷的登入按鈕

![](https://i.imgur.com/dHPMByV.png)

我有想過需不需要搞認證碼，但目前先算了吧...

我最後還額外加了一小段code，但意義不是很大，只能算是小提醒吧

```python=
myCursor.execute("SELECT EXISTS(SELECT * FROM health_leader WHERE class = {0})".format(student_class))
i = myCursor.fetchone()
if not i[0]:
    raise ValueError("無該班環保股長資料")
```

這樣安全性應該(希望)足夠了，以一個給學生填報的網站來說

### 同場加碼

最後在同學的敲碗之下，我加了個新功能，會寄信給衛生，告訴他有多少人沒填體溫，他好像能用上
```python=
def send_email_to_health_leader(user, cnt):
    try:
        # 信件內文
        msg = email.message.EmailMessage()
        msg["From"] = "kshstemperaturebot@gmail.com"
        msg["To"] = user 
        msg["Subject"] = "你們班還有" + str(cnt) + "個人沒填體溫"
        msg.add_alternative("""
            <p>有任何問題都可以連絡我喔<br>最後附上一張大黑的照片</p>
            <img src = "https://i.imgur.com/TMWDUEt.jpg" style = "width: 270px; height: 360px">
        """, subtype = "html")

        # 寄出
        server.send_message(msg)
        print("Email sent successfully")
    except:
        print("Got an error while sending email")


def activate_bot():
    # 登入temperature bot 帳號
    server.login("kshstemperaturebot@gmail.com", "temperature0518")
    # 遍歷每班的衛生股長
    myCursor.execute("SELECT * FROM health_leader")
    health_leader_list = myCursor.fetchall()
    for i in health_leader_list:
        # 爬蟲該班學生的座號與體溫並建表
        temp_table = crawl_data(i[1], i[2])

        
        # 建表 key是座號 value是Gmail
        mysecondCursor.execute("SELECT * FROM Student Where class = " + str(i[0]))
        email_table = { j[1]: j[2] for j in mysecondCursor }

        # 遍歷temp_table並發信 若該學生未上傳體溫(體溫呈現nan)且DB中有他的Gmail再發信 再寄信給環保 通知有多少人沒填體溫
        studentCnt = 0
        for j in range(1, len(temp_table) + 1):
            if not temp_table["溫度"][j]:
                studentCnt += 1
                if email_table.get(temp_table["座號"][j], False):
                    send_email_to_regular_user(email_table.get(temp_table["座號"][j]))
        send_email_to_health_leader(i[3], studentCnt)

    server.close()
```

### 當周小結論

這樣姑且就結束了，不知道安全性夠不夠，但我的肝真的快裂開，但Google登入那邊真的很酷很好玩，還有突然加了不少新功能，真的是計劃趕不上變化，希望下禮拜的部署能順順利利，真的超緊張的⊙﹏⊙∥

## 3/6 ~ 部署至雲端

體溫機器人部署的第一步就是...鳩佔鵲巢XD
目前heroku中已經有一個app，就是我的個人網站，但若我要再部署體溫機器人，dyno的時間會爆，所以要請個人網站移駕到另一個雲端平台Netlify上

![](https://i.imgur.com/WpKZAnn.png)

畢竟個人網站本來就是靜態網站，改用Netlify剛剛好，至於轉移過程我其實沒打算多講，因為的超簡單XD，只要拆掉flask server、創新repo、連結到Netlify就好了，超級無腦，真爽

這邊先部署註冊頁面，學過的流程像是建立app、連接repo就不細講，講講這次遇到的新東西
首先是連接ClearDB，這邊選擇免費版，因為我窮，但還是逃不掉認證信用卡的環節，反正除非體溫機器人太熱門，撐爆資料庫，不然應該都還是免費

![](https://i.imgur.com/yFi56tZ.png)

![](https://i.imgur.com/zFMgVC6.png)

再來要去config vars那邊找的DB的連結，那其中藏有資料庫包括password、user、root等資訊
![](https://i.imgur.com/yf6Nukr.png)
然後改變後端連接資料庫的地方到新的資料庫
```python=
db = mysql.connector.connect(
    host = "us-cdbr....",
    user = "b648....",
    password = "a3e....",
    database = "heroku_f....",
    buffered = True
)
```

居然第一次就看到綠色火箭而且能打開，我真是太感動了，當初架個人網站的時候還在摸索，吃了好多紅色火箭(話說深色模式的Github跟hackmd好不搭XD)

![](https://i.imgur.com/5Iob28x.png)


喔對了，Google Oauth要像上禮拜所說的，連接到新網站的URL
![](https://i.imgur.com/8ZwtxqI.png)


正當我開開心心要輸入第一筆資料測試的時候，就出現問題了，不管怎麼樣都無法登入，console裡面有個奇怪的bug
![](https://i.imgur.com/OpjsBbC.png)

後來在Google Oauth中加入學生註冊分頁的連結就好，看來它不支援重新導向

接下來又遇到奇怪的問題，明明是對的衛生股長的帳號無法用於登入

![](https://i.imgur.com/0kibYLz.png)


我懷疑是環境變數的問題，所以上網爬資料，結果是我自己兩光，忘了加這幾行

```python=
chrome_options.add_argument("--headless")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--no-sandbox")
```


另一個問題是不管有沒有註冊過，都會顯示已註冊過

![](https://i.imgur.com/AynweZO.png)

這就有趣了，因為在本機對ClearDB做一樣的操作，會得到無該班衛生的回覆，也就是正確的回覆

![](https://i.imgur.com/1BCgT2k.png)
(我後來才知道是環保股長在負責，只好將錯就錯ㄌ)

這邊來拆分解構一下我的code，看來是撞到mysql.connector.Error，因為我以為只有重複資料才可能引發這種error，看來部署上去後有了新的變因，所以我決定測試一下，把except改成:
```python=
except Exception as e:
    return(e)
```
來看看錯誤是甚麼，結果...

![](https://i.imgur.com/vu6bQx0.png)
![](https://i.imgur.com/Gb9iis8.png)


有時候出現2013，有時候出現2055，NANI??!!
後來我不停爬文，甚至去stack overflow上發文但沒人回，最後發現，我沒有在query後close我的資料庫連結跟cursor，超基本但在本地完全沒發現，加了之後就陸續成功了

![](https://i.imgur.com/TnNFY8o.png)

![](https://i.imgur.com/1vEFlNX.png)





再來要設置定時器，來啟動機器人，但問題來了，不管是heroku scheduler還是uptime robot都只能循環的去戳，不能客製化規避特殊時間，我或許能用python的time函數迴避掉周末，但國定假日呢?寒暑假呢?難道要我手動去戳，感覺很不可靠，所以我想到用Google日曆來設定，因為Google日曆能幫像是"直到7/1前的星期一到五的十點加一個事件"，這樣我就能把每個工作日選起來，再手動排除一些特殊假日或上課日就好，機器人就啟動前去抓API看有沒有那個事件，就能決定要不要執行
說起來簡單啦...
我一開始也是照著Google calendar api quickstart打，但credentials那邊一直出事，首先，我還真的不知道要選網頁還是PC程式，畢竟機器人自動啟動並沒有頁面，我試了許久，決定先給機器人一個簡單的Flask server
```python=
# timer.py
from __future__ import print_function

import datetime
import os.path

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

# If modifying these scopes, delete the file token.json.
SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']


def main():
    """Shows basic usage of the Google Calendar API.
    Prints the start and name of the next 10 events on the user's calendar.
    """
    creds = None
    # The file token.json stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        # Save the credentials for the next run
        with open('token.json', 'w') as token:
            token.write(creds.to_json())

    try:
        service = build('calendar', 'v3', credentials=creds)

        # Call the Calendar API
        now = datetime.datetime.utcnow().isoformat() + 'Z'  # 'Z' indicates UTC time
        print('Getting the upcoming 2 events')
        events_result = service.events().list(calendarId='primary', timeMin=now,
                                              maxResults=2, singleEvents=True,
                                              orderBy='startTime').execute()
        events = events_result.get('items', [])

        if not events:
            print('No upcoming events found.')
            return

        # Prints the start and name of the next 10 events
        for event in events:
            start = event['start'].get('dateTime', event['start'].get('date'))
            print(start, event['summary'])

    except HttpError as error:
        print('An error occurred: %s' % error)


# bot_server,py
from flask import Flask, render_template
import bot
import timer

timer.main()

app = Flask(__name__)

@app.route("/")
def main_page():
    return render_template("bot_page.html") 

if __name__ == "__main__":
    app.debug = True
    app.run()
```
後來遇到一堆問題，像是vscode一直說找不到credentials.json，但這真的就是所有教學跟Google自己的quickstart的程式，後來我改成絕對路徑就成功，更傻眼的在後面，之後要去驗證Google帳號，一直跳出400:redirect_uri_mismatch，在所有教學中，都說在Oauth的redirect uri中填入PC上的臨時網域，我是 localhost:5000，但失敗?，然後我看到底下有寫Google要求的URI是localhost:62387，奇怪，然後我開始隨機應變，把localhost:62387加到Oauth的redirect uri中，重下載credentials.json，出現403:access denied，超怪，後來在一個印度老哥的教學裡找到要在app裡面加user，我最信任的tech with tim居然直接忽略(老實說，他忽略了很多東西)，結果還是不行，後來爬了一堆資料，意外在一篇stack overflow上看到有人說，要在所有redirect uri後面加反斜線，ㄟ奇怪，我怎麼記得之前打Google登入的時候沒加，但我實驗性的加加看，就可以了，OMG

![](https://i.imgur.com/KRNWW1Z.png)

真的是千言萬語都無法形容我當下有多感動இ௰இ

而且這時候token也生成了，這是不是代表我可以丟掉臨時的server，直接用timer?那就直接push看看，結果隔天直接出錯，我還以為是timer壞掉，後來我就去看log，發現...

![](https://i.imgur.com/JJzbnyJ.png)

是爬蟲的問題?!?!?!?!啊，我測試的時候把爬蟲的PATH改成本地的chromedriver，再改回來應該就好

後來我發現advanced scheduler其實要錢，所以就自己寫了一個

```python=
from apscheduler.schedulers.blocking import BlockingScheduler
import bot, bot_2, timer

sched = BlockingScheduler()

@sched.scheduled_job('cron', day_of_week='mon-sun', hour=9, timezone='Asia/Taipei')
def bot_work():
    if timer.check_date():
        bot.activate_bot()

@sched.scheduled_job('cron', day_of_week='mon-sun', hour=10, timezone='Asia/Taipei')
def bot_work():
    if timer.check_date():
        bot.activate_bot()

@sched.scheduled_job('cron', day_of_week='mon-sun', hour=10, minute=10, timezone='Asia/Taipei')
def bot_2_work():
    if timer.check_date():
        bot_2.activate_bot_2()

sched.start()
```


---


再經過兩個禮拜的試營運之後，我發現了幾個問題

1. 使用者對於註冊興致缺缺
2. Gmail有時候不會跳通知(＃°Д°)
3. 我沒有對資料做加密 頗危險

第三點再研究，現在頭大的是一二點，我最後決定將功能縮減到只剩環保股長，簡化體溫機器人的功能，畢竟說實話，之前的功能有點過多且雜亂，還不如只讓少數人(環保股長)註冊，然後寄信告訴他還有多少人沒填體溫，這樣的刪減會對體溫機器人的程式有巨大的改變

---

# 體溫機器人Intro

這是我架設的KSHS體溫機器人的repository，容我介紹一下

## 動機

我就讀的高雄中學在疫情之下有個奇妙的政策，每天都要上傳自己的體溫到校網上，這帶來幾個麻煩

1. 學生沒在10:10前填報就會被罰愛校服務
2. 每班的環保股長還要在10:10去校網查未填報的人數並向學校回報

被這填體溫的政策困擾了一年多，我決定寫個程式改善現況，一開始想做自動填體溫機器人，但那個之前就有人被抓過而且違反校規，所以我最後做了能夠提醒同學填體溫、並統計未填人數以利環保股長督促+回報的機器人，並且有個註冊網頁，方便推廣給全校同學

## 功能

體溫機器人有三個啟動時間點，9:00、10:00、跟10:10，前兩個時間點會爬蟲進學校官網找出還未填體溫的同學，並寄信提醒，原定是十點但因為太接近deadline所以加開九點，10:10則是統計未填人數給環保股長，讓他不用上校網就能向學校回報，這個功能其實是後來我們班上的環保股長敲碗才加碼的