黄杰, 2012-07-10
root[a]linuxsand.info
在校期间就想做个类似的东西,结果拖到了假期才做。尽管这货几乎没什么用,不过从中好歹可以学点东西,所以我做了。
分解成这么几块:
第一步是工作量最大的,不过事后发现后两步花费时间也不少。第一步中,我需要:
这 2 个库我都是初次使用(新手上路)。学校的网站是很恶心的,不知道是什么时候做的。我要的数据是套在 iframe 里的。不过 mechanize 可以对付这个,尝试了几次后成功了:只需把它当成是浏览器,自己按浏览顺序拿到 URL 即可。示例代码:
def get_page_data(user, pwd, type='class_table'):
'''
type='class_table': get webpage including class table
type='show_grade': get webpage including grade
type='name': get webpage including your name
'''
login_url = 'http://info.just.edu.cn:81'
username, password = user, pwd
br = Browser()
br.open(login_url)
br.select_form(name='loginForm')
br['userName'], br['userPass'] = username, password
br.submit()
if type == 'class_table':
br.open('http://info.just.edu.cn:81/roamingAction.do?appId=BKS_XK')
br.open('http://jwxx.just.edu.cn:7777/pls/wwwbks/xk.CourseView')
class_page = br.response().get_data()
return class_page
elif type == 'show_grade':
br.open('http://info.just.edu.cn:81/roamingAction.do?appId=BKS_CJCX')
br.open('http://jwxx.just.edu.cn:7777/pls/wwwbks/bkscjcx.curscopre')
grade_page = br.response().get_data()
return grade_page
elif type == 'name':
br.open('http://info.just.edu.cn:81/roamingAction.do?appId=BKS_XJXX')
br.open('http://jwxx.just.edu.cn:7777/pls/wwwbks/bks_xj.xjcx')
name_page = br.response().get_data()
return name_page
当然,我从不同页面获取了多组数据。下面是分析提取课程表的函数,用到了BeatifulSoup。传入包含课表数据的页面,返回一个嵌套列表(二维数组)。
# coding: utf-8
def get_class(class_page):
'''
get class info from get_page_data(type='class_table')
data stored in a list.
'''
tables = BeautifulSoup(class_page)('table')
table = BeautifulSoup(str(tables[10]))
table_item = table('p', align="center")
item = [re.sub(r'<p align="center">|</p>|\n', '', str(i), 8)
for i in table_item]
whole_list = []
for j in range(0, len(item), 9):
try:
when_to_class = item[j+8].replace(u'\u00A0'.encode('utf8'), '')
if when_to_class == '' or ord(when_to_class[-1]) > 128:
continue
else:
class_name = item[j].replace(u'\u00A0'.encode('utf8'), '')
where = item[j+6].replace(u'\u00A0'.encode('utf8'), '') \
.replace(u'\uff13'.encode('utf8'), '3')
where_to_class = where[:-3] + '-' + where[-3:]
which_class = item[j+7].replace(u'\u00A0'.encode('utf8'), '')
tmp = [i + 1 for i, k in enumerate(when_to_class) if k == '1']
Min, Max = min(tmp), max(tmp)
time_to_class = '%02d-%02d' % (Min, Max)
# which_class = '3-2' -> wednesday 2nd class
# -> day = 3, class_ = 2
day = int(which_class.split('-')[0])
class_ = int(which_class.split('-')[1])
part_list = [day, class_,
'<br />%s<br />第%s周<br />%s<br /><br />' %
(class_name, time_to_class, where_to_class)]
whole_list.append(part_list)
except IndexError:
break
whole_list.sort()
class_list = [' 周一 周二 周三 周四 周五'.split(),
'第一大节 '.split(),
'第二大节 '.split(),
'第三大节 '.split(),
'第四大节 '.split(),
'第五大节 '.split(),
]
for line in whole_list:
class_list[line[1]][line[0]] += line[2]
return class_list
到这里,已经可以从控制台打印出需要的结果了。不过它还是一个嵌套的列表,我最终需要输出 HTML 表格,所以再转换下。
def gen_class_table(class_data, name):
'''
generate class table from get_class()
return value is a big string.
'''
table_list = []
for line in class_data:
line_list = []
for item in line:
line_list.append(' <td>' + item + '</td>\n')
_line = ' <tr>\n' + ''.join(line_list) + ' </tr>\n'
table_list.append(_line)
table = '''<table border="1" cellpadding="10" cellspacing="0" \
style="text-align: center;">\n
<caption style="font-size: 2em; font-weight: bold;">
%s的课程表</caption>\n''' % name + \
''.join(table_list) + '</table>'
return table
现在,我得到了 HTML 表格了(string)。
差不多可以整合到一起了。我用 GetClassTable
这个类来处理根目录,定义 GET / POST,渲染、提交表单以返回结果,以及发送邮件。
class GetClassTable:
def GET(self):
f = login_form()
return render.form(f)
def POST(self):
f = login_form()
if not f.validates():
return render.form(f)
else:
user, password = f.d.number, f.d.password
name_page = get_page_data(user, password, type='name')
name = get_name(name_page)
class_page = get_page_data(user, password, type='class_table')
class_data = get_class(class_page)
class_table = gen_class_table(class_data, name) or ''
try:
from google.appengine.api import mail
mail.send_mail(
sender="linuxsand@gmail.com",
to=f.d.email,
subject='%s你的课程表' % name,
body='',
html='''由 \
<a href="http://www.linuxsand.info/just/">
查课表</a> 自动发送,不要回复。\
<br />%s''' % class_table)
except ImportError:
pass
return render.class_table(table=class_table)
上面有发送邮件的代码。我本来是用 web.py 内置的功能完成的(Sending mail using gmail),甚至特意为此建立了新邮箱 just-notification@linuxsand.info,本地测试正常(部署到线上无法使用)。下面是表单,用内置的 form.py 来做,方便。
login_form = web.form.Form(
web.form.Textbox('number', web.form.notnull,
web.form.regexp('\d+', '- -|||'),
web.form.Validator('- -|||', lambda x:int(x)!=10),
description=u'学号'
),
web.form.Password('password', web.form.notnull, description=u'口令'),
web.form.Textbox('email',
web.form.regexp(r".*@.*", '- -|||'),
description=u'邮箱')
)
提交按钮我放到模板里了。试运行,调试等。
线上部署调试。mechanize 和 BeautifulSoup 在本机正常工作,但是部署到 GAE 就出错。
最后测试发现,从登录到返回结果页面很慢,大概有 15 秒。本地只要 2~3 秒,这我就没办法了。
本着节省一个 GAE app 的目的,我把这个 app 合并到原有的一个 app 内。web.py 支持子应用 。整理如下,备忘。
子应用 just.py
urls = (
'', 'redirect', # Make sure `DOMAIN/just` -> `DOMAIN/just/`
'/', 'login', # `DOMAIN/just/`
'/grade', 'grade') # `DOMAIN/just/grade`, without / at the end
app = web.application(urls, locals()) # locals(), not globals()
主应用 code.py
import just
urls = (
'/', 'index',
'/just', just.app,
'/(\S+)', 'show')
app = web.application(urls, locals()) # locals(), not globals()