解析 URP 教务系统, 创建查成绩 APP !

Posted on 2021-01-28  67 Views


写在前面

国庆在学校没事干,正好某课程表的查成绩功能又双叕崩了,一怒之下把它卸载!(课程表功能推荐苏大学长写的 wakeup课程表,各大商店都有)

正好学了点 kotlin,开始了我的小白安卓开发之旅~

项目地址:Github:https://github.com/SukiEva/HHUGrades

APK下载:https://github.com/SukiEva/HHUGrades/releases

APK也可以在蓝奏云下载:https://suki.lanzous.com/iIk7yh67eih 密码:b9ez
欢迎 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设计写死人,珍爱生命,远离安卓!


隐约雷鸣 阴霾天空 但盼风雨来 能留你在此