Compare commits

..

No commits in common. "master" and "dev" have entirely different histories.
master ... dev

12 changed files with 137 additions and 349 deletions

View File

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

20
api.py
View File

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

View File

@ -1,137 +0,0 @@
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,39 +4,33 @@ import os
import csv
from bs4 import BeautifulSoup as bs
USERNAME = ""
PASSWORD = ""
YEAR = 1111
session = requests.Session()
header = {
'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/"
courses = []
generalCourse = []
def login(username, password):
global session
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')
def getGeneralCourseData(year):
'''
透過年份取得 通識課程分類的csv檔
供後續課程對應
先儲存到 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
# 但實際操作後似乎僅開放當前學年度
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)
data = response.text
# 成功的話 return http 302, redirect
if len(response.history)!=0:
return True
else:
return False
courses = data.split('\r\n')[1:-1]
for course in courses:
course = course.split(',')
generalCourse.append(course)
def curlDepartmentCourseTable(year):
'''
@ -45,94 +39,74 @@ def curlDepartmentCourseTable(year):
'''
print("取得所有課程資料:")
# 切換年度,應該是用 cookie 儲存當前閱覽的年份
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='
response = session.get(url.format(year))
response = requests.get(mainURL+"aspmaker_course_opened_semester_stat_viewlist.php?x_year={}&recperpage=ALL".format(year), headers=header)
data = response.text
root = bs(data, "html.parser")
count = 1
departmentsTR = root.findAll('tr')[1:] # 清除 thead
for tr in departmentsTR:
name = tr.findAll('td')[4].find('span').find('span').string # 取得 科系名稱
link = mainURL + tr.find('a').get('data-url').replace('amp;', '') # 清除不必要符號, 取得 連結
print("擷取{}課程... ({}/{})...".format(name, count, len(departmentsTR)))
count += 1
extractDepartmentCourseTable(name, link) # 透過連結 開始擷取 各科系課程
# 取得 所有課程的 csv
response = session.get('https://ccweb6.ncnu.edu.tw/student/aspmaker_course_opened_detail_viewlist.php?export=csv')
with open("allCourses.csv", "wb") as fp:
fp.write(response.content)
def extractDepartmentCourseTable(year):
def extractDepartmentCourseTable(departmentName, link):
'''
透過各科系連結取得課程資訊
若為通識類別還要跟csv檔資料做對應取得正確通識類別
對應後存取到 output.json
'''
with open("allCourses.csv") as fp:
csvData = fp.read()
response = requests.get(link, headers=header)
data = response.text
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 = {}
tds = tr.find_all('td')
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['link'] = baseLink.format(year, data[1].zfill(6))
courseObj['year'] = data[0]
courseObj['number'] = data[1]
courseObj['class'] = data[2]
courseObj['name'] = data[3]
courseObj['department'] = data[4]
courseObj['graduated'] = data[6]
courseObj['grade'] = data[7]
courseObj['teacher'] = data[8]
courseObj['place'] = data[9]
courseObj['time'] = data[13].replace(' ', '')
courseObj['credit'] = data[14]
ans.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)
courseObj['link'] = mainURL + tds[0].find('a').get('href')
courseObj['year'] = tds[1].find('span').string
courseObj['number'] = tds[2].find('span').string
courseObj['class'] = tds[3].find('span').string
courseObj['name'] = tds[4].find('span').string
courseObj['department'] = tds[5].find('span').string
courseObj['graduated'] = tds[6].find('span').string
courseObj['grade'] = tds[7].find('span').string
courseObj['teacher'] = tds[8].find('span').string
courseObj['place'] = tds[9].find('span').string
courseObj['time'] = tds[11].find('span').string
if courseObj['department']=="99, 通識" :
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', 'w') as fp:
json.dump(courses, fp)
if __name__ == "__main__":
while True:
username = USERNAME
password = PASSWORD
if login(username, password):
print("登入成功!")
break
else:
print("登入失敗!")
curlDepartmentCourseTable(YEAR)
extractDepartmentCourseTable(YEAR)
updateGeneralCourse()
year = input("年份: ")
getGeneralCourseData(year)
curlDepartmentCourseTable(year)
print("\n\n=====================")
print("未列入追蹤的通識課程")
print("=====================\n")
for notIn in generalCourse:
if "體育:" not in notIn[5]:
print(" - 未列入追蹤的新通識課程: {}".format(notIn))

View File

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

View File

@ -8,25 +8,35 @@ var coursesList = {
},
methods: {
'getTime': function (timeString) {
let num;
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];
}
}, [])
: [];
if (timeString == null) {
return ""
}
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) {
var time = this.getTime(course.time)
// console.log(course.name, " ", time)
const isConflict = time.some((t) => this.selectedTime.includes(t))
return time.length && !isConflict
for (t of time) {
for (st of this.selectedTime) {
if (t == st)
return false
}
}
return true
},
'log': function (name, data) {
console.log(name, data)

View File

@ -9,7 +9,6 @@ var mainWindow = {
"user": "",
'token': "",
'is_print': false,
'creditNum': 0,
}
},
created() {
@ -97,14 +96,6 @@ var mainWindow = {
}).then(function (jsonData) {
console.log(jsonData)
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) {
alert("錯誤: " + err)
@ -114,12 +105,6 @@ var mainWindow = {
'saveCourseTable': function () {
var main = this
if (this.token != "") {
filteredCourses = []
for(var tempCourse of main.selectCourses){
if(tempCourse.temp == false){
filteredCourses.push(tempCourse);
}
}
fetch('https://api.snsd0805.com/courseTable', {
method: 'POST',
headers: {
@ -127,7 +112,7 @@ var mainWindow = {
},
body: JSON.stringify({
'token': main.token,
'data': filteredCourses
'data': main.selectCourses
})
})
.then(function (response) {
@ -149,18 +134,20 @@ var mainWindow = {
}
},
'getTime': function (timeString) {
let num;
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
},
'select': function (department) {
this.selectDepartment = department
@ -176,12 +163,9 @@ var mainWindow = {
'name': course.name,
'temp': false,
'number': course.number,
'class': course.class,
'credit': course.credit,
'link': course.link
'class': course.class
})
}
this.creditNum += parseFloat(course.credit)
},
'removeCourse': function (course) {
console.log("remove " + course.name)
@ -190,7 +174,6 @@ var mainWindow = {
this.selectCourses.splice(i, 1)
}
}
this.creditNum -= parseFloat(course.credit)
},
'saveTemp': function (course) {
if (course == null) {
@ -292,7 +275,6 @@ var mainWindow = {
</div>
</div>
</div>
<br>
<div class="row">
<div class="col-lg-3">
<div class="row mx-auto mb-2">
@ -317,18 +299,7 @@ var mainWindow = {
>
</course-anslist>
</div>
<div class="row">
<div class="col">
<div class="card">
<div class="card-body">
已經選了 {{ creditNum }} 學分
</div>
</div>
</div>
</div>
</div>
<br>
<div class="col-lg-9 table-responsive " >
<course-table
@ -357,11 +328,12 @@ var mainWindow = {
</div>
<div class="modal-body">
<ul>
<li>已經更新為 1101 新學期課表(包含通識課分類)</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>請善用連接 Facebook功能來儲存課表</li>
<li>已經更新為 1092 新課表(包含通識課分類)</li>
<li>使用 Facebook API 儲存課表</li>
<li>新增下載圖檔功能</li>
<li>新增分享課表功能</li>
</ul>
2021 07/16 更新
2021 01/23 更新
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">我知道了</button>
@ -381,7 +353,7 @@ var mainWindow = {
</div>
<div class="modal-body">
請複製以下網址給你的朋友跟他分享你的課表<br><br>
https://course.snsd0805.com/#/share/{{user.id}}
https://snsd0805.com/NCNU_Course/#/share/{{user.id}}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">我知道了</button>
@ -392,4 +364,4 @@ var mainWindow = {
</div>
</div>
`
}
}

View File

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

View File

@ -1,20 +0,0 @@
[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