课表是在一张table里的,提取table里的内容进行解析,解析方法不止一种,我在解析过程中也尝试了多种方法,直接看代码把
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
|
/**
* 根据网页返回结果解析课程并保存
*
* @param content
* @return
*/
public
String parseCourse(String content) {
StringBuilder result =
new
StringBuilder();
Document doc = Jsoup.parse(content);
Elements semesters = doc.select(option[selected=selected]);
String[] years=semesters.get(
0
).text().split(-);
int
startYear=Integer.parseInt(years[
0
]);
int
endYear=Integer.parseInt(years[
1
]);
int
semester=Integer.parseInt(semesters.get(
1
).text());
Elements elements = doc.select(table#Table1);
Element element = elements.get(
0
).child(
0
);
//移除一些无用数据
element.child(
0
).remove();
element.child(
0
).remove();
element.child(
0
).child(
0
).remove();
element.child(
4
).child(
0
).remove();
element.child(
8
).child(
0
).remove();
int
rowNum = element.childNodeSize();
int
[][] map =
new
int
[
11
][
7
];
for
(
int
i =
0
; i < rowNum -
1
; i++) {
Element row = element.child(i);
int
columnNum = row.childNodeSize() -
2
;
for
(
int
j =
1
; j < columnNum; j++) {
Element column = row.child(j);
int
week = fillMap(column, map, i);
//填充map,获取周几,第几节至第几节
//作用:弥补不能获取这些数据的格式
if
(column.hasAttr(rowspan)) {
try
{
System.out.println(周+ week+ 第+ (i +
1
)+ 节-第+ (i + Integer.parseInt(column.attr(rowspan))) + 节);
splitCourse(column.html(), startYear,endYear,semester,week, i +
1
,i + Integer.parseInt(column.attr(rowspan)));
}
catch
(NumberFormatException e) {
e.printStackTrace();
}
}
}
}
return
result.toString();
}
/**
* 根据传进来的课程格式转换为对应的实体类并保存
* @param sub
* @param startYear
* @param endYear
* @param semester
* @param week
* @param startSection
* @param endSection
* @return
*/
private
Course storeCourseByResult(String sub,
int
startYear,
int
endYear,
int
semester,
int
week,
int
startSection,
int
endSection) {
// 周二第1,2节{第4-16周} 二,1,2,4,16,null
// {第2-10周|3节/周} null,null,null,2,10,3节/周
// 周二第1,2节{第4-16周|双周} 二,1,2,4,16,双周
// 周二第1节{第4-16周} 二,1,null,4,16,null
// 周二第1节{第4-16周|双周} 二,1,null,4,16,双周
// str格式如上,这里只是简单考虑每个课都只有两节课,实际上有三节和四节,模式就要改动,其他匹配模式请自行修改
String reg = 周?(.)?第?(\d{
1
,
2
})?,?(\d{
1
,
2
})?节?\{第(\d{
1
,
2
})-(\d{
1
,
2
})周\|?((.*周))?\};
String splitPattern =
;
String[] temp = sub.split(splitPattern);
Pattern pattern = Pattern.compile(reg);
Matcher matcher = pattern.matcher(temp[
1
]);
matcher.matches();
Course course =
new
Course();
//课程开始学年
course.setStartYear(startYear);
//课程结束学年
course.setEndYear(endYear);
//课程学期
course.setSemester(semester);
//课程名
course.setCourseName(temp[
0
]);
//课程时间,冗余字段
course.setCourseTime(temp[
1
]);
//教师
course.setTeacher(temp[
2
]);
try
{
// 数组可能越界,即没有教室
course.setClasssroom(temp[
3
]);
}
catch
(ArrayIndexOutOfBoundsException e) {
course.setClasssroom(无教室);
}
//周几,可能为空,此时使用传进来的值
if
(
null
!= matcher.group(
1
)){
course.setDayOfWeek(getDayOfWeek(matcher.group(
1
)));
}
else
{
course.setDayOfWeek(getDayOfWeek(week+));
}
//课程开始节数,可能为空,此时使用传进来的值
if
(
null
!= matcher.group(
2
)){
course.setStartSection(Integer.parseInt(matcher.group(
2
)));
}
else
{
course.setStartSection(startSection);
}
//课程结束时的节数,可能为空,此时使用传进来的值
if
(
null
!= matcher.group(
3
)){
course.setEndSection(Integer.parseInt(matcher.group(
3
)));
}
else
{
course.setEndSection(endSection);
}
//起始周
course.setStartWeek(Integer.parseInt(matcher.group(
4
)));
//结束周
course.setEndWeek(Integer.parseInt(matcher.group(
5
)));
//单双周
String t = matcher.group(
6
);
setEveryWeekByChinese(t, course);
save(course);
return
course;
}
/**
* 提取课程格式,可能包含多节课
* @param str
* @param startYear
* @param endYear
* @param semester
* @param week
* @param startSection
* @param endSection
* @return
*/
private
int
splitCourse(String str,
int
startYear,
int
endYear,
int
semester,
int
week,
int
startSection,
int
endSection) {
String pattern =
;
String[] split = str.split(pattern);
if
(split.length >
1
) {
// 如果大于一节课
for
(
int
i =
0
; i < split.length; i++) {
if
(!(split[i].startsWith(
) && split[i].endsWith(
))) {
storeCourseByResult(split[i], startYear,endYear,semester,week, startSection,
endSection);
// 保存单节课
}
else
{
//
文化地理(网络课程)
周日第
10
节{第
17
-
17
周}
李宏伟
// 以上格式的特殊处理,此种格式在没有教师的情况下产生,即教室留空后
依旧存在
int
brLength =
.length();
String substring = split[i].substring(brLength,
split[i].length() - brLength);
storeCourseByResult(substring, startYear,endYear,semester,week, startSection,
endSection);
// 保存单节课
}
}
return
split.length;
}
else
{
storeCourseByResult(str, startYear,endYear,semester,week, startSection, endSection);
// 保存
return
1
;
}
}
/**
* 填充map,获取周几,第几节课至第几节课
* @param childColumn
* @param map
* @param i
* @return 周几
*/
public
static
int
fillMap(Element childColumn,
int
map[][],
int
i) {
//这个函数的作用自行领悟,总之就是返回周几,也是无意中发现的,于是就这样获取了,作用是双重保障,因为有些课事无法根据正则匹配出周几第几节到第几节
boolean
hasAttr = childColumn.hasAttr(rowspan);
int
week =
0
;
if
(hasAttr) {
for
(
int
t =
0
; t < map[
0
].length; t++) {
if
(map[i][t] ==
0
) {
int
r = Integer.parseInt(childColumn.attr(rowspan));
for
(
int
l =
0
; l < r; l++) {
map[i + l][t] =
1
;
}
week = t +
1
;
break
;
}
}
}
else
{
if
(childColumn.childNodes().size() >
1
) {
childColumn.attr(rowspan,
1
);
}
for
(
int
t =
0
; t < map[
0
].length; t++) {
if
(map[i][t] ==
0
) {
map[i][t] =
1
;
week = t +
1
;
break
;
}
}
}
return
week;
}
/**
* 设置单双周
* @param week
* @param course
*/
public
void
setEveryWeekByChinese(String week, Course course) {
// 1代表单周,2代表双周
if
(week !=
null
) {
if
(week.equals(单周))
course.setEveryWeek(
1
);
else
if
(week.equals(双周))
course.setEveryWeek(
2
);
}
// 默认值为0,代表每周
}
/**根据中文数字一,二,三,四,五,六,日,转换为对应的阿拉伯数字
* @param day
* @return int
*/
public
int
getDayOfWeek(String day) {
if
(day.equals(一))
return
1
;
else
if
(day.equals(二))
return
2
;
else
if
(day.equals(三))
return
3
;
else
if
(day.equals(四))
return
4
;
else
if
(day.equals(五))
return
5
;
else
if
(day.equals(六))
return
6
;
else
if
(day.equals(日))
return
7
;
else
return
0
;
}
|
以上代码是提取课程的关键代码,课程的格式是在一个table里,tr里有很多td,td里就是课程,一个td里可能不止一节课
td有rowspan属性,代表占了几行,2代表占了两行,也就是两节课,有些课不是两节课的,rowspan的值也就对应改变,课程的节数有1,2,3,4节都有。我们可以根据课程的时间提取该课程是周几上课,第几节课到第几节课,有了这些信息就可以在界面上显示出来了,但是,有些格式,如第2-10周|3节/周是没办法提取时间的,这时候就用一定的技巧提取它,这里使用了fillmap函数对一个7*12的数组进行填充,原理是扫描一行,如果具有rowspan值,则填充该行该列为1,如果rowspan大于等于2,则该列下面几行对应的列也填充为1,等到扫描下一行的时候,该位置不会有课程,且不会有td,则如果是一个空td,则填充该行第一个为0的位置。技巧有点难以理解,具体细节稍微自己琢磨领悟下,这样课程的信息就都提取出来了,当然提取方式不止一种。这种方法也不一定能提取所有格式的课程。
提取完毕后进行显示,本来呢是使用LinearLayout简单达到超级课程表的效果的,后来稍微暴力的使用了下自定义ViewGroup,注意了,这个自定义ViewGroup不具有现实使用意义,只是为了展示效果,里面的代码都太暴力了。。。所以看过一遍就无视吧,简直不忍直视
首先是自定义属性
1
2
3
4
5
6
7
8
9
10
|
<!--?xml version=
1.0
encoding=utf-
8
?-->
<resources><linknode><linknode>
<declare-styleable name=
"CourseView"
>
</attr></attr></attr></attr></declare-styleable>
</linknode></linknode></resources>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
package
cn.lizhangqu.kb.view;
import
android.content.Context;
import
android.content.res.TypedArray;
import
android.util.AttributeSet;
import
android.widget.Button;
import
cn.lizhangqu.kb.R;
public
class
CourseView
extends
Button {
private
int
courseId;
private
int
startSection;
private
int
endSection;
private
int
weekDay;
public
CourseView(Context context) {
this
(context,
null
);
}
public
CourseView(Context context, AttributeSet attrs) {
this
(context, attrs,
0
);
}
public
CourseView(Context context, AttributeSet attrs,
int
defStyle) {
super
(context, attrs, defStyle);
TypedArray array=context.obtainStyledAttributes(attrs, R.styleable.CourseView);
courseId = array.getInt(R.styleable.CourseView_courseId,
0
);
startSection=array.getInt(R.styleable.CourseView_startSection,
0
);
endSection=array.getInt(R.styleable.CourseView_endSection,
0
);
weekDay=array.getInt(R.styleable.CourseView_weekDay,
0
);
array.recycle();
}
public
int
getCourseId() {
return
courseId;
}
public
void
setCourseId(
int
courseId) {
this
.courseId = courseId;
}
public
int
getStartSection() {
return
startSection;
}
public
void
setStartSection(
int
startSection) {
this
.startSection = startSection;
}
public
int
getEndSection() {
return
endSection;
}
public
void
setEndSection(
int
endSection) {
this
.endSection = endSection;
}
public
int
getWeek() {
return
weekDay;
}
public
void
setWeek(
int
week) {
this
.weekDay = week;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
|
package
cn.lizhangqu.kb.view;
import
java.util.ArrayList;
import
java.util.List;
import
android.content.Context;
import
android.content.res.TypedArray;
import
android.util.AttributeSet;
import
android.util.DisplayMetrics;
import
android.view.View;
import
android.view.ViewGroup;
import
android.view.WindowManager;
public
class
CourseLayout
extends
ViewGroup {
private
List<courseview><linknode><linknode> courses =
new
ArrayList<courseview>();
private
int
width;
//布局宽度
private
int
height;
//布局高度
private
int
sectionHeight;
//每节课高度
private
int
sectionWidth;
//每节课宽度
private
int
sectionNumber =
12
;
//一天的节数
private
int
dayNumber =
7
;
//一周的天数
private
int
pideWidth =
2
;
//分隔线宽度,dp
private
int
pideHeight =
2
;
//分隔线高度,dp
public
CourseLayout(Context context) {
this
(context,
null
);
}
public
CourseLayout(Context context, AttributeSet attrs) {
this
(context, attrs,
0
);
}
public
CourseLayout(Context context, AttributeSet attrs,
int
defStyle) {
super
(context, attrs, defStyle);
width = getScreenWidth();
//默认宽度全屏
height = dip2px(
600
);
//默认高度600dp
pideWidth = dip2px(
2
);
//默认分隔线宽度2dp
pideHeight = dip2px(
2
);
//默认分隔线高度2dp
}
@Override
protected
void
onMeasure(
int
widthMeasureSpec,
int
heightMeasureSpec) {
setMeasuredDimension(width, height);
}
@Override
protected
void
onLayout(
boolean
changed,
int
l,
int
t,
int
r,
int
b) {
courses.clear();
//清除
sectionHeight = (getMeasuredHeight() - pideWidth * sectionNumber)/ sectionNumber;
//计算每节课高度
sectionWidth = (getMeasuredWidth() - pideWidth * dayNumber)/ dayNumber;
//计算每节课宽度
int
count = getChildCount();
//获得子控件个数
for
(
int
i =
0
; i < count; i++) {
CourseView child = (CourseView) getChildAt(i);
courses.add(child);
//增加到list中
int
week = child.getWeek();
//获得周几
int
startSection = child.getStartSection();
//开始节数
int
endSection = child.getEndSection();
//结束节数
int
left = sectionWidth * (week -
1
) + (week) * pideWidth;
//计算左边的坐标
int
right = left + sectionWidth;
//计算右边坐标
int
top = sectionHeight * (startSection -
1
) + (startSection) * pideHeight;
//计算顶部坐标
int
bottom = top + (endSection - startSection +
1
) * sectionHeight+ (endSection - startSection) * pideHeight;
//计算底部坐标
child.layout(left, top, right, bottom);
}
}
public
int
dip2px(
float
dip) {
float
scale = getContext().getResources().getDisplayMetrics().density;
return
(
int
) (dip * scale +
0
.5f);
}
public
int
getScreenWidth() {
WindowManager manager = (WindowManager) getContext().getSystemService(
Context.WINDOW_SERVICE);
DisplayMetrics displayMetrics =
new
DisplayMetrics();
manager.getDefaultDisplay().getMetrics(displayMetrics);
return
displayMetrics.widthPixels;
}
}
</courseview></linknode></linknode></courseview>
|
使用控件,记得增加命名空间
1
2
3
4
5
6
7
8
9
|
<linearlayout android:layout_height=
"match_parent"
android:layout_width=
"match_parent"
android:orientation=
"vertical"
tools:context=
"${relativePackage}.${activityClass}"
xmlns:android=
"https://schemas.android.com/apk/res/android"
xmlns:custom=
"https://schemas.android.com/apk/res/cn.lizhangqu.kb"
xmlns:tools=
"https://schemas.android.com/tools"
><linknode><linknode>
<linearlayout android:id=
"@+id/weekend"
android:layout_height=
"30dp"
android:layout_width=
"match_parent"
android:orientation=
"horizontal"
><button android:background=
"@drawable/kb0"
android:layout_height=
"match_parent"
android:layout_weight=
"1"
android:layout_width=
"0dp"
android:text=
"周一"
android:textcolor=
"#2e94da"
android:textsize=
"12sp"
></button><button android:background=
"@drawable/kb0"
android:layout_height=
"match_parent"
android:layout_weight=
"1"
android:layout_width=
"0dp"
android:text=
"周二"
android:textcolor=
"#2e94da"
android:textsize=
"12sp"
></button><button android:background=
"@drawable/kb0"
android:layout_height=
"match_parent"
android:layout_weight=
"1"
android:layout_width=
"0dp"
android:text=
"周三"
android:textcolor=
"#2e94da"
android:textsize=
"12sp"
></button><button android:background=
"@drawable/kb0"
android:layout_height=
"match_parent"
android:layout_weight=
"1"
android:layout_width=
"0dp"
android:text=
"周四"
android:textcolor=
"#2e94da"
android:textsize=
"12sp"
></button><button android:background=
"@drawable/kb0"
android:layout_height=
"match_parent"
android:layout_weight=
"1"
android:layout_width=
"0dp"
android:text=
"周五"
android:textcolor=
"#2e94da"
android:textsize=
"12sp"
></button><button android:background=
"@drawable/kb0"
android:layout_height=
"match_parent"
android:layout_weight=
"1"
android:layout_width=
"0dp"
android:text=
"周六"
android:textcolor=
"#2e94da"
android:textsize=
"12sp"
></button><button android:background=
"@drawable/kb0"
android:layout_height=
"match_parent"
android:layout_weight=
"1"
android:layout_width=
"0dp"
android:text=
"周日"
android:textcolor=
"#2e94da"
android:textsize=
"12sp"
>
<scrollview android:id=
"@+id/scrollview"
android:layout_height=
"wrap_content"
android:layout_width=
"wrap_content"
android:scrollbars=
"none"
>
<cn.lizhangqu.kb.view.courselayout android:background=
"#e8e8e8"
android:id=
"@+id/courses"
android:layout_height=
"wrap_content"
android:layout_width=
"wrap_content"
>
</cn.lizhangqu.kb.view.courselayout>
</scrollview></button></linearlayout></linknode></linknode></linearlayout>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
package
cn.lizhangqu.kb.activity;
import
java.util.List;
import
android.app.Activity;
import
android.content.res.ColorStateList;
import
android.graphics.Color;
import
android.os.Bundle;
import
android.util.TypedValue;
import
android.view.Gravity;
import
cn.lizhangqu.kb.R;
import
cn.lizhangqu.kb.model.Course;
import
cn.lizhangqu.kb.service.CourseService;
import
cn.lizhangqu.kb.util.CommonUtil;
import
cn.lizhangqu.kb.view.CourseLayout;
import
cn.lizhangqu.kb.view.CourseView;
/**
* @author lizhangqu
* @date 2015-2-1
*/
public
class
CourseActivity
extends
Activity {
//某节课的背景图,用于随机获取
private
int
[] bg={R.drawable.kb1,R.drawable.kb2,R.drawable.kb3,R.drawable.kb4,R.drawable.kb5,R.drawable.kb6,R.drawable.kb7};
private
CourseService courseService;
private
CourseLayout layout;
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_course);
initValue();
initView();
}
/**
* 初始化变量
*/
private
void
initValue() {
courseService=CourseService.getCourseService();
}
/**
* 初始化视图
*/
private
void
initView() {
//这里有逻辑问题,只是简单的显示了下数据,数据并不一定是显示在正确位置
//课程可能有重叠
//课程可能有1节课的,2节课的,3节课的,因此这里应该改成在自定义View上显示更合理
List<course><linknode><linknode> courses=courseService.findAll();
//获得数据库中的课程
layout=(CourseLayout) findViewById(R.id.courses);
Course course=
null
;
//循环遍历
for
(
int
i =
0
; i < courses.size(); i++) {
course=courses.get(i);
CourseView view=
new
CourseView(getApplicationContext());
view.setCourseId(course.getId());
view.setStartSection(course.getStartSection());
view.setEndSection(course.getEndSection());
view.setWeek(course.getDayOfWeek());
int
bgRes=bg[CommonUtil.getRandom(bg.length-
1
)];
//随机获取背景色
view.setBackgroundResource(bgRes);
view.setText(course.getCourseName()+@+course.getClasssroom());
view.setTextColor(Color.WHITE);
view.setTextSize(
12
);
view.setGravity(Gravity.CENTER);
layout.addView(view);
}
}
}
</linknode></linknode></course>
|
背景图使用的是shape,这里贴出其中一个,其余的就只是颜色不同
1
2
3
4
5
6
7
|
<!--?xml version=
1.0
encoding=utf-
8
?-->
<shape android:shape=
"rectangle"
xmlns:android=
"https://schemas.android.com/apk/res/android"
><linknode><linknode>
<solid android:color=
"#ef9ea0/"
>
<stroke android:color=
"#ef9ea0"
android:width=
"1dp"
>
<corners android:radius=
"5dp/"
>
</corners></stroke></solid></linknode></linknode></shape>
|
至此,超级课程表的一键提取课表功能就完成了。显示最终效果见下方
整个过程可简单概括为抓包分析,数据提取,数据显示,其中关键的一步就是数据的提取。这个过程中有个注意点就是抓课程数据的时候header请求头信息里的referer信息请务必设置为登录成功后的网址,即https://***.***.***.***/xs_main.aspx?xh=XH,否则抓数据的时候页面会被循环重定向,将抓不到数据,程序也会报异常。