# 體溫機器人製作歷程 就像我在網站裡的"今年願景"的第一項寫的一樣,要做一個"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則是統計未填人數給環保股長,讓他不用上校網就能向學校回報,這個功能其實是後來我們班上的環保股長敲碗才加碼的