黑大助手 的功能:
黑大助手是一款为黑龙江大学在校学生服务的 Android 端应用,提供 查课表,查成绩,查考试,问答社区以及获取校内最新资讯的功能。
下面开始整理整个项目从构思到完成这个过程的一些学习经历
由于学校官方移动应用 i黑大实在太难用,而且数据更新不及时,消息推送不及时,所以想着自己写一个应用,可以查看一些学生信息,比如课程表,成绩表,考试安排等信息,思路还是比较简单的,就是使用爬虫抓取数据,然后放在移动端展示。
有了思路,接下来就是实现思路的过程,从思路来看,这个项目分为两部分:后端爬虫抓取数据,移动端展示数据
其实最初设想的时候是没有考虑到使用后端的,爬虫可以直接在移动端实现,Java 中的 Jsoup 是一个很不错的网页解析库,用它可以在 Android 端很轻松的实现爬虫功能,但是考虑到可复用性和可扩展性(比如要想在 iOS 上开发,那就还要再写一个 iOS 上的爬虫,或者校园网网页改版了,移动端也要跟着改爬虫代码…),最终没有选择这样的方案,最终决定搭建一个后台,用 Python 编写爬虫,解析成 json 格式的数据,提供接口给移动端调用
目前主流的云服务器都支持 Python 开发环境,而且学习 Python 有一段时间了,正好用来练练手,于是选择使用 Python 写爬虫和部署后端应用。起初的模拟登录过程很容易实现,只需要观察页面跳转,和一些数据传递,这里推荐一下 Windows 上的 Fiddler,一款很好用的抓包工具,因为校园网的登录页面中,如果登录成功,需要打开新窗口,如果使用浏览器自带的开发者工具查看网络,观察起来会比较费劲,而如果使用 Fiddler4 查看,计算机上所有的网络请求都会被记录下来,按着顺序查看就可以了,这样子对于分析网页数据流程很方便。
前面模拟登录的过程就是分析网页流程的过程,通过模拟发送 GET 或者 POST 请求,获取返回数据,就能解析到需要的数据,使用了 Python 中的 Requests 库,当然这种简单的请求使用内置的 urllib 库也能实现,这里选择使用 Requests 的原因是校园网的一些安全措施,在某些页面需要 session 验证,而 Requests 中可以使用 session 类来实现,非常方便。
在校园网上的某些页面,比如显示课程表的页面,数据是通过 javascript 动态加载出来的,如果使用上面那种方法,直接构造请求获取数据,得到的网页中只是包含有 javascript 的代码,而不是它运行过后获取到的课程表数据,这样就没办法解析数据了,必须要执行这段 javascript 代码来获取数据。经过查阅资料,发现有两种方法来解决:
第一种方法是模拟浏览器内核来执行这段代码,获取最后的结果数据,在 Python 中的实现就是使用 Selenium + PhantomJS,通过这两个库,可以模拟出浏览器环境来执行 javascript 代码,关于他们的使用,可以在官网查阅文档;
第二种方法是分析 javascript 代码执行结果构造的请求,再模拟这个请求去抓取数据,而这个数据就是想要的课程表数据。
在校园网上抓取到的静态网页数据,就是该网页的 HTML 代码,要想获取到指定内容,还需要一个解析器,在 Python 中使用的是 BeautifulSoup 完成解析工作,而如果通过抓取动态网页中通过 javascript 动态生成的数据,得到的是 json 数据,在 Python 中使用内置的 json 库就能实现解析。
到此,爬虫的工作就算是完成了,最终将网页内容解析成有价值的数据,接下来就是搭建服务器,将 Python 代码部署到服务器上
Python 中服务器框架也有很多,这里选择的是 Flask,一款轻量级 Web 应用框架,用来搭建这个应用的后台是足够用了。选择云服务器的时候遇到一些问题,先后尝试过阿里云,新浪云,LeanCloud 云引擎,最终选择了 LeanCloud 云引擎,这三家服务器部署都还算简单,跟着教程走就能实现,这里选择 LeanCloud 云引擎的原因是考虑到移动端开发的便捷性,因为 LeanCloud 同时还提供一套移动应用开发 SDK,可以很方便的实现移动应用于后台通信
在测试接口的时候,有一些问题暴露了出来,比如云服务器访问校园网慢,服务器上要不要做数据缓存,具体的缓存策略是什么,测试的时候发现,一次接口调用的,从开始调用到返回数据,整个过程平均耗时10秒左右,而且期间偶尔会有连接超时的情况发生,这样的结果是很不满意的。于是尝试做服务器的缓存,大致的缓存策略是:用户首次登录时,会从校园网上抓取数据,返回给用户,并且保存到云服务器上,用户接下来的请求都直接从云服务器上获取,而服务器会每隔一个指定的时间去校园网抓取数据,判断有没有更新,有则自动更新到云服务器上。这个策略最终没有被采纳,原因是云服务器定时去校园网抓取数据这个功能无法实现,因为从校园网抓取数据,需要用户登录后返回的 Token 作为请求头才能获取到。而要实现定时抓取,就需要在 云服务器上存放用户的账号信息,这是不安全的,由于这是个人开发的项目,并没有经过学校同意,考虑到安全性,整个应用的流程最好都不要涉及到用户的校园网账号密码。具体的解决方案会在后面移动端开发的时候讲到。
因为要在服务器上做数据缓存,所以需要用到数据库,由于第一次做这个,在设计的时候难免会有遗漏某些字段或者考虑不够周全,没关系,后期再根据具体情况修改即可,初始设计的数据库中,用户表包含了用户登录信息(比如用户名,密码,当然密码是经过加密的)和用户资料,这样做的优点是逻辑编写方便,但是缺点是存在安全隐患(如果用户表可以查询用户资料,就需要公开查询权限,这样对于登录信息是不安全的)而且可扩展性低,所以后来修改用户表结构,将其分为两张表:登录信息表和用户信息表
登录信息表:存放用户登录信息(用户名,密码,学号,指向对应用户信息的指针)
用户信息表:存放用户的个人信息(学号,姓名,学院,学分绩点等)
这样一样,登录表的权限设为 Private,不允许查询,可以保证登录信息的安全。关于可扩展性,可以参阅这篇文章 ,其他的数据表设计就不多说,也就是简单的数据存储,没有什么难度
移动端的开发暂时先开发 Android 端,接着做 iOS 端的开发
在写代码之前,需要先确定项目架构和界面逻辑,UI 方面是遵循 Material Design 设计规范,很大程度上有模仿知乎 Android 端 3.0 版的界面,我还是很喜欢 Material Design 风格的,界面这块就暂时先这样,具体再修改也行的。关于项目架构,了解到 RxJava 最近讨论的比较多,于是了解了一下相关知识,顺带又了解到 Retrofit,Realm 等开源框架,于是选择尝试使用 RxJava + Retrofit + Realm + Picasso 这样一个开源框架组合来实现这个应用,项目结构中有一个问题倒是纠结了一下,就是:到底是使用一个 Activity 加上 多个 Fragment 来实现整个应用呢,还是多个 Activity 加多个 Fragment 实现,关于这个问题,知乎上有讨论,最终选择了后者。
在写 Android 端的时候,最先做的功能是登录,因为这个应用是给黑大学生使用的,并且数据都是从校园网上抓取,所以需要使用到学生的校园网账号,而 LeanCloud 的云服务器上有一套自己的用户体系,现在就需要将校园网账号与 LeanCloud 服务器上的账号进行绑定,这样,整个应用只有登录功能,没有注册功能,用户安装应用后,直接使用校园网账号就能登录。要实现这个用户体系的绑定,需要解决以下两个问题
什么时候在 LeanCloud 上注册,什么时候登录?
校园网用户修改密码后怎样修改 LeanCloud 用户的密码?
对于第一个问题的解决方案是:在应用中,用户执行登录操作,接着在服务器端用这个账号密码去登录校园网,如果登录成功,再执行注册 LeanCloud 用户,此时,如果 LeanCloud 上已经存在当前用户,会报错,这时,如果报错,就执行登录,接着更新用户信息,返回给客户端,如果登录校园网错误,则直接返回,提示用户名或密码错误。
对于第二个问题的解决方案是:如果登录校园网成功,而登录 LeanCloud 失败返回错误信息为用户名或密码错误,这就表明用户修改了校园网上的账号,此时就执行 LeanCloud 用户修改密码的操作。这样就能解决用户修改密码的问题,以上,这样就能完成校园网用户体系与LeanCloud 用户体系的绑定。
关于数据更新策略,前面有提过,由于 LeanCloud 服务器访问校园网慢,而且会有可能连接超时,导致不好的用户体验,这里对于数据更新的策略分为两个部分:
对于课程表和成绩表数据,考虑到数据改动会很小,一般就每学期改动一次,因此对于这种数据的更新,采用的是 Android 端进行数据缓存,用户在 Android 端登录成功后,后台下载 课程表和成绩表数据(可能会耗时比较长,而且会有超时重试),这样在用户想要查看课表的时候,数据可以直接显示而无需等待,而当数据有更新时,需要用户手动点击刷新,这个过程可能会比较耗时,但是只需要等待这一次就好,一旦数据更新了,以后很长时间内都无需再更新,所以目前采用这种方式来更新课程表和成绩表的数据。
对于考试安排表,它的特点是临近考试的那段时间(一般是一个月左右)才会有数据,因此,这里对考试安排表的数据更新策略是:Android 端会缓存,但是每次打开页面时会首先加载最新数据,如果没有最新数据或者加载失败,再加载缓存
黑大助手提供了一个类似知乎的问答社区,当然,只是一个最简单的实现,即:
用户可以提问题,提出的问题会在首页按照时间顺序倒序排列,所有其他用户通过刷新,就能在首页看到最新的问题,点击进入问题详情页面,可以回答,一旦发送回答,会有推送通知给提问者。目前的版本就是实现了这样一个简单的问答模块功能,后期可能会加上点赞,加关注之类的功能。
关于问答模块,主要涉及到的技术点就是 RecyclerView 加载数据和数据的及时更新,RecyclerView 加载数据这个就很好完成,以前有过相关经验的,实现起来很容易,比如下拉刷新,上拉加载更多,加载不同类型的视图等这些功能,在 RecyclerViewAdapter 中实现即可。而关于数据的即时更新,主要是两点:
一是提问发送完成或者回答完成返回时显示最新的提问或者回答,这个就是对Fragment 或者 Activity 生命周期的把握了,在返回到之前界面的时候,可以做一些数据更新操作,这样来显示最新的提问或者回答,而达到及时更新的目的
二是回答完成,对应的问题的回答数变量的更新,因为每个问题下都会显示这个问题的回答数,之前的方案是在客户端实现这个逻辑,即:回答完成,点击发送时,会查找对应的问题,然后更新其回答数这个字段的值,返回界面时再刷新问题详情。而现在的逻辑是将这些操作都放在后端去执行,也就是定义云函数,检测回答数这个数据表,一旦有新增数据(也就是新增了回答),就查找其对应的问题,更新回答数字段的值,这样,关于数据即时更新的两点问题就解决了。
考虑到移动端数据即时更新以及流量消耗的问题,需要在移动端做好数据缓存。Android 端使用的网络通信框架是 Retrofit,其底层实现是用的 OkHttp 这个库,自带有缓存功能,因此,对于一些即时更新的数据(比如首页问答数据,新闻资讯数据,回答列表数据)可以直接使用 OkHttp 的缓存功能来实现缓存,而缓存策略也可以根据不同的网络情况做出不同的选择。OkHttp 的缓存实现也很简单,通过拦截响应头,设置其 “Cache-Control” 值即可,这样,可以根据当前网络情况来设置这个值,关于 OkHttp 缓存的设置,可以参考这篇文章。对于用户个人资料的缓存,在 Android 端使用的是 SharedPreferences,因为用户个人资料只有一条数据,如果新建一个数据表来存放,有点浪费空间的感觉,因此决定将其存放在 SharedPreferences 中,读写也是很方便的,并且,判断当前用户是否已登录也是通过判断 SharedPreferences 是否有数据作为依据的。在登录界面,如果用户登录成功,会将用户资料保存在 SharedPreferences 中,然后以后每次启动应用,检查 SharedPreferences 中是否有数据,有则说明用户已登录,并且检查其中存放的 SessionToken 和 SchoolToken 是否有效(SessionToken 是 LeanCloud 登录返回的验证,SchoolToken 是登录校园网返回的验证,每次请求都需要在请求头中加上这两个验证数据来验证身份),否则是没有登录,在用户点击退出登录时,会清空 SharedPreferences 的数据。以上,这样一个逻辑就实现了判断用户是否登录以及验证登录是否过期。关于其他数据的缓存,比如 课程表,成绩表,考试安排表等,都是通过数据库做的缓存,Android 中用到的是 Realm,由于是第一次使用 Realm,不是很上手,在这块遇到过一些问题,比如跨线程访问,多个实例操作等,当然,通过仔细阅读官方文档和查看官方示例代码都是能够解决的
初次接触 RxJava,简单的学习了一下基本使用和相关思想,感觉差不多能上手了,结合 Retrofit,网络操作实现起来确实很棒,经过一些封装,整个应用的代码非常简洁漂亮,但是在结合 Relam 使用的时候,就并不是很好用了,因为 Realm 不允许跨线程通信,他有自己的异步操作实现