前言
时光飞逝,日月如梭,转眼间四年的大学生活已经结束啦!开始了程序员的加班生活。我的第二学位的毕业设计是实现一个学习小助手。这其中最重要的环节就是模拟登录学校的教务系统,获取到教务系统的数据并解析,用自己的数据库存储,展示在自己设计的界面上。例如课程表我是仿照超级课程表的界面来设计的。废话不多说下面先看看效果。
抓取教务系统登录时需要传递的参数
模拟登录之前我们首先需要抓取我们登录时所需要传递的数据,可能有人会想用专业的抓包工具来抓包,其实不用那么复杂,用火狐浏览器或者Chrom浏览器的开发者模式就可以了。博主用的是火狐浏览器抓包,传的一些参数也是按照火狐浏览器来的。
首先,我们来看一下模拟器登录时需要传递的参数及HTTP请求头,我们可以看到请求是post请求,请求头字段基本差不多
传递的参数有这么些
其中这些参数中lt这个是搞的我最头疼的。本以为lt是一个固定值,但是模拟登录的时候发现不对,每次抓包lt是不一样的。最后,只能通过学习JS代码,研究这个lt的取值,最后发现lt及其他参数都隐藏在返回的HTML页面的代码中:(如下图)
搞清楚需要传什么参数和请求的HTTP头了,我们就可以模拟登录教务系统了。
自定义HTTPClient实现模拟登录
实现模拟登录首先要自定义HTTPClient,来模拟浏览器发送HTTP请求。自定义HTTP如下代码:
/**
* Http请求工具类
* @author leiqi
* @date 2017-3-4
*/
public class HttpUtil {
private static AsyncHttpClient client = new AsyncHttpClient(); // 实例话对象
// Host地址
public static final String HOST = "cas.hdu.edu.cn";
// 基础地址
public static final String URL_BASE = "http://i.hdu.edu.cn/dcp/xphone/";
// 验证码地址
public static final String URL_CODE = "http://cas.hdu.edu.cn/cas/Captcha.jpg";
// 登陆地址
public static final String URL_LOGIN = "http://cas.hdu.edu.cn/cas/login?service=http%3a%2f%2fi.hdu.edu.cn%2fdcp%2fxphone%2fm.jsp";
// 登录成功的首页
public static String URL_MAIN = "http://i.hdu.edu.cn/dcp/xphone/m.jsp";
//当前学期课表
public static String URL_Course= "http://i.hdu.edu.cn/dcp/xphone/kbcx0.jsp";
//考试安排url
public static String URl_KSAP = "http://i.hdu.edu.cn/dcp/xphone/ksap.jsp";
//校园新闻
public static String Url_XYXW = "http://m.hdu.edu.cn/";
//上课时间
public static String Url_Time = URL_BASE+"TimeTable.jsp";
//我的学费
public static String Url_WDXF = "http://yxt.hdu.edu.cn/EducationManager/xphone/m.jsp";
/**
* 请求参数
*/
public static String captcha = "";//验证码
public static String encodedService = "http%3a%2f%2fi.hdu.edu.cn%2fdcp%2fxphone%2fm.jsp";
public static String loginErrCnt = "0";
public static String lt = "LT-1552500-7P9jMewfE3wH2dWObBuJ";
public static String password = "19191cf09b99b03ab0df1db04c3840ed";
public static String username = null;
public static String service = "http://i.hdu.edu.cn/dcp/xphone/m.jsp";
public static String serviceName = null;
public static String ticket = null;
public static String Cookie = "key_dcp_cas=HHxRYyDMhr0GmDfQ6vgPxXQT8yCsvPSCg0MWBnnnWMGv4dQngpLS!748587538; route=4376efc7edf61c9fe699e82a2fb7a34f" ;
// 静态初始化
static {
client.setTimeout(10000); // 设置链接超时,如果不设置,默认为10s
// 设置请求头
client.addHeader("Accept","text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
client.addHeader("Accept-Encoding","gzip, deflate");
client.addHeader("Accept-Language","zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3");
client.addHeader("Connection","keep-alive");
client.addHeader("Cookie", Cookie);
client.addHeader("Host", HOST);
client.addHeader("Referer", URL_LOGIN);
client.addHeader("User-Agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:52.0) Gecko/20100101 Firefox/52.0");
client.addHeader("Upgrade-Insecure-Requests","1");
}
/**
* get,用一个完整url获取一个string对象
*
* @param urlString
* @param res
*/
public static void get(String urlString, AsyncHttpResponseHandler res) {
client.get(urlString, res);
}
/**
* get,url里面带参数
* @param urlString
* @param params
* @param res
*/
public static void get(String urlString, RequestParams params,
AsyncHttpResponseHandler res) {
client.get(urlString, params, res);
}
/**
* get,不带参数,获取json对象或者数组
*
* @param urlString
* @param res
*/
public static void get(String urlString, JsonHttpResponseHandler res) {
client.get(urlString, res);
}
/**
* get,带参数,获取json对象或者数组
*
* @param urlString
* @param params
* @param res
*/
public static void get(String urlString, RequestParams params,
JsonHttpResponseHandler res) {
client.get(urlString, params, res);
}
/**
* get,下载数据使用,会返回byte数据
*
* @param uString
* @param bHandler
*/
public static void get(String uString, BinaryHttpResponseHandler bHandler) {
client.get(uString, bHandler);
}
/**
* post,不带参数
*
* @param urlString
* @param res
*/
public static void post(String urlString, AsyncHttpResponseHandler res) {
client.post(urlString, res);
}
/**
* post,带参数
*
* @param urlString
* @param params
* @param res
*/
public static void post(String urlString, RequestParams params,
AsyncHttpResponseHandler res) {
client.post(urlString, params, res);
}
/**
* post,不带参数,获取json对象或者数组
*
* @param urlString
* @param res
*/
public static void post(String urlString, JsonHttpResponseHandler res) {
client.post(urlString, res);
}
/**
* post,带参数,获取json对象或者数组
*
* @param urlString
* @param params
* @param res
*/
public static void post(String urlString, RequestParams params,
JsonHttpResponseHandler res) {
client.post(urlString, params, res);
}
/**
* post,返回二进制数据时使用,会返回byte数据
*
* @param uString
* @param bHandler
*/
public static void post(String uString, BinaryHttpResponseHandler bHandler) {
client.post(uString, bHandler);
}
/**
* 返回请求客户端
*
* @return
*/
public static AsyncHttpClient getClient() {
return client;
}
/**
* 获得登录时所需的请求参数
*
* @return
*/
public static RequestParams getLoginRequestParams() {
// 设置请求参数
RequestParams params = new RequestParams();
params.add("captcha", captcha);
params.add("encodedService", encodedService);
params.add("loginErrCnt", loginErrCnt);
params.add("lt", lt);
params.add("password", password);
params.add("username", username);
params.add("service", service);
params.add("serviceName", serviceName);
return params;
}
/**
* 获取请求首页参数
* @return
*/
public static RequestParams gethomeRequestParams(){
RequestParams params = new RequestParams();
params.add("ticket",ticket);
return params;
}
}
定义完HTTP客户端,我们就需要去模拟登录,当然模拟登录必须要设置CookieStore如下所示:
/**
* 初始化Cookie
*/
private void initCookie(Context context) {
//必须在请求前初始化
cookie = new PersistentCookieStore(context);
HttpUtil.getClient().setCookieStore(cookie);
httpClient.setCookieStore(cookie);
}
然后在请求登录页面时候就可以将返回的cookie设置给HTTPClient了。
难到这样就可以了?那那个lt值怎么获取传递过去昵?先贴一段获取lt的代码后面做解释:
/**
* 获取lt
*/
private void getlt(){
Document doc = null;
doc = Jsoup.parse(GetShouye());
if (doc != null)
if (doc != null) {
Elements login = doc.body().getElementsByClass("login_form");
Document containerDoc = Jsoup.parse(login.toString());
Elements ddd = containerDoc.getElementsByTag("input");
for (Element aaa : ddd) {
if (aaa.toString().contains("lt")) {
String l = aaa.toString();
String lt = l.substring(38, l.length() - 4);
HttpUtil.lt = lt;
}
}
}
}
/**
* 请求首页
* @return
*/
public String GetShouye()
{
String result= "";
// 创建HttpGet或HttpPost对象,将要请求的URL通过构造方法传入HttpGet或HttpPost对象。
if (urlll == null){
urlll = HttpUtil.URL_MAIN;
}
HttpGet httpRequst = new HttpGet(urlll);
// new DefaultHttpClient().execute(HttpUriRequst requst);
try {
//使用DefaultHttpClient类的execute方法发送HTTP GET请求,并返回HttpResponse对象。
HttpResponse httpResponse = httpClient.execute(httpRequst);//其中HttpGet是HttpUriRequst的子类
if (httpResponse.getStatusLine().getStatusCode() == 200) {
HttpEntity httpEntity = httpResponse.getEntity();
result = EntityUtils.toString(httpEntity);//取出应答字符串
// 一般来说都要删除多余的字符
result.replaceAll("\r", "");//去掉返回结果中的"\r"字符,否则会在结果字符串后面显示一个小方格
} else{
httpRequst.abort();
}
}catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
result = e.getMessage().toString();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
result = e.getMessage().toString();
}
return result;
}
所有准备工作做好我们就可以发送登录请求了
/**
* 登录
*/
private void login() {
HttpUtil.username = username.getText().toString().trim();
String md5 = "@";
try {
md5= MD5Until.GetMD5Code(password.getText().toString().trim());
} catch (Exception e) {
e.printStackTrace();
}
Log.d("md5 ",md5);
HttpUtil.password = md5;
//需要时打开验证码注释
HttpUtil.captcha = secrectCode.getText().toString().trim();
Log.d("cookie",HttpUtil.Cookie);
if (TextUtils.isEmpty(HttpUtil.username)
|| TextUtils.isEmpty(HttpUtil.password)) {
Toast.makeText(getApplicationContext(), "账号或者密码不能为空!",
Toast.LENGTH_SHORT).show();
return;
}
final ProgressDialog dialog =CommonUtil.getProcessDialog(BindActivity.this,"正在登录中!!!");
dialog.show();
RequestParams params = HttpUtil.getLoginRequestParams();// 获得请求参数
Log.d("http",params.toString());
HttpUtil.getClient().setURLEncodingEnabled(true);
HttpUtil.post(HttpUtil.URL_LOGIN, params,
new AsyncHttpResponseHandler() {
@Override
public void onSuccess(int arg0, Header[] arg1, byte[] arg2) {
try {
String resultContent = new String(arg2, "gb2312");
Log.d("Header",arg1.toString());
Log.d("resunt",resultContent);
// List<String> list = Getlt.match(resultContent,"input","name=\"lt\" ");
// Log.d("list",list.toString());
if(linkService.isLogin(resultContent)!=null){
String ret = linkService.parseMenu(resultContent);
Log.d("cas", "login success:"+ret);
GetHerfUrl(resultContent);
Toast.makeText(getApplicationContext(),
"登录成功!!!", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
String s = GetShouye();
Log.d("sdfsdaf",s);
String ss = Getksap();
Log.d("qqqqqq",ss);
jump2Main();
}
}).start();
}else{
getCode();
Toast.makeText(getApplicationContext(),"账号或者密码错误!!!", Toast.LENGTH_SHORT).show();
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} finally {
dialog.dismiss();
}
}
@Override
public void onFailure(int arg0, Header[] arg1, byte[] arg2,
Throwable arg3) {
Toast.makeText(getApplicationContext(), "登录失败!!!!",
Toast.LENGTH_SHORT).show();
dialog.dismiss();
}
});
}
登录成功后会返回一个链接,并不是登录后的教务首页,我们还需要解析提取链接并用GET请求去请求,即可!下面我们就来说一下关于HTML网页数据的解析。
JSOUP对HTML网页数据的解析
JSOUP是一个非常好用的,用JAVA来抓取网页数据的开源框架。使用起来也非常简单,就是根据HTML内容,先获取BODY====》》》提取字段对应的class====》》》对应的标签ID。我们还是以lt为例来看:
/**
* 获取lt
*/
private void getlt(){
Document doc = null;
doc = Jsoup.parse(GetShouye());
if (doc != null)
if (doc != null) {
//获取解析后的body及对应的class——login_form
Elements login = doc.body().getElementsByClass("login_form");
Document containerDoc = Jsoup.parse(login.toString());
//获取到标签id对应的数据
Elements ddd = containerDoc.getElementsByTag("input");
for (Element aaa : ddd) {
//从这些数据中提取出lt
if (aaa.toString().contains("lt")) {
String l = aaa.toString();
String lt = l.substring(38, l.length() - 4);
HttpUtil.lt = lt;
}
}
}
}
后面所有的数据获取都是这样,不过后面都是解析table而已。下面觉得重点应该说一下对课程信息的存储。
课程表信息的存储与读取
课程表信息的存储也是非常麻烦的一件事,当时没想明白,存储完之后发现读到的数据全乱了。
{\"Course_address\":\"第12教研楼405\",\"Course_name\":\"网站规划与设计\",
\"Course_teacher\":\"李君君\",\"Course_type\":\"必修\",\"Course_week\":\"周一第3,4节{第1-16周}\"},
上面是用JSON数组来存储每节课信息的存储结构,绘制课程表时,根据课表中的字段。如下所示:
public class CourseActivity extends Activity {
private ArrayList<CourseBean> mCourse;
private SharedPreferences mShaerPreferences;
// 每天有多少节课
private int mMaxCouese;
// 一共有多少周
private int mMaxWeek;
// 现在是第几周
private int mNowWeek;
// 左边一节课的高度
private float mLeftHeight;
// 左边一节课的宽度
private float mLeftWidth;
private TextView mChangeWeek;
private LinearLayout mLeftNo;
private LinearLayout mMonday;
private LinearLayout mTuesday;
private LinearLayout mWednesday;
private LinearLayout mThursday;
private LinearLayout mFirday;
private LinearLayout mSaturday;
private LinearLayout mWeekend;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_course);
// 实例化所有对象
initCtrl();
// 初始化数据
initData();
// 绘制左边的课程节数
drawLeftNo();
// 绘制当前周
drawNowWeek();
// 绘制所有课程 其实可以使用redrawAll替代三步
drawAllCourse();
}
/**
* 实例化所有对象
*/
private void initCtrl() {
mChangeWeek = (TextView) findViewById(R.id.changeWeek);
mLeftNo = (LinearLayout) findViewById(R.id.leftNo);
mMonday = (LinearLayout) findViewById(R.id.monday);
mTuesday = (LinearLayout) findViewById(R.id.tuesday);
mWednesday = (LinearLayout) findViewById(R.id.wednesday);
mThursday = (LinearLayout) findViewById(R.id.thursday);
mFirday = (LinearLayout) findViewById(R.id.firday);
mSaturday = (LinearLayout) findViewById(R.id.saturday);
mWeekend = (LinearLayout) findViewById(R.id.weekend);
}
/**
* 初始化所有数据
*/
private void initData() {
// 初始化课表
praseJson();
// 读取配置信息
readIniFile();
// 点击选择切换周
mChangeWeek.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
showChangeWeekDlg(v);
}
});
}
/**
* 绘制左边的课程节数
*/
private void drawLeftNo() {
mLeftHeight = getResources().getDimension(R.dimen.left_height);
mLeftWidth = getResources().getDimension(R.dimen.left_width);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
(int) mLeftWidth, (int) mLeftHeight);
for (int i = 1; i <= mMaxCouese; i++) {
TextView tv = new TextView(this);
tv.setText(i + "");
tv.setGravity(Gravity.CENTER);
tv.setTextColor(getResources().getColor(R.color.font));
tv.setBackgroundResource(R.drawable.boder);
mLeftNo.addView(tv, lp);
}
}
/**
* 绘制课表
*
* @param ll
* 绘制课表到哪一个LinearLayout上
* @param dayOfWeek
* 绘制的数据来自周几 一二三四五六七
*/
private void drawCourse(LinearLayout ll, char dayOfWeek) {
// 删除所有子View
ll.removeAllViews();
// 上一节课结束是第几节
int perCourse = -1;
for (CourseBean course : mCourse) {
// 判断是否显示这节课
// 是不是同一天 是不是这一周
if (course.getDayOfWeek() != dayOfWeek
|| !course.inThisWeek(mNowWeek))
continue;
// 设置TextView的属性样式
TextView tv = new TextView(this);
tv.setText(course.getCourse_name() + "\n@"
+ course.getCourse_address());
tv.setBackgroundResource(R.drawable.course);
tv.setTextColor(getResources().getColor(R.color.course_font_color));
tv.setTextSize(12);
// 将数据绑定到TextView上
tv.setTag(course);
tv.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
CourseBean tag = (CourseBean) v.getTag();
showCouseDetails(tag);
}
});
// 设置TextView的位置
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT,
(int) (course.getStep() * mLeftHeight));
// 说明这节课为第一节课
if (perCourse == -1) {
lp.setMargins(1,
(int) ((course.getMinCourse() - 1) * mLeftHeight), 1, 0);
// useHeight = (int) ((course.getMaxCourse()-1) * mLeftHeight);
} else {
lp.setMargins(1, (course.getMinCourse() - perCourse - 1)
* (int) mLeftHeight, 1, 0);
// useHeight = useHeight + (course.getMaxCourse() - perCourse -
// 1)* (int) mLeftHeight;
}
perCourse = course.getMaxCourse();
ll.addView(tv, lp);
}
}
/**
* 绘制当前周
*/
private void drawNowWeek() {
mChangeWeek.setText("第" + mNowWeek + "周");
}
/**
* 重新绘制所有,不包括标题栏和星期几 在修改每天的节数后调用
*/
/*private void redrawAll() {
drawLeftNo();
drawNowWeek();
drawAllCourse();
}*/
/**
* 绘制课程,用于周数切换以后
*/
private void drawAllCourse() {
drawCourse(mMonday, '一');
drawCourse(mTuesday, '二');
drawCourse(mWednesday, '三');
drawCourse(mThursday, '四');
drawCourse(mFirday, '五');
drawCourse(mSaturday, '六');
drawCourse(mWeekend, '日');
}
/**
* 读取配置信息
*/
private void readIniFile() {
mShaerPreferences = getSharedPreferences("iniFile",
Context.MODE_PRIVATE);
mMaxCouese = mShaerPreferences.getInt("mMaxCouese", -1);
mMaxWeek = mShaerPreferences.getInt("mMaxWeek", -1);
mNowWeek = mShaerPreferences.getInt("mNowWeek", -1);
Editor edit = mShaerPreferences.edit();
// 默认12节课
if (mMaxCouese == -1) {
edit.putInt("mMaxCouese", 12);
}
// 默认20周
if (mMaxWeek == -1) {
edit.putInt("mMaxWeek", 17);
}
// 默认第一周
if (mNowWeek == -1) {
edit.putInt("mNowWeek", 15);
}
edit.commit();
}
/**
* 解析来自strings.xml里面的Json课表数据
*/
private void praseJson() {
String json = getResources().getString(R.string.kb);
Gson gson = new Gson();
mCourse = gson.fromJson(json, new TypeToken<ArrayList<CourseBean>>() {
}.getType());
}
/**
* 弹出窗口,显示课程详细信息
*
*/
public void showCouseDetails(CourseBean bean) {
AlertDialog.Builder builder = new Builder(this);
AlertDialog dialog = builder.create();
dialog.show();
dialog.setContentView(R.layout.details_layout);
TextView textView = (TextView) dialog.findViewById(R.id.name);
textView.setText(bean.getCourse_name());
textView = (TextView) dialog.findViewById(R.id.type);
textView.setVisibility(View.GONE);
textView.setText(bean.getCourse_type());
textView = (TextView) dialog.findViewById(R.id.teacher);
textView.setText(bean.getCourse_teacher());
textView = (TextView) dialog.findViewById(R.id.address);
textView.setText(bean.getCourse_address());
textView = (TextView) dialog.findViewById(R.id.week);
textView.setText(bean.getCourse_week());
}
/**
* 显示切换当前周的窗口
*/
public void showChangeWeekDlg(View v) {
View view = View.inflate(this, R.layout.changweek_layout, null);
ListView weekList = (ListView) view.findViewById(R.id.weekList);
ArrayList<String> strList = new ArrayList<String>();
for (int i = 1; i < mMaxWeek; i++) {
strList.add("第" + i + "周");
}
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
R.layout.item, strList);
weekList.setAdapter(adapter);
view.measure(0, 0);
final PopupWindow pop = new PopupWindow(view, 300, 500, true);
pop.setBackgroundDrawable(new ColorDrawable(0x00000000));
int xOffSet = -(pop.getWidth() - v.getWidth()) / 2;
pop.showAsDropDown(v, xOffSet, 0);
weekList.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapter, View view,
int positon, long id) {
mNowWeek = positon + 1;
pop.dismiss();
drawNowWeek();
drawAllCourse();
}
});
}
}
项目地址:https://github.com/Terrybthvi/HduStudyHelper