写在前面
国庆在学校没事干,正好某课程表的查成绩功能又双叕崩了,一怒之下把它卸载!(课程表功能推荐苏大学长写的 wakeup课程表,各大商店都有)
正好学了点 kotlin,开始了我的小白安卓开发之旅~
2021年更新:
适配了最新版URP系统,美化UI设计,修改项目地址为:
https://github.com/SukiEva/Myhhu
欢迎 Star 和 Fork!
特别警告:
连接教务系统需在内网下,即连接校园网才能成功,
直接使用流量连接会卡死,请在校园网或校园VPN连接下使用该APP!
成果图
(别问我为什么有的UI没对齐,都是为了适配我自己的手机o(╥﹏╥)o
开始干活
声明一下使用了哪些依赖,防止看代码看不懂,很多操作通过已有的库会简化很多。
implementation 'org.jsoup:jsoup:1.13.1'
implementation "com.squareup.okhttp3:okhttp:4.9.0"
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation "org.jetbrains.anko:anko:$anko_version"
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4'
1、模拟登陆
模拟登陆和爬取信息我之前用python的Requests库写过了,所以只是把相关换成了Java 的 okhttp 和 jsoup。
思路是通过okhttp请求验证码的链接,并记录cookie,通过 bitmap 将图片显示在android上。
这部分我是看的 Android客户端加载网站验证码(okHttp Jsoup)
网页请求分析我就不介绍了,直接贴代码:
// LoginActivity.kt
private fun loadingCaptchaPic() {
//client = OkHttpClient()
initJwxt()
client = OkHttpClient().newBuilder()
.cookieJar(object : CookieJar {
//cookie的缓存区
private val cookieStore: HashMap<String, List<Cookie>> = HashMap()
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
//添加cookie
cookieStore[url.host] = cookies
cookie = cookies[0].name + "=" + cookies[0].value
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val cookies = cookieStore[url.host]
//当Request 连接到网络的时候,OkHttp会调用loadForRequest()
// if (cookies != null) {
// println("加载了cookie:" + cookies)
// }
return cookies ?: ArrayList()
}
}).build()
val ImgUrl = homeUrl + "validateCodeAction.do"
//加载验证码图片代码
Thread(
object : Runnable {
var captchaPic: Bitmap? = null
override fun run() {
try {
val request = Request.Builder()
.removeHeader("User-Agent")
.addHeader(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4209.2 Safari/537.36"
)
.url(ImgUrl)
.build()
val response = client!!.newCall(request).execute()
val `is`: InputStream = response.body!!.byteStream()
captchaPic = BitmapFactory.decodeStream(`is`)
checkCodePicture?.post({ checkCodePicture!!.setImageBitmap(captchaPic) })
} catch (e: Exception) {
e.printStackTrace()
}
}
}).start()
}
因为我校有4个教务系统网址,有时候会随机崩几个,所以在请求之前还写了个找到没崩网址的方法:
// LoginActivity.kt
private fun initJwxt() {
val headerMap = mapOf(
"User-Agent" to "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4209.2 Safari/537.36"
)
var pos = 0
var time = 0
while (time < 15) {
homeUrl = homeUrls[pos]
try {
Jsoup
.connect(homeUrl)
.headers(headerMap)
.ignoreContentType(true)
.ignoreHttpErrors(true)
.timeout(2000)
.execute()
return
} catch (e: Exception) {
pos++
if (pos > 3) pos = 0
} finally {
time++
}
}
if (time >= 15) {
alert("教务系统崩溃啦!!![○・`Д´・ ○]") { positiveButton("٩( 'ω' )و get!") {} }
}
return
}
验证码图片搞定,然后就是 Post 过去就行了,okhttp开启cookie后会自动保持,我们直接用同一个 client 就行:
(细心就会发现我里面还放了处理信息的函数,会在下面介绍~)
// LoginActivity.kt
private fun ButtonClickHandler() {
loginNum = findViewById<EditText>(R.id.loginNum).text.toString()
loginPassword = findViewById<EditText>(R.id.loginPassword).text.toString()
yzm = findViewById<EditText>(R.id.loginYzm).text.toString()
when {
loginNum.equals("") -> {
alert("请填写学号! ̄へ ̄") { positiveButton("٩( 'ω' )و get!") {} }.show()
return
}
loginPassword.equals("") -> {
alert("请填写密码!(* ̄︿ ̄)") { positiveButton("٩( 'ω' )و get!") {} }.show()
return
}
yzm.equals("") -> {
alert("请填写验证码!凸(艹皿艹 )") { positiveButton("٩( 'ω' )و get!") {} }.show()
return
}
}
// android sharedpreferences 保存账号密码,可略过~
if (rembox!!.isChecked) {
val editor = sp!!.edit()
editor.putString("uname", loginNum)
editor.putString("upswd", loginPassword)
editor.putBoolean("checkboxBoolean", true)
editor.commit()
} else {
val editor = sp!!.edit()
editor.putString("uname", null)
editor.putString("upswd", null)
editor.putBoolean("checkboxBoolean", false)
editor.commit()
}
val LoginUrl = homeUrl + "loginAction.do"
val requestbody: RequestBody = FormBody.Builder()
.add("zjh", loginNum)
.add("mm", loginPassword)
.add("v_yzm", yzm)
.build()
try {
val request = Request.Builder()
.removeHeader("User-Agent")
.addHeader(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4209.2 Safari/537.36"
)
.url(LoginUrl)
.post(requestbody)
.build()
val response = client!!.newCall(request).execute()
val homehtml = response.body?.string()
if (homehtml!!.contains("学分制综合教务")) {
val grades = GetGrades()
val rank = GetRank()
val datas = Datas(grades, rank)
val intent:Intent
if (this.flag)
intent = Intent(this, ShowResultsActivity::class.java)
else
intent = Intent(this, ShowRankActivity::class.java)
intent.putExtra("datas", datas)
indeterminateProgressDialog("登录中")
startActivity(intent)
finish()
} else {
alert("要不重试一下?…(⊙_⊙;)…") {
title = "登录失败꒰꒪꒫꒪⌯꒱"
positiveButton("٩( 'ω' )و get!") {}
}.show()
findViewById<EditText>(R.id.loginYzm).setText("")
loadingCaptchaPic()
}
} catch (e: Exception) { // 一般就是登录失败~
e.printStackTrace()
alert("要不重试一下?…(⊙_⊙;)…") {
title = "登录失败꒰꒪꒫꒪⌯꒱"
positiveButton("٩( 'ω' )و get!") {}
}.show()
findViewById<EditText>(R.id.loginYzm).setText("")
loadingCaptchaPic()
}
}
相关 Layout 代码建议查看源码,我贴一大堆也没人想看~
2、爬取信息
推荐另一个 获取正方教务系统成绩文章,我参考了一下,适配 URP 系统~
主要获取成绩信息和排名信息,同理 请求分析自行解决~
// LoginActivity.kt
fun GetGrades(): MutableList<List<String>>? {
val GradesUrl = homeUrl + "bxqcjcxAction.do"
try {
val courses: MutableList<List<String>> = mutableListOf()
val request = Request.Builder()
.removeHeader("User-Agent")
.addHeader(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4209.2 Safari/537.36"
)
.url(GradesUrl)
.build()
val response = client!!.newCall(request).execute()
val gradeshtml = response.body?.string()
val parse = Jsoup.parse(gradeshtml)
val trs = parse.getElementsByClass("odd")
val tds = trs.tagName("td")
val regex = Regex("[a-z\"]+", RegexOption.IGNORE_CASE)
val regex2 = Regex("\\s+")
for (td in tds) {
val str = td.text().replace(regex, "")
val course: MutableList<String> = str.split(regex2).toMutableList()
val ncouse: MutableList<String> = mutableListOf()
if (course.size >= 11) course.removeAt(3)
ncouse.add(course[2])
ncouse.add(course[8])
ncouse.add("课程属性:" + course[4])
ncouse.add("学分:" + course[3])
courses.add(ncouse)
}
return courses
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
fun GetRank(): MutableList<String>? {
val RankUrl = homeUrl + "reportFiles/bzrcx/jdpmcx.jsp?temp=1"
try {
val rankinfos: MutableList<String> = mutableListOf()
val request = Request.Builder()
.removeHeader("User-Agent")
.addHeader(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4209.2 Safari/537.36"
)
.url(RankUrl)
.build()
val response = client!!.newCall(request).execute()
val rankhtml = response.body?.string()
val parse = Jsoup.parse(rankhtml)
val infos = parse.getElementsByClass("report1_1_3_1")
for (info in infos) {
rankinfos.add(info.text())
}
return rankinfos
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
3、相关 UI 设计
自己随便写了些,想美化的自己修改,代码请去Github查看~
总结一下
数据获取随便写,安卓Layout设计写死人,珍爱生命,远离安卓!