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

    科技2022-07-15  222

    文章目录

    写在前面成果图开始干活1、模拟登陆2、爬取信息3、相关 UI 设计 总结一下

    写在前面

    国庆在学校没事干,正好某课程表的查成绩功能又双叕崩了,一怒之下把它卸载!(课程表功能推荐苏大学长写的 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设计写死人,珍爱生命,远离安卓!

    Processed: 0.015, SQL: 8