Skip to content

Commit

Permalink
Merge pull request #51 from ludoux/master
Browse files Browse the repository at this point in the history
西北工业大学:新教务适配;Parser.kt 更新了上游的可被重写方法
  • Loading branch information
YZune authored Apr 10, 2022
2 parents 9399a8c + 224c767 commit eff8260
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 3 deletions.
17 changes: 17 additions & 0 deletions src/main/java/LoginException.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main.java.exception

class NetworkErrorException(message: String) : Exception(message)

class ServerErrorException(message: String) : Exception(message)

class UserNameErrorException(message: String) : Exception(message)

class PasswordErrorException(message: String) : Exception(message)

class CheckCodeErrorException(message: String) : Exception(message)

class QueuingUpException(message: String) : Exception(message)

class GetTermDataErrorException(message: String) : Exception(message)

class EmptyException(message: String) : Exception(message)
304 changes: 304 additions & 0 deletions src/main/java/parser/NWPUParser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
package main.java.parser

import Common

import bean.Course
import com.google.gson.JsonParser
import main.java.exception.EmptyException
import main.java.exception.GetTermDataErrorException
import main.java.exception.NetworkErrorException
import main.java.exception.PasswordErrorException
import org.jsoup.Connection
import org.jsoup.Jsoup
import parser.Parser

