Compare commits

..

24 Commits
dev ... master

Author SHA1 Message Date
17e6e1421e
fix: filter selected courses
在手機端使用時,「預覽排課」的功能會導致未經點選+號的「預覽課程」被選入

經儲存於資料庫後會導致學分數計算錯誤。

solution: 在前端先篩選出正確的課程再送給後端API儲存
2022-08-30 20:53:55 +08:00
f8d2147fc9
fix: run as non-sudoer 2022-08-29 18:55:20 +08:00
6b80254e0b
feat: systemd service unit file 2022-08-28 23:02:59 +08:00
70d41cc4bc
fix: change URL 2022-07-26 23:19:52 +08:00
1cf4193ecb
fix: merge py file 2022-01-10 13:44:56 +08:00
4485c14503
feat: 自動從 generalCourse.in 讀取資料,修改通識課程分類(#20) 2022-01-10 13:22:01 +08:00
f351e73844
fix: exception handling when timeout (#22) 2021-08-28 23:49:22 +08:00
b6d20406f8
fix: add timeout parameter(issue #22) 2021-08-28 16:17:20 +08:00
snsd0805
8ed796a2a5
docs: 更新爬蟲程式說明 2021-08-23 18:03:01 +08:00
4ea60b6a9e
Merge branch 'master' of github.com:snsd0805/NCNU_Course 2021-08-23 17:55:27 +08:00
bea92a83f0
fix: update notification 2021-08-23 17:55:11 +08:00
856a44ad61
feat: webpage description 2021-08-23 17:49:22 +08:00
ed7bad423c
feat: 顯示連結在table上 2021-07-25 18:16:05 +08:00
d289a7af55
feat: 新增學分數計算 2021-07-25 17:22:02 +08:00
b9c2089514
fix: 修正格式錯誤的時間 2021-07-17 15:14:04 +08:00
acbc3296f2
fix: 更新api.py 的 cors URL 2021-07-16 22:28:02 +08:00
e4ebb36d4e
fix: update output.json 2021-07-16 21:32:47 +08:00
c24681aa1c
fix: update URL 2021-07-16 21:32:29 +08:00
2afa08efce
feat: 新增1101學年度課表資料 2021-07-16 21:11:43 +08:00
c61489ed9c
feat: 修改爬蟲方式 2021-07-16 21:10:58 +08:00
snsd0805
db020f765f
Merge pull request #18 from x3388638/patch-getTime
Adjust getTime & isOK logic
2021-01-24 23:17:29 +08:00
YY
bf880ae585 Merge branch 'master' into patch-getTime 2021-01-24 18:03:14 +08:00
YY
6ea1bbf9c0 Fix course conflic logic 2021-01-24 18:02:02 +08:00
YY
a3471b4f1d Filter invalid time 2021-01-24 17:51:37 +08:00
12 changed files with 351 additions and 139 deletions

View File

@ -41,6 +41,9 @@
- [x] 把版排好(選課框框改成可下拉(才可以同時看到課表)) - [x] 把版排好(選課框框改成可下拉(才可以同時看到課表))
# 課程爬蟲使用說明 # 課程爬蟲使用說明
> 因為學校教務系統更新通識分類的部份很慢,因此目前的程式碼已經修改成無法對應「通識課程分類」的版本,
> 實際上線的 data 是依靠「工人智慧」,
> 如果有需要爬取資料,建議使用較舊版本的 python code
安裝所需套件 安裝所需套件
``` ```

8
api.py
View File

@ -5,12 +5,16 @@ import sqlite3
from flask_cors import CORS from flask_cors import CORS
app = Flask(__name__) app = Flask(__name__)
CORS(app, resources={r"/.*": {"origins": ["https://snsd0805.com"]}}) CORS(app, resources={r"/.*": {"origins": ["https://course.snsd0805.com"]}})
def facebookAuth(token): def facebookAuth(token):
url = "https://graph.facebook.com/v9.0/me?access_token={}" url = "https://graph.facebook.com/v9.0/me?access_token={}"
response = requests.get(url.format(token)) try:
response = requests.get(url.format(token), timeout=5)
except:
return False, None, None
else:
data = json.loads(response.text) data = json.loads(response.text)
# 若 access code 通過 facebook 驗證 # 若 access code 通過 facebook 驗證

137
generalCourse.in Normal file
View File

@ -0,0 +1,137 @@
department 特色通識—在地實踐
994017
994057
994065
994068
994071
994075
994076
994077
994078
994080
994086
994089
994112
994113
994114
department 特色通識—綠概念
993062
994001
994012
994020
994024
994074
994027
department 特色通識—東南亞
992106
994030
994096
994098
994099
994102
994103
994105
994108
994109
994110
994111
994010
department 自然—生命與科學
993001
993002
993022
993054
993086
993093
993106
993126
993131
993132
993133
993137
993145
993008
department 自然—工程與科技
993023
993052
993055
993060
993064
993075
993116
993156
993157
993013
993066
993111
993120
993143
department 社會—社經與管理
991094
992033
992035
992110
992120
992129
992141
992143
992177
992191
992193
992203
992205
992213
992214
992216
992217
992223
992062
992211
992232
department 社會—法政與教育
984003
992076
992108
992112
992178
992179
992180
992188
992206
992234
992185
department 人文—歷史哲學與文化
991068
991075
991087
991140
991144
991154
991163
991192
991199
991212
992073
992087
992171
994044
department 人文—文學與藝術
460135
991040
991062
991065
991069
991167
991170
991183
991190
991193
991201
991203
991207
991209
991210
991211
992176
991032
991071

View File

@ -4,33 +4,39 @@ import os
import csv import csv
from bs4 import BeautifulSoup as bs from bs4 import BeautifulSoup as bs
USERNAME = ""
PASSWORD = ""
YEAR = 1111
header = { session = requests.Session()
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0',
'Cookie': '輸入登入暨大教務系統後所得到的cookie'
}
mainURL = "https://ccweb.ncnu.edu.tw/student/" mainURL = "https://ccweb.ncnu.edu.tw/student/"
courses = [] courses = []
generalCourse = [] generalCourse = []
def getGeneralCourseData(year): def login(username, password):
''' global session
透過年份取得 通識課程分類的csv檔 response = session.get('https://ccweb.ncnu.edu.tw/student/login.php')
供後續課程對應 root = bs(response.text, 'html.parser')
loginToken = root.find('input', {'name': 'token'}).get('value')
先儲存到 generalCourse list後續再用 courseID 對應通識分類 # request login page
''' response = session.post(
"https://ccweb.ncnu.edu.tw/student/login.php",
data={
'token': loginToken,
'modal': '0',
'username': username,
'password': password,
'type': 'a'
}
)
# 教務系統有開放 年度的query # 成功的話 return http 302, redirect
# 但實際操作後似乎僅開放當前學年度 if len(response.history)!=0:
response = requests.get(mainURL+"aspmaker_student_common_rank_courses_viewlist.php?x_studentid=0&z_studentid=LIKE&x_year={}&z_year=%3D&cmd=search&export=csv".format(year), headers=header) return True
data = response.text else:
return False
courses = data.split('\r\n')[1:-1]
for course in courses:
course = course.split(',')
generalCourse.append(course)
def curlDepartmentCourseTable(year): def curlDepartmentCourseTable(year):
''' '''
@ -39,74 +45,94 @@ def curlDepartmentCourseTable(year):
''' '''
print("取得所有課程資料:") print("取得所有課程資料:")
response = requests.get(mainURL+"aspmaker_course_opened_semester_stat_viewlist.php?x_year={}&recperpage=ALL".format(year), headers=header) # 切換年度,應該是用 cookie 儲存當前閱覽的年份
data = response.text url = 'https://ccweb6.ncnu.edu.tw/student/aspmaker_course_opened_detail_viewlist.php?cmd=search&t=aspmaker_course_opened_detail_view&z_year=%3D&x_year={}&z_courseid=%3D&x_courseid=&z_cname=LIKE&x_cname=&z_deptid=%3D&x_deptid=&z_division=LIKE&x_division=&z_grade=%3D&x_grade=&z_teachers=LIKE&x_teachers=&z_not_accessible=LIKE&x_not_accessible='
root = bs(data, "html.parser") response = session.get(url.format(year))
count = 1 # 取得 所有課程的 csv
departmentsTR = root.findAll('tr')[1:] # 清除 thead response = session.get('https://ccweb6.ncnu.edu.tw/student/aspmaker_course_opened_detail_viewlist.php?export=csv')
for tr in departmentsTR: with open("allCourses.csv", "wb") as fp:
name = tr.findAll('td')[4].find('span').find('span').string # 取得 科系名稱 fp.write(response.content)
link = mainURL + tr.find('a').get('data-url').replace('amp;', '') # 清除不必要符號, 取得 連結
print("擷取{}課程... ({}/{})...".format(name, count, len(departmentsTR)))
count += 1
extractDepartmentCourseTable(name, link) # 透過連結 開始擷取 各科系課程
def extractDepartmentCourseTable(departmentName, link): def extractDepartmentCourseTable(year):
''' '''
透過各科系連結取得課程資訊 透過各科系連結取得課程資訊
若為通識類別還要跟csv檔資料做對應取得正確通識類別 若為通識類別還要跟csv檔資料做對應取得正確通識類別
對應後存取到 output.json 對應後存取到 output.json
''' '''
response = requests.get(link, headers=header) with open("allCourses.csv") as fp:
data = response.text csvData = fp.read()
root = bs(data, "html.parser")
ans = []
courses = csvData.split('"\n')[1:-1]
for course in courses:
course = course.replace('\n', '.')
# print(course)
data = course[1:].split('","')
courseTR = root.findAll('tr')[1:] # 清除 thead
for tr in courseTR:
courseObj = {} courseObj = {}
tds = tr.find_all('td')
courseObj['link'] = mainURL + tds[0].find('a').get('href') baseLink = "https://ccweb6.ncnu.edu.tw/student/aspmaker_course_opened_detail_viewlist.php?cmd=search&t=aspmaker_course_opened_detail_view&z_year=%3D&x_year={}&x_courseid={}"
courseObj['year'] = tds[1].find('span').string courseObj['link'] = baseLink.format(year, data[1].zfill(6))
courseObj['number'] = tds[2].find('span').string courseObj['year'] = data[0]
courseObj['class'] = tds[3].find('span').string courseObj['number'] = data[1]
courseObj['name'] = tds[4].find('span').string courseObj['class'] = data[2]
courseObj['department'] = tds[5].find('span').string courseObj['name'] = data[3]
courseObj['graduated'] = tds[6].find('span').string courseObj['department'] = data[4]
courseObj['grade'] = tds[7].find('span').string courseObj['graduated'] = data[6]
courseObj['teacher'] = tds[8].find('span').string courseObj['grade'] = data[7]
courseObj['place'] = tds[9].find('span').string courseObj['teacher'] = data[8]
courseObj['time'] = tds[11].find('span').string courseObj['place'] = data[9]
courseObj['time'] = data[13].replace(' ', '')
courseObj['credit'] = data[14]
if courseObj['department']=="99, 通識" : ans.append(courseObj)
flag = False
for row in generalCourse:
if row[2] == '"{}"'.format(courseObj['number']):
courseObj['department'] = row[0].replace('"', '')
generalCourse.remove(row)
flag = True
break
if not flag:
print(" - 找不到對應的通識類別: {} ( {} )".format(courseObj['name'], courseObj['number']))
courses.append(courseObj) with open("歷年課程資料/{}_output.json".format(year), 'w') as fp:
json.dump(ans, fp, ensure_ascii=False)
def updateGeneralCourse():
with open("歷年課程資料/{}_output.json".format(YEAR)) as fp:
courses = json.load(fp)
with open("generalCourse.in") as fp:
line = fp.readline()
while line:
count = 0
line = line.split()
if len(line) == 2:
department = line[1]
else:
for course in courses:
if course['number'] == line[0]:
course['department'] = department
count += 1
if count == 0 and len(line) != 2:
print("{} 可能輸入錯誤 - {}".format(line[0], department))
line = fp.readline()
print("還沒有對應到的課程:")
for course in courses:
if course['department'] == "99, 通識":
course['department'] = "99, 通識(未分類)"
print("{} {}".format(course['number'], course['name']))
with open("歷年課程資料/{}_output.json".format(YEAR), "w") as fp:
json.dump(courses, fp, ensure_ascii=False)
with open('output.json', 'w') as fp:
json.dump(courses, fp)
if __name__ == "__main__": if __name__ == "__main__":
year = input("年份: ") while True:
username = USERNAME
password = PASSWORD
if login(username, password):
print("登入成功!")
break
else:
print("登入失敗!")
getGeneralCourseData(year) curlDepartmentCourseTable(YEAR)
curlDepartmentCourseTable(year) extractDepartmentCourseTable(YEAR)
updateGeneralCourse()
print("\n\n=====================")
print("未列入追蹤的通識課程")
print("=====================\n")
for notIn in generalCourse:
if "體育:" not in notIn[5]:
print(" - 未列入追蹤的新通識課程: {}".format(notIn))

View File

@ -3,8 +3,8 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="" /> <meta name="description" content="你還在用紙筆或Excel在安排下學期的課表嗎「暨大排課表」幫你篩選衝堂、科系分類、通識課程分類讓你輕鬆排課表" />
<meta name="author" content="" /> <meta name="author" content="snsd0805" />
<title>暨大排課表</title> <title>暨大排課表</title>
<!-- Favicon--> <!-- Favicon-->
<link rel="icon" type="image/x-icon" href="assets/img/favicon.ico" /> <link rel="icon" type="image/x-icon" href="assets/img/favicon.ico" />

View File

@ -8,35 +8,25 @@ var coursesList = {
}, },
methods: { methods: {
'getTime': function (timeString) { 'getTime': function (timeString) {
if (timeString == null) { let num;
return "" const timeRegex = new RegExp(/^\d[\da-z]*[a-z]$/);
return timeRegex.test(timeString)
? [...timeString].reduce((res, c) => {
if (Number.isInteger(+c)) {
num = c;
return res;
} else {
return [...res, num + c];
} }
}, [])
ans = [] : [];
number = ""
for (var i of timeString) {
if (i >= "0" && i <= "9") {
number = i
} else if (i >= "a" && i <= "z") {
ans.push(number + i)
}
else {
ans.push(timeString)
break
}
}
return ans
}, },
'isOK': function (course) { 'isOK': function (course) {
var time = this.getTime(course.time) var time = this.getTime(course.time)
// console.log(course.name, " ", time) // console.log(course.name, " ", time)
for (t of time) { const isConflict = time.some((t) => this.selectedTime.includes(t))
for (st of this.selectedTime) {
if (t == st) return time.length && !isConflict
return false
}
}
return true
}, },
'log': function (name, data) { 'log': function (name, data) {
console.log(name, data) console.log(name, data)

View File

@ -9,6 +9,7 @@ var mainWindow = {
"user": "", "user": "",
'token': "", 'token': "",
'is_print': false, 'is_print': false,
'creditNum': 0,
} }
}, },
created() { created() {
@ -96,6 +97,14 @@ var mainWindow = {
}).then(function (jsonData) { }).then(function (jsonData) {
console.log(jsonData) console.log(jsonData)
main.selectCourses = JSON.parse(jsonData['data']) main.selectCourses = JSON.parse(jsonData['data'])
var courseSet = new Set()
for (var course of main.selectCourses) {
if (!courseSet.has(course.number+course.class)) { // courseID +
main.creditNum += parseFloat(course.credit)
courseSet.add(course)
}
}
}) })
.catch(function (err) { .catch(function (err) {
alert("錯誤: " + err) alert("錯誤: " + err)
@ -105,6 +114,12 @@ var mainWindow = {
'saveCourseTable': function () { 'saveCourseTable': function () {
var main = this var main = this
if (this.token != "") { if (this.token != "") {
filteredCourses = []
for(var tempCourse of main.selectCourses){
if(tempCourse.temp == false){
filteredCourses.push(tempCourse);
}
}
fetch('https://api.snsd0805.com/courseTable', { fetch('https://api.snsd0805.com/courseTable', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -112,7 +127,7 @@ var mainWindow = {
}, },
body: JSON.stringify({ body: JSON.stringify({
'token': main.token, 'token': main.token,
'data': main.selectCourses 'data': filteredCourses
}) })
}) })
.then(function (response) { .then(function (response) {
@ -134,20 +149,18 @@ var mainWindow = {
} }
}, },
'getTime': function (timeString) { 'getTime': function (timeString) {
ans = [] let num;
number = "" const timeRegex = new RegExp(/^\d[\da-z]*[a-z]$/);
for (var i of timeString) { return timeRegex.test(timeString)
if (i >= "0" && i <= "9") { ? [...timeString].reduce((res, c) => {
number = i if (Number.isInteger(+c)) {
} else if (i >= "a" && i <= "z") { num = c;
ans.push(number + i) return res;
} else {
return [...res, num + c];
} }
else { }, [])
ans.push(timeString) : [];
break
}
}
return ans
}, },
'select': function (department) { 'select': function (department) {
this.selectDepartment = department this.selectDepartment = department
@ -163,9 +176,12 @@ var mainWindow = {
'name': course.name, 'name': course.name,
'temp': false, 'temp': false,
'number': course.number, 'number': course.number,
'class': course.class 'class': course.class,
'credit': course.credit,
'link': course.link
}) })
} }
this.creditNum += parseFloat(course.credit)
}, },
'removeCourse': function (course) { 'removeCourse': function (course) {
console.log("remove " + course.name) console.log("remove " + course.name)
@ -174,6 +190,7 @@ var mainWindow = {
this.selectCourses.splice(i, 1) this.selectCourses.splice(i, 1)
} }
} }
this.creditNum -= parseFloat(course.credit)
}, },
'saveTemp': function (course) { 'saveTemp': function (course) {
if (course == null) { if (course == null) {
@ -275,6 +292,7 @@ var mainWindow = {
</div> </div>
</div> </div>
</div> </div>
<br>
<div class="row"> <div class="row">
<div class="col-lg-3"> <div class="col-lg-3">
<div class="row mx-auto mb-2"> <div class="row mx-auto mb-2">
@ -299,7 +317,18 @@ var mainWindow = {
> >
</course-anslist> </course-anslist>
</div> </div>
<div class="row">
<div class="col">
<div class="card">
<div class="card-body">
已經選了 {{ creditNum }} 學分
</div> </div>
</div>
</div>
</div>
</div>
<br>
<div class="col-lg-9 table-responsive " > <div class="col-lg-9 table-responsive " >
<course-table <course-table
@ -328,12 +357,11 @@ var mainWindow = {
</div> </div>
<div class="modal-body"> <div class="modal-body">
<ul> <ul>
<li>已經更新為 1092 新課表(包含通識課分類)</li> <li>已經更新為 1101 新學期課表(包含通識課分類)</li>
<li>使用 Facebook API 儲存課表</li> <li>有發現 Bug 可以到 <a href='https://github.com/snsd0805/NCNU_Course/issues'>GitHub</a> issue <a href='mailto: levi900227@gmail.com'>mail</a></li>
<li>新增下載圖檔功能</li> <li>請善用連接 Facebook功能來儲存課表</li>
<li>新增分享課表功能</li>
</ul> </ul>
2021 01/23 更新 2021 07/16 更新
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">我知道了</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">我知道了</button>
@ -353,7 +381,7 @@ var mainWindow = {
</div> </div>
<div class="modal-body"> <div class="modal-body">
請複製以下網址給你的朋友跟他分享你的課表<br><br> 請複製以下網址給你的朋友跟他分享你的課表<br><br>
https://snsd0805.com/NCNU_Course/#/share/{{user.id}} https://course.snsd0805.com/#/share/{{user.id}}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">我知道了</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">我知道了</button>

View File

@ -3,6 +3,7 @@ var courseDiv = {
template: ` template: `
<div style='border: 5px #1abc9c solid; text-align: center;'> <div style='border: 5px #1abc9c solid; text-align: center;'>
{{ course.name }} {{ course.name }}
<a v-bind:href="course.link" target="_blank"><i class="fas fa-info-circle"></i></a>
<button type="button" <button type="button"
v-if="!is_shared" v-if="!is_shared"
v-on:click="$emit('remove-course', course)" v-on:click="$emit('remove-course', course)"
@ -54,7 +55,9 @@ var courseTable = {
'name': c.name, 'name': c.name,
'number': c.number, 'number': c.number,
'class': c.class, 'class': c.class,
'temp': c.temp 'temp': c.temp,
'credit': c.credit,
'link': c.link
} }
if(c.time[0]==6 || c.time[0]==7){ if(c.time[0]==6 || c.time[0]==7){

20
ncnu-course-api.service Normal file
View File

@ -0,0 +1,20 @@
[Unit]
Description=NCNU-Course Python Backend API
After=network.target
[Service]
Type=simple
ExecStart=python3 api.py
Restart=always
WorkingDirectory=/var/www/html/NCNU_Course
User=course
RestartSec=10s
StandardOutput=syslog
StandardOutput=syslog
SyslogIdentifier=ncnu-course
[Install]
WantedBy=multi-user.target

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long