之前学了一些java web的编程,理解了web应用的原理后,就突然想到,可以用java模拟登录吉珠的教务系统,然后爬取里面的课表、成绩、个人信息等等数据,然后就可以写成一个简易的课表APP。
一、第三方工具
这里我用到了httpClient这个第三方包,相信很多人都认识这个包,不认识的话可以自行百度一下,httpClient和传统的URLConnection相比,更加强大,更加灵活,更加易用,我用的是httpcomponents-client-4.5.2,下载地址:http://hc.apache.org/downloads.cgi
二、思路描述
这里就拿获取课表的例子说一下,首先,要登录教务系统,就要获取登录的验证码,然后输入学号密码和验证码后,向教务网发起登录请求,接着要做的就是维持登录状态,登录状态是靠cookie里的sessionID这个参数维持的,只需要保存cookie就行了,在后续的所有提交的请求里,只要都带着cookie一起提交,就能保持在登录状态,保证获取正确的页面,在登录成功后,就在响应页面里找到查询个人课表的链接,然后再提交查询课表的请求,得到的响应就是个人课表的页面,这时就直接抓取课表部分的内容就行了。至于怎么准确抓取相应的数据,当然是用正则表达式了,不懂正则表达式的自行百度。
三、页面分析
按F12打开浏览器的开发者工具(我用的是火狐浏览器),然后访问学校的正方教务网,点击页面上的登录按钮后,开发者工具窗口中可以看到如下图片中的内容
可以看到点击登录按钮后浏览器提交了一个post请求,请求/default2.aspx这个页面,然后表单数据里有若干个参数,包括学号、密码、验证码等,这里面还有一个__VIEWSTATE参数,查看页面源码得知这是一个表单隐藏域,这个参数的值要每次都在相应页面中通过正则匹配抽取出来。如果登录成功的话,会重定向到页面/xs_main.aspx?xh=***,这里的"***"就是对应的学生学号。
接下来点击个人课表的链接,在开发者工具中可以看到请求课表页面的链接和对应参数,这些参数的值都可以在响应的HTML中匹配出来。
最后,在个人课表的页面HTML中,我们能看到所有的课程信息,利用正则匹配我们就能把每个课程的名称、上课时间、上课教室、任课老师等信息抽取出来,并封装成一个java对象。
四、代码实现
在这之前,我把一些固定的链接等字符串封装到一个常量类里,代码如下
/**
* 常量类
* @author EsauLu
*
*/
public class Constant {
/**
* 验证码为空
*/
public static final String CHECK_NULL_ERROR="验证码不能为空,如看不清请刷新!!";
/**
* 验证码不正确
*/
public static final String CHECK_ERROR="验证码不正确!!";
/**
* 密码错误
*/
public static final String PASSWD_ERROR="密码错误!!";
/**
* 字符编码
*/
public static final String ENCODING="GB2312";
/**
* 用户类型
*/
public static final String RADIO_BUTTON_LIST="学生";
/**
* 教务网地址
*/
public static final String BASE_URL="http://jw.jluzh.com";
/**
* 验证码URL
*/
public static final String CHECK_IMAGE_URL=BASE_URL+"/CheckCode.aspx";
/**
* 登陆URL
*/
public static final String LOGIN_URL=BASE_URL+"/default2.aspx";
/**
* 登陆后主页面URL
*/
public static final String STUDENT_URL=BASE_URL+"/xs_main.aspx?xh=";
}
首先,我定义了一个HttpInterface接口,定义了一些用于登录、获取验证码、获取课表等等的方法,详细请看代码
import org.apache.http.client.entity.UrlEncodedFormEntity;
import com.jluzh.jw.bean.CourseTable;
import com.jluzh.jw.bean.PersonalInfo;
import com.jluzh.jw.bean.User;
/**
* 获取数据的接口
* @author EsauLu
*
*/
public interface HttpInterface {
/**
* 初始化,主要用于收集cookie和viewState
*/
public void init();
/**
* 根据指定url发送给请求
* @param url 请求url
* @param ref 引用
* @return 响应页面的HTML文档
*/
public StringBuffer sendGetRequest(String url,String ref);
/**
* 根据指定url和参数值发送post请求
* @param url 请求url
* @param ref 引用
* @param entity 参数列表
* @return 响应页面的HTML文档
*/
public StringBuffer sendPostRequest(String url,String ref, UrlEncodedFormEntity entity);
/**
* 获取验证码
* @return 验证码图片
*/
public byte[] getCheckImg();
/**
* 登陆
* @param user 用户信息
* @return 返回登陆是否成功
*/
public boolean login(User user);
/**
* 根据学年度和学期获取课表
* @param xnd 学年度
* @param xqd 学期
* @return 课表
*/
public CourseTable getCourseTable(String xnd,String xqd);
/**
* 根据学年度和学期获取课表的Json串
* @param xnd 学年度
* @param xqd 学期
* @return 课表Json串
*/
public String getCourseTableAsJson(String xnd,String xqd);
/**
* 获取个人信息
* @param url 查询个人信息的url
* @return 个人信息
*/
public PersonalInfo getPersonalInfo(String url);
/**
* 获取错误信息
* @return 返回错误信息
*/
public String getErrorMessege();
}
然后下面就是接口的实现。需要说明一下的是,HttpService这个实现类做了维持登录的工作,就是把cookie保存起来。在初始化时,init()函数里首先访问一下教务网,然后记录cookie,cookie里有个参数ASP.NET_SessionId的参数,维持登录靠的就是这个参数。然后在获取验证码、登录、获取课表等的函数实现里,提交的请求都要设置cookie域,否则的话响应内容将是登录页面(这个知道web后端开发的都懂吧)。
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import com.jluzh.jw.bean.Course;
import com.jluzh.jw.bean.CourseTable;
import com.jluzh.jw.bean.PersonalInfo;
import com.jluzh.jw.bean.StuSimpleInfo;
import com.jluzh.jw.bean.User;
import com.jluzh.jw.constant.Constant;
import com.jluzh.jw.tool.HtmlTools;
public class HttpService implements HttpInterface {
/**
* Http客户端
*/
private CloseableHttpClient mHttpClient;
/**
* 记录cookie
*/
private String mCookie;
/**
* 记录正方教务系统页面表单的__VIEWSTATE的值
*/
private String mViewState;
/**
* 已登陆用户的信息
*/
private User mUser;
/**
* 登陆错误信息
*/
private String mErrorMessege;
/**
* 查询课程表信息的URL
*/
private String mCourseURL;
/**
* 查询个人信息的URL
*/
private String mPersonalInfoURL;
/**
* 查询成绩表的URL
*/
private String mScorceURL;
/**
* 构造函数
*/
public HttpService() {
// TODO Auto-generated constructor stub
this.mErrorMessege="no error";
mHttpClient=HttpClients.createDefault();
init();
}
@Override
public void init() {
// TODO Auto-generated method stub
String url=Constant.BASE_URL;
try {
HttpGet httpGet=new HttpGet(url);
CloseableHttpResponse response=mHttpClient.execute(httpGet);//提交请求获得响应
mCookie=response.getFirstHeader("Set-Cookie").getValue(); //获取cookie
StringBuffer sb=sendGetRequest(url,null); //发送访问请求并获得响应页面
mViewState=HtmlTools.findViewState(sb.toString()); //提取页面表单中的__VIEWSTATE的值
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public StringBuffer sendGetRequest(String url,String ref) {
// TODO Auto-generated method stub
StringBuffer sb=new StringBuffer();
InputStream in=null;
try {
HttpGet httpGet=new HttpGet(url);
httpGet.setHeader("Cookie", mCookie);//设置cookie
if(ref!=null&&!ref.equals("")){
httpGet.setHeader("Referer",ref);//如果有地址引用则设置
}
CloseableHttpResponse response=mHttpClient.execute(httpGet);//提交请求获得响应
in=response.getEntity().getContent();
//获取响应内容
if(in!=null){
int len=-1;
byte[] data=new byte[1024];
while((len=in.read(data))!=-1){
String s=new String(data,0,len,Constant.ENCODING);
sb.append(s);
}
}
} catch (Exception e) {
// TODO: handle exception
}finally{
try {
if(in!=null){
in.close();
}
} catch (Exception e2) {
// TODO: handle exception
}
}
return sb;
}
@Override
public StringBuffer sendPostRequest(String url, String ref, UrlEncodedFormEntity entity) {
// TODO Auto-generated method stub
StringBuffer sb=new StringBuffer();
HttpPost httpPost=new HttpPost(url);
InputStream in=null;
try {
httpPost.setHeader("Cookie", mCookie); //设置cookie
if(ref!=null&&!ref.equals("")){
httpPost.setHeader("Referer",ref);//如果有地址引用则设置
}
httpPost.setEntity(entity);//设置请求参数
CloseableHttpResponse response=mHttpClient.execute(httpPost);//提交请求
in=response.getEntity().getContent();//获得响应流对象
//获取响应内容
int len=-1;
byte[] data=new byte[1024];
while((len=in.read(data))!=-1){
String s=new String(data,0,len,Constant.ENCODING);
sb.append(s);
}
} catch (Exception e) {
// TODO: handle exception
}finally{
try {
if(in!=null){
in.close();
}
} catch (Exception e2) {
// TODO: handle exception
}
}
return sb;
}
@Override
public byte[] getCheckImg() {
// TODO Auto-generated method stub
String url=Constant.CHECK_IMAGE_URL;
HttpGet httpGet=new HttpGet(url);
byte[] imgByte=null;
try {
CloseableHttpResponse response=mHttpClient.execute(httpGet);
imgByte=EntityUtils.toByteArray(response.getEntity());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return imgByte;
}
@Override
public boolean login(User user) {
// TODO Auto-generated method stub
//组织登陆请求参数
ArrayList<BasicNameValuePair> params=new ArrayList<BasicNameValuePair>();
params.add(new BasicNameValuePair("__VIEWSTATE", mViewState));//__VIEWSTATE,不可缺少这个参数
params.add(new BasicNameValuePair("txtUserName", user.getName()));//学号
params.add(new BasicNameValuePair("TextBox2", user.getPasswd()));//密码
params.add(new BasicNameValuePair("txtSecretCode",user.getCheck()));//验证码
params.add(new BasicNameValuePair("RadioButtonList1", Constant.RADIO_BUTTON_LIST));//登陆用户类型
params.add(new BasicNameValuePair("Button1", ""));
params.add(new BasicNameValuePair("lbLanguage", ""));
params.add(new BasicNameValuePair("hidPdrs", ""));
params.add(new BasicNameValuePair("hidsc", ""));
try {
UrlEncodedFormEntity entity=new UrlEncodedFormEntity(params,Constant.ENCODING); //封装成参数对象
StringBuffer sb=sendPostRequest(Constant.LOGIN_URL,null, entity);//发送请求
mUser=user;//记录登陆的用户
String html=sb.toString();
//检测是否有登陆错误的信息,有则记录信息,否则登陆成功
if(html.contains(Constant.CHECK_ERROR)){
mErrorMessege=Constant.CHECK_ERROR;
}else if(html.contains(Constant.CHECK_NULL_ERROR)){
mErrorMessege=Constant.CHECK_NULL_ERROR;
}else if(html.contains(Constant.PASSWD_ERROR)){
mErrorMessege=Constant.PASSWD_ERROR;
}else{
//登陆成功,重定向获取主页面
StringBuffer userHtml=sendGetRequest(Constant.STUDENT_URL+user.getName(),null);
saveQueryURL(userHtml.toString()); //根据响应内容找到并保存查询各种信息的URL
return true;//返回登陆成功
}
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return false;
}
@Override
public CourseTable getCourseTable(String xnd,String xqd) {
// TODO Auto-generated method stub
String url=Constant.BASE_URL+mCourseURL;//查询课表的URL
String referer=Constant.STUDENT_URL+mUser.getName();//引用地址
StringBuffer courseHtml=null;
CourseTable couresTable=new CourseTable();
try {
//没有学年度和学期的的信息,则发送get请求,否则发送post请求
if(xnd==null||xqd==null){
courseHtml=sendGetRequest(url, referer);
}else{
//记录post参数后发送请求
ArrayList<BasicNameValuePair> params=new ArrayList<BasicNameValuePair>();
params.add(new BasicNameValuePair("__EVENTTARGET", "xqd"));
params.add(new BasicNameValuePair("__EVENTARGUMENT", ""));
params.add(new BasicNameValuePair("__VIEWSTATE", mViewState));
params.add(new BasicNameValuePair("xnd", xnd));
params.add(new BasicNameValuePair("xqd", xqd));
UrlEncodedFormEntity entity=new UrlEncodedFormEntity(params,Constant.ENCODING);
courseHtml=sendPostRequest(url, referer, entity);
}
String html=courseHtml.toString(); //响应HTML文档
mViewState=HtmlTools.findViewState(html);//记录__VIEWSTATE
StuSimpleInfo stuInfo=HtmlTools.getStuInfo(html);
String[] xnds=HtmlTools.getXnd(html); //在响应内容中获取学年度选项列表
String[] xqds=HtmlTools.getXqd(html); //在响应内容中获取学期选项列表
ArrayList<Course> courses=HtmlTools.getCourseList(html); //在响应内容中获取课表
//如果传进来的学年度和学期参数为空,则使用默认选项
if(xnd==null&&xnds.length>0) xnd=xnds[0];
if(xqd==null&&xqds.length>0) xqd=xqds[0];
//保存获取到的所有信息
couresTable.setXnd(xnds);
couresTable.setXqd(xqds);
couresTable.setCurrXnd(xnd);
couresTable.setCurrXqd(xqd);
couresTable.setCourses(courses);
couresTable.setSimpleInfo(stuInfo);
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return couresTable;
}
@Override
public String getCourseTableAsJson(String xnd, String xqd) {
// TODO Auto-generated method stub
CourseTable courseTable=getCourseTable(xnd, xqd);
StringBuffer sb=new StringBuffer();
sb.append("{");
for(Course c:courseTable.getCourses()){
sb.append("\n\t{");
sb.append("\n\t\t\"name\":\""+c.getName()+"\"");
sb.append("\n\t\t\"classRoom\":\""+c.getClassRoom()+"\"");
sb.append("\n\t\t\"teacher\":\""+c.getTeacher()+"\"");
sb.append("\n\t\t\"classTime\":\""+c.getClassTime()+"\"");
sb.append("\n\t\t\"weekNum\":\""+c.getWeekNum()+"\"");
sb.append("\n\t\t\"startWeek\":\""+c.getStartWeek()+"\"");
sb.append("\n\t\t\"endWeek\":\""+c.getEndWeek()+"\"");
sb.append("\n\t\t\"weekState\":\""+c.getWeekState()+"\"");
sb.append("\n\t\t\"number\":\""+c.getNumber()+"\"");
sb.append("\n\t\t\"day\":\""+c.getDay()+"\"");
sb.append("\n\t}\n");
}
sb.append("}");
return sb.toString();
}
@Override
public PersonalInfo getPersonalInfo(String url) {
// TODO Auto-generated method stub
return null;
}
@Override
public String getErrorMessege() {
// TODO Auto-generated method stub
return mErrorMessege;
}
/**
* 查找并保存查询各种信息的URL
* @param html HTML文档
*/
private void saveQueryURL(String html) {
// TODO Auto-generated method stub
String pattern="<a href=\"(\\w+)\\.aspx\\?xh=(\\d+)&xm=(.+?)&gnmkdm=N(\\d+)\" target='zhuti' οnclick=\"GetMc\\('(.+?)'\\);\">(.+?)</a>";
Pattern p=Pattern.compile(pattern);
Matcher m=p.matcher(html);
while(m.find()){
String res=m.group();
String url=res.substring(res.indexOf("href=\"")+6);
url=url.substring(0,url.indexOf("\""));
url="/"+url;
if(res.contains("学生个人课表")){
mCourseURL=url;
continue;
}
if(res.contains("成绩查询")){
mScorceURL=url;
continue;
}
if(res.contains("个人信息")){
mPersonalInfoURL=url;
}
}
}
public User getmUser() {
return mUser;
}
}
上面的HttpService里用到一个HtmlTools类,这个类里包含了一些了的静态方法,是专门用来解析响应页面的HTML里面的信息的,代码如下
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.jluzh.jw.bean.Course;
import com.jluzh.jw.bean.StuSimpleInfo;
import com.jluzh.jw.factor.BeanFactor;
/**
* HTML工具类,主要用于提取HTML文档中的信息
* @author EsauLu
*
*/
public class HtmlTools {
private static final int FIND_XND=1;//学年度
private static final int FIND_XQD=2;//学期
/**
* 查找__VIEWSTATE参数的值
* @param html HTML文档
* @return 返回
*/
public static String findViewState(String html) {
String res="";
String pattern="<input type=\"hidden\" name=\"__VIEWSTATE\" value=\"(.*?)\" />";
Pattern p=Pattern.compile(pattern);
Matcher m=p.matcher(html);
if(m.find()){
res=m.group();
res=res.substring(res.indexOf("value=\"")+7,res.lastIndexOf("\""));
}
return res;
}
/**
* 查找课表部分的HTML字符串
* @param html HTML文档
* @return 课表的HTML串
*/
private static String findCourseTableHtml(String html){
String res="";
String tar="<table id=\"Table1\" class=\"blacktab\" bordercolor=\"Black\" border=\"0\" width=\"100%\">";
String pattern="<table id=\"Table1\" class=\"blacktab\" bordercolor=\"Black\" border=\"0\" width=\"100%\">([\\S\\s]+?)</table>";
Pattern p=Pattern.compile(pattern);
Matcher m=p.matcher(html);
if(m.find()){
res=m.group(0);
res=res.substring(res.indexOf(tar)+tar.length(),res.lastIndexOf("</table>")).trim();
}else{
System.out.println(html);
System.out.println("什么都没有");
}
return res;
}
/**
* 在HTML中提取学年度或者学期的选项的HTML记录串
* @param html HTML文档
* @param x 代表学期或者学年度的参数
* @return 返回HTML表示的学年度或者学期选项
*/
private static String findXndOrXqdHtml(String html,int x){
String res="";
String pattern=null;
switch(x){
case FIND_XND:
pattern="<select name=\"xnd\" οnchange=\"__doPostBack\\('xnd',''\\)\" language=\"javascript\" id=\"xnd\">([\\s\\S]+?)</select>";
break;
case FIND_XQD:
pattern="<select name=\"xqd\" οnchange=\"__doPostBack\\('xqd',''\\)\" language=\"javascript\" id=\"xqd\">([\\s\\S]+?)</select>";
break;
}
Pattern p=Pattern.compile(pattern);
Matcher m=p.matcher(html);
if(m.find()){
res=m.group(1);
}
return res.trim();
}
/**
* 在HTML中提取学年度或者学期的选项列表
* @param html HTML文档
* @param x 代表学年度或者学期
* @return 返回学年度或者学期的选项数组
*/
private static String[] getOptions(String html,int x){
String[] ops=null;
String res="";
String tar=findXndOrXqdHtml(html, x);
ArrayList<String> arr=new ArrayList<String>();
String pattern="<option([\\s\\S]*?)>(.*?)</option>";
Pattern p=Pattern.compile(pattern);
Matcher m=p.matcher(tar);
while(m.find()){
res=m.group(2);
arr.add(res);
}
ops=new String[arr.size()];
for(int i=0;i<ops.length;i++){
ops[i]=arr.get(i);
}
return ops;
}
/**
* 获取学年度选项
* @param html HTML文档
* @return 返回学年度选项数组
*/
public static String[] getXnd(String html){
return getOptions(html, FIND_XND);
}
/**
* 获取学期选项数组
* @param html HTML文档
* @return 学期选项数组
*/
public static String[] getXqd(String html){
return getOptions(html, FIND_XQD);
}
/**
* 获取学生简要信息
* @param html HTML文档
* @return 返回学生简要信息数组
*/
public static StuSimpleInfo getStuInfo(String html){
String[] info=null;
//(<span id="([\\s\\S]+?)">([\\s\\S]+?)</span>\\|)?<span id="(.+)">(.+?)</span>
String res="";
String pattern="<TR class=\"trbg1\">"
+ "([\\s\\S]*?)<TD>([\\s\\S]*?)"
+ "<span id=\"Labe(.+?)\">([\\s\\S]+?)</span>(\\|([\\s\\S]*?)<span id=\"Labe(.+?)\">([\\s\\S]+?)</span>)*";
Pattern p=Pattern.compile(pattern);
Matcher m=p.matcher(html);
if(m.find()){
res=m.group(0);
info=res.split("\\|");
pattern="<span id=\"Labe(.+?)\">([\\s\\S]+?):([\\s\\S]+?)</span>";
p=Pattern.compile(pattern);
for(int i=0;i<info.length;i++){
m=p.matcher(info[i]);
if(m.find()){
info[i]=m.group(3);
}else{
info[i]="";
}
}
}
return BeanFactor.createStuSimpleInfo(info);
}
/**
* 在HTML文档中提取课表
* @param html HTML文档
* @return 返回课表
*/
public static ArrayList<Course> getCourseList(String html){
String courseTableHtml=findCourseTableHtml(html);//找到课表部分的HTML
ArrayList<Course> courses=new ArrayList<Course>();
String[] rows=courseTableHtml.split("</tr><tr>");//按上课时间分隔HTML
for(int i=2;i<rows.length;i+=2){
String r=rows[i];
String[] cols=r.split("</td><td([\\S\\s]*?)>");//按星期几分隔HTML
int j=1;
if(i==2||i==6||i==10){
j=2;
}
int x=1;
for(;j<cols.length;x++,j++){
String c=cols[j];
String[] info=c.split("<br>");
if(info[0].contains(" ")) continue;
String[] tem=new String[4];
int t=0;
for(int k=0;k<info.length;k++){
String item=info[k].trim();
if(item.equals("")){
//处理同一时间不同周数的课程
t=0;
tem=new String[4];
continue;
}
tem[t++]=item;
if(t==4){
Course course=BeanFactor.createCourse(tem, i-1, x);
courses.add(course);
}
}
}
}
return courses;
}
}
然后还有一些封装类的代码就不贴上来了,后面有完整代码的链接
五、总结
上面的HttpService类已经为实现了访问教务网的所有操作,利用这个HttpService,我们就能得到课程表的信息,然后就可以写一个界面将这些课程信息展示出来,我自己写了一个简单的界面,并放到了GitHub上,有兴趣的读者可以去看一下,地址是
https://github.com/EsauLu/JluzhJW。
另外,我还依照这些思路写了一个安卓的课表APP,由于安卓的一些特性,写这个APP时并没有采用HttpClient这个包而是用了另一个包OkHttpClient,这个课表APP的源码地址: https://github.com/EsauLu/CourseTable
另外,我还依照这些思路写了一个安卓的课表APP,由于安卓的一些特性,写这个APP时并没有采用HttpClient这个包而是用了另一个包OkHttpClient,这个课表APP的源码地址: https://github.com/EsauLu/CourseTable