好久未写技术类文章了,今天把这几天搞的“事儿”总结一下。
长期以来我们都使用Pentaho Server自带的Schedule给Boss定时发送数据报表,相信很多人都用过,这种定时的Schedule好是好,但有个弊端,就是一旦你要发送的报表数据依赖某些前置作业,你必须保证你设置的定时发送时间要大于前置作业的完成时间。一旦你的前置作业失败,pentaho也会傻不啦叽的定时发送报表,导致发出去的报表数据有问题,这可是发给Boss的,可得多想想。
为了避免发送脏数据,最开始我使用的是一种简单粗暴的方式,就是判断前置作业是否成功,一旦失败,我就把整个pentaho server停掉。这样是可以避免发送脏数据,但也阻止了服务器上其他正常的报表发送。
正确的做法是:有问题的报表数据别发,没问题的报表数据就发出来呗。但目前Pentaho Server并没有提供相应的依赖配置接口(报表与作业依赖的接口,目前可以使用kettle job发送报表,但不够稳定,且不满足我们的现实要求,因为我们的作业逻辑都是使用的存储过程,而非kettle job),这块不得不自己搞。
要解决这个依赖问题,首先想到的是自己写个发送报表的程序,而弃用Pentaho的定时发送。这个程序应该要包括两个部分:一是生成eml格式的报表;二是使用Java Mail来发送。一旦脚本完成,就可以使用调度程序将脚本和前置作业的依赖配置到我们的调度系统中。
Java Mail发送邮件很简单,关键在于如何生成eml格式的报表。愚笨的我看不懂Pentaho的源代码,显然自己来实现eml有难度,特别是还要保持格式正确,貌似就更难了。好在Pentaho提供了一个下载eml的方式,这给了我灵感,我能不能写个爬虫来下载这个eml呢?
不懂Web开发的我硬着头皮看了Pentaho权限请求相关内容。于是有了下面的代码:
user_agent = (
'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.11 (KHTML, like Gecko) '
'Chrome/20.0.1132.57 Safari/536.11'
)
def __init__(self, username, password, cookies_tosave='pentahologin.cookies'):
# 设置请求头
self.username = username
self.password = password
self.cookies_tosave = cookies_tosave
# 在session中发送登录请求,此后这个session里就存储了cookie
# 可以用print(session.cookies.get_dict())查看
self.session = requests.session()
self.session.headers['User-Agent'] = self.user_agent
def login(self):
#构造Form表单数据
data = {
'j_username': self.username,
'j_password': self.password
}
#登录页面
login_url="http://xxx.xxx.xxx.xxx/pentaho/j_spring_security_check"
resp = self.session.post(login_url, data=data,verify=False, timeout=10)
#print resp
def downloadFile(self,type,prpt_file_name,file_path):
if type=='eml':
#构造url,该url为登录后才能访问的网页
url = "http://xxx.xxx.xxx.xxx/pentaho/api/repos/%3Ahome%3Abi_team%3A"+prpt_file_name\
+".prpt/generatedContent?output-target=mime-message/text/html"
eml_content = self.session.get(url).content
#print eml_content
#所有带中文的字符串都是byte string,而函数中所产生的字符串则被认为是unicode string
#因此需要将byte string转成unicode string,使用unicode函数
full_file_name = unicode(file_path+"\\"+prpt_file_name+".eml",'utf-8')
# 写入文件,采用二进制写入文件
with open(full_file_name, 'wb') as f:
f.write(eml_content)
代码倒是很简单,但这些代码对于一个新手来说还是有难度。简单说一下:
init():这个很简单,就是初始化一个session,这个session是成功下载文件的关键,当你要向pentaho server请求下载一个文件时,它会先看看你有没有权限,因此我们需要保留登录成功的session,已保证用同一个session去请求下载文件。只所以用session而不使用cookie来获得用户登录信息是因为我有账号和密码,不需要再解析cookie了。
login(): 根据你请求的页面(这里请求的页面就是login_url),构造post参数,返回的resp就是response页面。因为我们的pentaho server登录没有验证,所有这太easy了,直接把j_username,j_password作为参数传进去就可以了。至于为什么要用j_username,j_password作为参数,详见:https://help.pentaho.com/Documentation/8.2/Developer_Center/REST_API
downloadFile():在login成功后,就可以使用同一个session去请求下载eml文件了,直接使用Get方法获取response页面的内容就可以了。为了保持格式,在写入file时,我使用了'wb'二进制模式写入。
OK,这三步就完成了登录Pentaho Server并下载eml的的逻辑,当然,你还可以下载Pentaho server提供的其他类型的文件,比如Pdf,excel,excel2007等等,只是在downloadFile中请求的url需要换换。
详见:https://help.pentaho.com/Documentation/8.2/Developer_Center/Embed_Pentaho_Server
发个牢骚:Pentaho的帮助文档太差,国外的资料比较少,国内的更没法看,都是千篇一律。上面两个url找了好久才找到,怎么用也没个sample,像我这样的小白只能靠不断尝试。
至于如果使用Java Mail来发送邮件,也就是python生成的eml文件,我就不再赘述了,BaiDu一下一箩筐,都是精品。
再完成上述两个部分后,我如释重负,报表的发送终于可以不用定时发送而是依靠前置作业的完成情况来发送了。
但过了两天,我再来看自己的代码,貌似可以更简单点儿,既然Pentaho server有手动触发发送报表的功能,为什么我不可以调用它的这个功能呢?如果可以用程序来实现手动触发,那不是更简单,都不用把eml文件download下来,然后还要调用Java Mail来发送报表这么复杂了。
有了前几天的努力,这步实现起来貌似轻松很多,上代码:
def triggerNow(self,jobId):
header_dict = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko',
"Content-Type": "application/json"}
#POST请求触发报表发送
triggerNow_url = "http://xxxx.xxx.xxx.xxx/pentaho/api/scheduler/triggerNow"
#jobId通过请求http://xxxx.xxx.xxx.xxx/pentaho/api/scheduler/jobs获得
request_body={"jobId": jobId}
data_json=json.dumps(request_body)
resp2 = self.session.post(triggerNow_url,headers=header_dict, data=data_json,verify=False, timeout=10)
return resp2.content
header_dict:就是请求头,需要注意一定是这里需要设置Content-Type为application/json,表示使用json格式来传递数据。
triggerNow_url:请求地址,也是找了老半天,详见:
https://help.pentaho.com/Documentation/7.0/0R0/070/020/050/Scheduler_Resource
jobId:这里的jobId是通过Get方式请求http://xxxx.xxx.xxx.xxx/pentaho/api/scheduler/jobs得到的内容,详见:https://help.pentaho.com/Documentation/7.0/0R0/070/020/050/Scheduler_Resource中关于/scheduler/job部分描述
json.dumps 将数据转成json格式
再调用triggerNow()前依然要先调用login()来获得同一个session,否则会告诉你授权失败。
天生愚钝的我搞这个搞了一周,除了自己不太熟悉Web开发外,更多的是缺乏练习,这些代码不算一个爬虫,只是有点儿爬虫的影子罢了,也没考虑过多的细节,希望各路大神赐教~
附上完整代码,有需要的可以参考下:
#encoding=utf-8
import requests
import sys,getopt
import json
import chardet
class PentahoLogin:
user_agent = (
'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.11 (KHTML, like Gecko) '
'Chrome/20.0.1132.57 Safari/536.11'
)
def __init__(self, username, password, cookies_tosave='pentahologin.cookies'):
# 设置请求头
self.username = username
self.password = password
self.cookies_tosave = cookies_tosave
# 在session中发送登录请求,此后这个session里就存储了cookie
# 可以用print(session.cookies.get_dict())查看
self.session = requests.session()
self.session.headers['User-Agent'] = self.user_agent
def login(self):
#构造Form表单数据
data = {
'j_username': self.username,
'j_password': self.password
}
#登录页面
login_url="http://xxx.xxx.xxx.xxx/pentaho/j_spring_security_check"
resp = self.session.post(login_url, data=data,verify=False, timeout=10)
#print resp
def triggerNow(self,jobId):
header_dict = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko',
"Content-Type": "application/json"}
#POST请求触发报表发送
triggerNow_url = "http://xxx.xxx.xxx.xxx/pentaho/api/scheduler/triggerNow"
#jobId通过请求http://xxx.xxx.xxx.xxx/pentaho/api/scheduler/jobs获得
request_body={"jobId": jobId}
data_json=json.dumps(request_body)
resp2 = self.session.post(triggerNow_url,headers=header_dict, data=data_json,verify=False, timeout=10)
return resp2.content
def getJobId(self,scheduleName):
jobs_url = "http://xxx.xxx.xxx.xxx/pentaho/api/scheduler/jobs"
jobs_response = self.session.get(jobs_url)
json_dict = json.loads(jobs_response.content)
#得到所有的jobs
value_dict = json_dict.values()[0]
res_item = ''
#遍历每个job,查找其jobid中是否包含了需要调度的job
for i in range(0,len(value_dict)):
#print value_dict[i]['jobId']
items = value_dict[i]['jobId'].split('\t')
for item in items:
if item==scheduleName:
res_item=value_dict[i]['jobId']
break
return res_item
def downloadFile(self,type,prpt_file_name,file_path):
if type=='eml':
#构造url,该url为登录后才能访问的网页
url = "http://xxx.xxx.xxx.xxx/pentaho/api/repos/%3Ahome%3Abi_team%3A"+prpt_file_name\
+".prpt/generatedContent?output-target=mime-message/text/html"
eml_content = self.session.get(url).content
#print eml_content
#所有带中文的字符串都是byte string,而函数中所产生的字符串则被认为是unicode string
#因此需要将byte string转成unicode string,使用unicode函数
full_file_name = unicode(file_path+"\\"+prpt_file_name+".eml",'utf-8')
# 写入文件,采用二进制写入文件
with open(full_file_name, 'wb') as f:
f.write(eml_content)
def main(argv):
scheduleName=''
try:
opts, args = getopt.getopt(argv,"hi:")
except getopt.GetoptError:
print 'e.g. PentahoLogin.py '
sys.exit(2)
for opt, arg in opts:
if opt == '-h' or len(opts) <> 1:
print 'e.g. PentahoLogin.py <调度作业名称>'
sys.exit()
elif opt == '-i':
scheduleName = arg
username = 'x'
password = 'x'
pl = PentahoLogin(username, password)
pl.login()
return_code = pl.triggerNow(pl.getJobId(scheduleName))
print return_code
if return_code<>'NORMAL':
sys.exit(2)
else:
sys.exit()
if __name__ == '__main__':
main(sys.argv[1:])
'''
def main(argv):
prpt_file_name = ''
file_path = ''
try:
opts, args = getopt.getopt(argv,"hi:o:")
except getopt.GetoptError:
print 'PentahoLogin.py -i -o <生成文件的路径>'
sys.exit(2)
for opt, arg in opts:
if opt == '-h' or len(opts)<>2:
print 'PentahoLogin.py -i -o <生成文件的路径>'
sys.exit()
elif opt == '-i':
prpt_file_name = arg
elif opt == '-o':
file_path = arg
# chardet.detect(prpt_file_name)
print '输入的文件为:', prpt_file_name.decode('gb2312').encode('utf-8')
print '输出的文件路径为:', file_path
username = 'x'
password = 'x'
pl = PentahoLogin(username, password)
pl.login()
pl.downloadFile('eml', prpt_file_name.decode('gb2312').encode('utf-8'), file_path)
if __name__ == "__main__":
main(sys.argv[1:])
'''