精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (1)
|
|
---|---|
作者 | 正文 |
发表时间:2011-03-25
最后修改:2011-03-25
背景相信大家对velocity这一类模板语言都并不陌生,一般velocity大部分是在web应用中,替换jsp的一种选择,做为html页面的渲染。实现UED和开发人员的分离。 但在最近的一个项目中,遇到了一个比较"另类"的需求,就是我们需要抓取一个外部网站的页面内容信息,比如aleax排名。
设计
针对这样的需求,设计上需要考虑的点:
分析:
1. 外部网站的请求。可以使用HttpClient,使用开源的技术可以减少一些风险,比如编码问题,SSL支持,POST提交等问题。
2. 网站的信息分析和抓取,得看具体网站的返回信息,需要支持各种协议的数据。比如xml , json , 普通的html。
3. 外部网站页面变化的应对策略:
大的技术内容已经明确,这里主要考虑的是如何设计灵活的抓取策略的规则。
抓取策略大致内容:
1. 构造请求参数,不同的页面有不同的参数
2. 获取外部网站返回结果。比如返回编码处理
3. 解析对应的返回内容,按照json,xml,字符串进行处理。
4. 返回对应的结果。 不同页面的返回结果会出现不同。
因为按照需求设计分析,已经很明确抓取策略的这一块仅仅是通过xml这类静态配置会设计的很复杂,很明显一类动态语言比较适合干这样的活,但项目组中对动态语言基本没使用,大部门下使用的也几乎没有,存在比较大的技术风险。这时候,就轮到velocity登场了,使用模板语言代替动态语言。事实证明,使用velocity处理这类问题也挺适合
常见的velocity处理过程:
总的来说,可以分为两步: 模板查找,模板渲染。
模板查找:
根据给定的name,返回对应的String,InputSteam流。
模板渲染:boolean evaluate( Context context, Writer out, String logTag, String instring )
根据给定的上下文,和模板内容,进行渲染,并将最终的合并结果直接输出到Writer中。
项目特殊使用:
1. 模板查找:
因为对应的模板来自于数据库,所以这里就直接省去了模板查找的这一过程,直接给出对应的script。 同样你也可以使用velocity自带提供的基于database的模板查找DataSourceResourceLoader。
resource.loader = ds ds.resource.loader.public.name = DataSource ds.resource.loader.description = Velocity DataSource Resource Loader ds.resource.loader.class = org.apache.velocity.runtime.resource.loader.DataSourceResourceLoader ds.resource.loader.resource.datasource = java:comp/env/jdbc/Velocity ##依赖jndi ds.resource.loader.resource.table = tb_velocity_template ds.resource.loader.resource.keycolumn = id_template ds.resource.loader.resource.templatecolumn = template_definition ds.resource.loader.resource.timestampcolumn = template_timestamp ds.resource.loader.cache = false ds.resource.loader.modificationCheckInterval = 60 因为项目使用的是spring database管理,所以这里不用velocity自带的一套。
2. 模板渲染:
构造了自己的Context,进行页面渲染。直接从context获取对应的返回结果,因为velocity默认是进行字符串输出,对于复杂的返回对象不好处理
减简过后,最后的类设计:
对应的时序图:
核心类代码
public class VelocityScriptExecutor implements ScriptExecutor { private static final String RESULT = "result"; private static final int DEFAULT_CACHE_SIZE = 1000; private RuntimeInstance ri = new RuntimeInstance(); private Map<String, SimpleNode> cache; private int cacheSize = DEFAULT_CACHE_SIZE; private Properties velocityProperties; private boolean needCache = true; private static final Writer nop = new Writer() { //自定义空的write对象 public void close() throws IOException { } public void flush() throws IOException { } public void write(char[] cbuf, int off, int len) { } }; public void initialize() throws ScriptException { if (cacheSize <= 0) {// 不考虑cacheSize为0的情况,强制使用LRU cache机制 cacheSize = DEFAULT_CACHE_SIZE; } cache = new LRULinkedHashMap<String, SimpleNode>(cacheSize); // 初始化velocity instance ri.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new NullLogChute());// 设置日志输出 if (velocityProperties != null) { try { ri.init(velocityProperties); } catch (Exception e) { throw new ScriptException(e); } } } /** * <pre> * 1. 接受VelocityScriptContext上下文,自身并不关心所谓的pullTool的存在 * 2. script针对对应name下的script脚本 * </pre> */ public Object evaluate(ScriptContext context, String script) throws ScriptException { InternalContextAdapterImpl ica = new InternalContextAdapterImpl((AbstractContext) context); SimpleNode nodeTree = null; if (needCache) { nodeTree = get(script); } if (nodeTree == null) { try { nodeTree = ri.parse(new StringReader(script), ""); } catch (Exception e) { throw new ScriptException(e); } nodeTree.init(ica, ri); if (needCache) { put(script, nodeTree); } } try { nodeTree.render(ica, nop); } catch (Exception e) { throw new ScriptException(e); } return ica.get(RESULT); //直接从context中获取对应#set(result=xxx),对应的result变量 }
public class VelocityScriptContext extends AbstractContext implements ScriptContext { private Map context = new HashMap(); // 基于map的上线文实现 private Map pullTool = new HashMap(); // pullTool上下文实现 @Override public boolean internalContainsKey(Object key) { return context.containsKey(key) || pullTool.containsKey(key); } @Override public Object internalGet(String key) { return context.get(key) != null ? context.get(key) : pullTool.get(key); } @Override public Object[] internalGetKeys() { return context.keySet().toArray(); } @Override public Object internalPut(String key, Object value) { return context.put(key, value); } @Override public Object internalRemove(Object key) { return context.remove(key); } // ================== setter / getter ======================= public void setContext(Map context) { this.context = context; } public void setPullTool(Map pullTool) { this.pullTool = pullTool; } } 说明: pulltool的概念就是类似于velocity toolbox提供的一堆工具util,比如HttpClientTool , JsonTool , XmlTool。可以按照自己的需求封装对应的tool.
最后一个script配置实现:(比如抓取google收录某关键字的记录数)
##处理参数 #set($keyword = $param.keyword) #set($userAgent = $param.userAgent) ## 构造url #set($url="http://www.google.com.hk/search?hl=zh-CN&q=${keyword}") ## 获取页面 #set($param=$httpClient.createParam()) $param.config.setReadTimeout(3000) $param.config.setConnectionTimeout(3000) $param.config.setContentEncoding("UTF-8") $param.header.setUserAgent($userAgent) #set($html=$httpClient.request($url,$param)) ## 处理页面 #set($recordNum=$stringTool.substringBetween($html,"找到约","条结果")) #set($cost=$stringTool.substringBetween($html,"(用时","秒)")) #set($record=$stringTool.trim($record)) #set($cost=$stringTool.trim($cost)) ## 生成返回结果 #set($result = $resultTool.createMap()) $result.put("recordNum",$recordNum) $result.put("cost",$cost)
返回结果,一个map对象,包含了两个key,一个是记录数,一个是耗时。最后在页面上展示时,可以包装成json对象进行控制和显示。
{recordNum="4,140,000,000 ",cost="0.13"} 最后本文并没有太多高深的技术,只是对velocity的另一种使用,用于解决一些特定的业务场景。欢迎大家拍砖! 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2011-03-25
说的简单点,就是直接将Velocity做为简易的"动态语言"在使用,而不仅仅再是一个模板输出而已。
|
|
返回顶楼 | |
发表时间:2011-03-26
思路真不错。
不过如果是我就直接用groovy了 |
|
返回顶楼 | |
发表时间:2011-03-26
最后修改:2011-03-26
tedeyang 写道 思路真不错。
不过如果是我就直接用groovy了 恩,技术上是可以直接使用groovy。 但考虑一个公司背景,项目其他成员大部分不熟悉动态语言,相应的开发和维护会是有一个比较大的熟悉成本,而velocity可以说是我们用得挺烂熟的了,可以说是0成本。 |
|
返回顶楼 | |
发表时间:2011-03-26
有写好这个脚手架的时间,用groovy模拟请求抓别人首页足够了。
|
|
返回顶楼 | |
发表时间:2011-03-26
油炸大龙虾 写道 有写好这个脚手架的时间,用groovy模拟请求抓别人首页足够了。
写脚手架无非也就是为项目组的成员熟悉和维护的成本最低,单从技术上来说groovy是能解决这问题,但感觉杀鸡用牛刀了,反而对项目是个负担 |
|
返回顶楼 | |
发表时间:2011-03-26
引用 因为按照需求设计分析,已经很明确抓取策略的这一块仅仅是通过xml这类静态配置会设计的很复杂,很明显一类动态语言比较适合干这样的活 动态语言没学过,只是简单了解过,请问这里为什么说动态语言比较适合干呢? |
|
返回顶楼 | |
发表时间:2011-03-26
最后修改:2011-03-26
onlylau 写道 引用 因为按照需求设计分析,已经很明确抓取策略的这一块仅仅是通过xml这类静态配置会设计的很复杂,很明显一类动态语言比较适合干这样的活 动态语言没学过,只是简单了解过,请问这里为什么说动态语言比较适合干呢? 分析得到的抓取策略大致内容: 1. 构造请求参数,不同的页面有不同的参数 2. 获取外部网站返回结果。比如返回编码处理 3. 解析对应的返回内容,按照json,xml,字符串进行处理。 4. 返回对应的结果。 不同页面的返回结果会出现不同。 如果按照这样的需求,你可以尝试考虑一下用xml解决,你会发现有其复杂度,以及它的一些限制。 因为XML每个标签或者属性都得有相应的parser进行解析,从而转换成相应的业务代码,所以有比较强的约束性。 动态语言是不需要编译处理,比较符合我原先CRUD管理的需求,所以说这里用来做script配置比较适合。 你也可以去看下jetty的配置文件,它的配置文件的语法也是有一些动态语言的味道,比如ref/invoke等,所以说它在整体设计上就非常适合做内嵌式web容器。 |
|
返回顶楼 | |
发表时间:2011-03-27
思路清晰,不错的解决方法!
从长远看,觉得还是选用groovy比较合适。短期会有学习成本,但省去了长期自己维护一套的负担。 |
|
返回顶楼 | |
发表时间:2011-03-27
可以用Javascript,JDK6可以直接解析。
|
|
返回顶楼 | |