# 體溫機器人製作歷程
就像我在網站裡的"今年願景"的第一項寫的一樣,要做一個"daily routine reminder",就是會提醒我每天例行公事的機器人,指的就是現在這個主題,OK,聽我娓娓道來
## 動機
我們學校有個規定,每天早上10:10分之前要上傳當天的體溫,逾時填報三次就要吃警告,然而我和很多很多同學真的都挺...健忘的,偶爾還是會忘了填,最後就會被健康中心打電話到班上催促,我們都稱之為通緝。所以為了讓同學能夠少被記一點警告,也讓護士阿姨輕鬆一點,我想寫一個會提醒各位填體溫的程式,並且挑戰自己網頁開發尚未觸及的主題,就是後端與資料庫。
至於呈現方式,會跟上次有所不同,真的逐周紀錄,畢竟這次應該不會有像個人網頁一樣挑主題色調就挑一星期的情況(我真的有點吹毛求雌),然後應該會只打一篇,不會拆成不同的markdown,個人網站的歷程在那邊上一篇下一篇的,搞得像linked list一樣,弄出一堆分頁,有點酷但也有點煩,所以這次就循著左方的navigation(手機版左下方會有個小小的漢堡,點開就能看到)看下去就對了,立志要做出各方面都更優於上一篇的學習歷程( •̀ ω •́ )y
## 進度規劃
| 時間 | 工作內容 |
| -------- | -------- |
| 第一周 | 基礎前後端跟整體構想 |
| 第二周 | 使機器人能在本機進行爬蟲、寄信 |
| 第三周 | 學習MySQL,操作本地資料庫 |
| 第四周 | 部署至heroku、建立ClearDB雲端資料庫 |
| 第五周 | 部署至heroku、設定定時器 |
| 第六 ~ ?周 | 前端設計與後續維護 |
稍微解釋一下,第一周就是基礎的東西,二三周會有點挑戰,畢竟資料庫不知道要學多久,但那時候應該比較閒,兩周的時間應該可以,再來是部署,這邊感覺有點難且不確定性多,所以給自己多一點時間,再加上會撞上月考 + TOI初選,甚至有可能延長,最後我才要做前端設計,一定要先搞好變數多的後端、資料庫、heroku,再來處理前端,至於期限我打問號,主要是因為看我要設計得多精美,目前腦中有很多不錯的點子,再說吧~
## 2/11 ~ 2/18 基本前後端
我在這邊先說明一下這個企劃的前後端各自是如何運行的,一樣上示意圖

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

左邊的流程圖是網頁表單取得使用者資料,有分兩種,衛生股長跟一般學生,分別插入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()
```
這樣就有非常基礎的表單

美觀那些之後再說,先做比較重要的後端跟資料庫
## 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畫了張圖,這張圖有資料庫的布局跟爬蟲+寄信的流程(字有點醜,抱歉)

然後是基本設定
```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

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

然後就是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原本的限制,總之,這樣就有酷酷的登入按鈕

我有想過需不需要搞認證碼,但目前先算了吧...
我最後還額外加了一小段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上

畢竟個人網站本來就是靜態網站,改用Netlify剛剛好,至於轉移過程我其實沒打算多講,因為的超簡單XD,只要拆掉flask server、創新repo、連結到Netlify就好了,超級無腦,真爽
這邊先部署註冊頁面,學過的流程像是建立app、連接repo就不細講,講講這次遇到的新東西
首先是連接ClearDB,這邊選擇免費版,因為我窮,但還是逃不掉認證信用卡的環節,反正除非體溫機器人太熱門,撐爆資料庫,不然應該都還是免費


再來要去config vars那邊找的DB的連結,那其中藏有資料庫包括password、user、root等資訊

然後改變後端連接資料庫的地方到新的資料庫
```python=
db = mysql.connector.connect(
host = "us-cdbr....",
user = "b648....",
password = "a3e....",
database = "heroku_f....",
buffered = True
)
```
居然第一次就看到綠色火箭而且能打開,我真是太感動了,當初架個人網站的時候還在摸索,吃了好多紅色火箭(話說深色模式的Github跟hackmd好不搭XD)

喔對了,Google Oauth要像上禮拜所說的,連接到新網站的URL

正當我開開心心要輸入第一筆資料測試的時候,就出現問題了,不管怎麼樣都無法登入,console裡面有個奇怪的bug

後來在Google Oauth中加入學生註冊分頁的連結就好,看來它不支援重新導向
接下來又遇到奇怪的問題,明明是對的衛生股長的帳號無法用於登入

我懷疑是環境變數的問題,所以上網爬資料,結果是我自己兩光,忘了加這幾行
```python=
chrome_options.add_argument("--headless")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--no-sandbox")
```
另一個問題是不管有沒有註冊過,都會顯示已註冊過

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

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


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


再來要設置定時器,來啟動機器人,但問題來了,不管是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

真的是千言萬語都無法形容我當下有多感動இ௰இ
而且這時候token也生成了,這是不是代表我可以丟掉臨時的server,直接用timer?那就直接push看看,結果隔天直接出錯,我還以為是timer壞掉,後來我就去看log,發現...

是爬蟲的問題?!?!?!?!啊,我測試的時候把爬蟲的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則是統計未填人數給環保股長,讓他不用上校網就能向學校回報,這個功能其實是後來我們班上的環保股長敲碗才加碼的