/*
* // 接口地址
/* harmony default export */ __webpack_exports__[\"default\"] = ({
// 获取所有学期
getAllSemester: function getAllSemester() {
return axios__WEBPACK_IMPORTED_MODULE_1___default.a.get('/api/semester/list');
},
// 获取所有周课表
getAllWeek: function getAllWeek(id) {
return axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(\"/api/semester/weekList/\".concat(id));
},
// 获取所有周课表
getSemesterAndWeek: function getSemesterAndWeek(id) {
return axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(\"/api/semester/semesterAndWeek/\".concat(id));
},
// 根据学期+周id 获取课程表数据
getCourseByWeekId: function getCourseByWeekId(semesterId, weekId) {
return axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(\"/api/courseTable/\".concat(semesterId, \"/\").concat(weekId));
},
// 获取当前周的数据
getCurrentWeek: function getCurrentWeek() {
return axios__WEBPACK_IMPORTED_MODULE_1___default.a.get('/api/semester/currentWeek');
},
// 获取学生名单列表
getStudentList: function getStudentList(id) {
return axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(\"/api/stdList/list/\".concat(id));
},
// 课程安排表
getSchedule: function getSchedule(semesterId, courseId) {
return axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(\"/api/courseList/schedule/\".concat(semesterId, \"/\").concat(courseId));
},
// 获取学期全部课程
getAllCourseBySemesterId: function getAllCourseBySemesterId(semesterId) {
return axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(\"/api/courseList/list/\".concat(semesterId));
},
// 获取课程选项数据
getCourseOptions: function getCourseOptions(semesterId) {
return axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(\"/api/courseList/names/\".concat(semesterId));
},
// 根据课程名获取课程列表
getCourseByName: function getCourseByName(semesterId, keyword) {
return axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(\"/api/courseList/list/\".concat(semesterId, \"/\").concat(keyword));
},
//获取用户头像
getAvatar: function getAvatar(stdCode) {
return axios__WEBPACK_IMPORTED_MODULE_1___default.a.get(\"/api/user/avatar/\".concat(stdCode));
}
});
* */
/*
维护小提示:
接口是从 https://students-schedule.nwpu.edu.cn/ui/#/courseTable 来的
大致为 get 拿cookie -> post 发登录凭证,回复url有token -> token 写 header 里面取所有学期列表 -> token 写 header 根据匹配的学期拿课表
关于时间表:是在作者私仓 com/suda/yzune/wakeupschedule/schedule_import/ImportViewModel.kt 里addNwpuTimeTables函数写的。
不管导入的是哪个校区,如上函数都会新建所有的时间表(存在同名不会覆盖)
假如时间表变更,请联系作者更改里面的函数。注意假如有同名时间表将不会覆盖,所以建议新建以“西工大长安v2”类似来命名,同时这里的_timeTableName也相应更改。
假如太仓校区确认了,同上逻辑,建议新建以“西工大太仓”类似来命名,这里的_timeTableName也相应更改。
关于学期开始日期等:_startDate等变量以及 override 的相关 get 函数,wakeup会读取自动正确存储。这里是因为courseadapter没做相关功能,所以导出的wakeup数据时间等不对,但实际上app是对的。
假如你要改相关UI,在作者私仓 com/suda/yzune/wakeupschedule/schedule_import/LoginWebFragment.kt 里,请联系开发者。
*/

/*
* Last_Update: 2022-4-9
* Creator: @ludoux ([email protected])
* Maintainer:
*/
class NWPUParser(
private val xh: String,
private val pwd: String,
private val semesterYear: String,
private val semesterTerm: Int
) : Parser("") {
private val headers: Map<String, String>? = mapOf(
"User-Agent" to "Mozilla/5.0 (X11; Linux x86_64; rv:99.0) Gecko/20100101 Firefox/99.0",
"Accept-Language" to "zh-CN"
)
private val timeout = 5000

private var _timeTableName = "西工大长安"
private var _tableName = "西工大 2020-2021 春学期"
private var _nodes = 13
private var _startDate = "1970-01-01"
private var _maxWeek = 22

//相当于 login_school/NWPU/nwpu 里的 getWebApi。所以这两个函数不建议融合在一起
private fun webFunc(): String {
//针对 vue 应用网页 https://students-schedule.nwpu.edu.cn/ui/
var rt = ""
val cookies: Map<String, String>?
var token: String? = ""
if (false) {//强制SSO//原来是[port==1] 教务系统直接登录
/*cookies = withContext(Dispatchers.IO) {
Jsoup.connect("http://us.nwpu.edu.cn/eams/login.action")
.headers(headers)//第一步获取cookies
.timeout(timeout).method(Connection.Method.GET).execute().cookies()
}
responseBody = withContext(Dispatchers.IO) {
Jsoup.connect("http://us.nwpu.edu.cn/eams/login.action").headers(headers)
.cookies(cookies)//第二步骤模拟登录
.data("username", xh).data("password", pwd).data("encodedPassword", "")
.data("session_locale", "zh_CN")
.timeout(timeout).method(Connection.Method.POST).execute().body()
}
if (responseBody.contains("欢迎使用西北工业大学教务系统。")) {
//ok
} else if (responseBody.contains("已经被锁定")) {
throw NetworkErrorException("账号被锁定,请稍等后重试")
} else if (responseBody.contains("密码错误")) {
throw PasswordErrorException("密码错误哦")
} else if (responseBody.contains("账户不存在")) {
throw UserNameErrorException("登录失败,账户不存在。")
} else if (responseBody.contains("验证码不正确")) {
throw NetworkErrorException("登录失败,失败尝试过多,请尝试更换网络环境。")
}*/
} else {//走统一登录
var response =
Jsoup.connect("https://uis.nwpu.edu.cn/cas/login?service=https://students-schedule.nwpu.edu.cn/login/cas?redirect_uri=https://students-schedule.nwpu.edu.cn/ui/")
.headers(headers)//第一步获取cookies
.timeout(timeout).method(Connection.Method.GET).execute()

cookies = response.cookies()
//Copy from SUSTechParser.kt@GGAutomaton,thanks!
val executionValue =
response.parse().getElementsByAttributeValue("name", "execution")[0].attributes().get("value")
response =
Jsoup.connect("https://uis.nwpu.edu.cn/cas/login?service=https://students-schedule.nwpu.edu.cn/login/cas?redirect_uri=https://students-schedule.nwpu.edu.cn/ui/")
.headers(headers).cookies(cookies)//第二步骤模拟登录
.data("username", xh).data("password", pwd).data("currentMenu", "1")
.data("execution", executionValue).data("_eventId", "submit").data("geolocation", "")
.data("submit", "稍等片刻……")
.timeout(timeout).method(Connection.Method.POST).execute()

if (response.statusCode() != 200 && response.statusCode() != 302) {
throw PasswordErrorException("登录失败。因帐号或密码错误,或失败过多暂时被锁定,请核对或稍后更换网络后重试。[${response.statusCode()}]")
}

token = Regex("token=.+$").find(response.url().toString())?.value
if (token != null) {
token = token.replace("token=", "")
} else {
throw NetworkErrorException("获取 token 失败,这不应该发生。若持续出错请联系维护者。[${response.url()}]")
}
}

//获得学期id
var response =
Jsoup.connect("https://students-schedule.nwpu.edu.cn/api/semester/list")
.header("X-Id-Token", token).header("X-Device-Info", "PC").header("X-Requested-With", "XMLHttpRequest")
.header("X-Terminal-Info", "PC").header("X-User-Type", "student")
.ignoreContentType(true)//Jsoup 不支持返回的content-type为 application/json,所以强制忽略
.timeout(5000).method(Connection.Method.GET).execute()

rt = response.body()

//本来不应该解析json的,但是考虑到要获取课表要先知道学期id,所以还是解析了。
var json = JsonParser.parseString(response.body())
var semesterId = ""
if (json.asJsonObject.get("msg").asString == "成功") {
val semestersName = "秋春夏"
json.asJsonObject.get("data").asJsonArray.forEach {
var curSemesterName = it.asJsonObject.get("name").toString()
//以防后面学校会变更,所以这里不精确匹配
if (curSemesterName.contains("$semesterYear-") && curSemesterName.contains(semestersName[semesterTerm])) {
semesterId = it.asJsonObject.get("id").asString
}
}
} else {
throw GetTermDataErrorException("没有在教务网站上查找到相关学期信息,请重新选择。若持续出错请联系维护者。")
}

response =
Jsoup.connect("https://students-schedule.nwpu.edu.cn/api/courseList/list/$semesterId")
.header("X-Id-Token", token).header("X-Device-Info", "PC").header("X-Requested-With", "XMLHttpRequest")
.header("X-Terminal-Info", "PC").header("X-User-Type", "student")
.ignoreContentType(true)//Jsoup 不支持返回的content-type为 application/json,所以强制忽略
.timeout(5000).method(Connection.Method.GET).execute()


json = JsonParser.parseString(response.body())
println(json.asJsonObject.get("msg").asString)
if (json.asJsonObject.get("msg").asString != "成功") {
throw EmptyException("查询课表信息失败,这不应该发生。若持续出错请联系维护者。[${json.asJsonObject.get("msg").asString}]")
}
rt = rt + "<split>" + response.body() + "<split>" + semesterId
//所以rt其实是 回应1<split>回应2<split>学期id
return rt
}

override fun generateCourseList(): List<Course> {
val ori = webFunc().split("<split>")
//回应1<split>回应2<split>学期id
var json = JsonParser.parseString(ori[0])
val semesterId = ori[2]

json.asJsonObject.get("data").asJsonArray.forEach {
if (it.asJsonObject.get("id").asString == semesterId) {
_tableName = "西工大 " + it.asJsonObject.get("code").asString // 西工大 2019-2020-秋
_maxWeek = it.asJsonObject.get("weeks").asJsonArray.count()
_startDate = it.asJsonObject.get("startDay").asString
}
}

json = JsonParser.parseString(ori[1])

val courseList = ArrayList<Course>()
json.asJsonObject.get("data").asJsonArray.forEach { course ->


//如果为 false,则为网课一类,不会在日历上显示,skip
if (!course.asJsonObject.get("hasSchedule").asBoolean)
return@forEach//相当于continue

course.asJsonObject.get("weekdayUnits").asJsonArray.forEach { unit ->

val weekList = arrayListOf<Int>()
//weeks: [1,2,3,5]
unit.asJsonObject.get("weeks").asJsonArray.forEach { x ->
weekList.add(x.asInt)
}

Common.weekIntList2WeekBeanList(weekList).forEach {
courseList.add(Course(
name = course.asJsonObject.get("name").asString,
day = unit.asJsonObject.get("weekday").asInt,
room = unit.asJsonObject.get("roomString").asString.replace(regex = Regex("(长安|友谊|太仓)校区 "), replacement = ""),
teacher = unit.asJsonObject.get("teacherString").asString.toString().trim(),
startNode = unit.asJsonObject.get("startUnit").asInt,
endNode = unit.asJsonObject.get("endUnit").asInt,
startWeek = it.start,
endWeek = it.end,
type = it.type,
credit = course.asJsonObject.get("credit").asFloat,
note = course.asJsonObject.get("code").asString
))

when {
unit.asJsonObject.get("roomString").asString.contains("长安校区") -> {
_nodes = 13
_timeTableName = "西工大长安"
}
unit.asJsonObject.get("roomString").asString.contains("友谊校区") -> {
_nodes = 12
val month = java.util.Calendar.getInstance().get(java.util.Calendar.MONTH)
_timeTableName = if (month >= 10 || month <= 4) {
"西工大友谊冬(10.1-4.30)"
} else {
"西工大友谊夏(5.1-9.30)"
}
}
unit.asJsonObject.get("roomString").asString.contains("太仓校区") -> {
_nodes = 13
_timeTableName = "西工大太仓(未实现)"
}
}
}
}
}
println("课表名称:$_tableName\n启用时间表:$_timeTableName\n每天节数:$_nodes\n学期开始日期(务必周一):$_startDate\n学期周数:$_maxWeek")
return courseList
}

/*override fun generateTimeTable(): TimeTable {
return TimeTable(name = _timeTableName, timeList = listOf())
}*/
override fun getTableName(): String {
return _tableName
}

override fun getNodes(): Int {
return _nodes
}

override fun getStartDate(): String? {
return if (_startDate == "1970-01-01") {
null
} else {
_startDate
}
}

override fun getMaxWeek(): Int? {
return _maxWeek
}

}
8 changes: 8 additions & 0 deletions src/main/java/parser/Parser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ abstract class Parser(val source: String) {
// TimeTable中的name属性将起到标识作用,如果在数据库中发现同名时间表,则不再覆盖写入
open fun generateTimeTable(): TimeTable? = null

open fun getTableName(): String? = null

open fun getNodes(): Int? = null

open fun getStartDate(): String? = null

open fun getMaxWeek(): Int? = null

private fun convertCourse() {
generateCourseList().forEach { course ->
var id = Common.findExistedCourseId(_baseList, course.name)
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/parser/qz/QzCrazyParser.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package parser.qz

class QzCrazyParser(source: String) : QzParser(source) {
override val tableName: String
override val webTableName: String
get() = "kbcontent1"
}
4 changes: 2 additions & 2 deletions src/main/java/parser/qz/QzParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ open class QzParser(source: String) : Parser(source) {

private val sundayFirstDayMap = arrayOf(0, 7, 1, 2, 3, 4, 5, 6)
private var sundayFirst = false
open val tableName = "kbcontent"
open val webTableName = "kbcontent"

open fun parseCourseName(infoStr: String): String {
return Jsoup.parse(infoStr.substringBefore("<font").trim()).text()
Expand Down Expand Up @@ -84,7 +84,7 @@ open class QzParser(source: String) : Parser(source) {
day++
val divs = td.getElementsByTag("div")
for (div in divs) {
val courseElements = div.getElementsByClass(tableName)
val courseElements = div.getElementsByClass(webTableName)
if (courseElements.text().isBlank()) {
continue
}
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/test/NWPUTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main.java.test

import main.java.parser.NWPUParser

fun main() {
//NWPUParser 里面有维护小提示,希望可以帮助到你。
//秋春夏对应0、1、2
val parser = NWPUParser("2019000000", "password", "2021", 1)
parser.saveCourse(true)
}

0 comments on commit eff8260

Please sign in to comment.