背景
相信大家对velocity这一类模板语言都并不陌生,一般velocity大部分是在web应用中,替换jsp的一种选择,做为html页面的渲染。实现UED和开发人员的分离。
但在最近的一个项目中,遇到了一个比较"另类"的需求,就是我们需要抓取一个外部网站的页面内容信息,比如aleax排名。
设计
针对这样的需求,设计上需要考虑的点:
- 外部网站不稳定,超时时间的设置,访问的域名变化(域名换成ip等)
- 我们需要精确抓取外部网站的信息,外部网站页面的任意变化,都会导致抓取失败
- 有几个类似的需求,需要进行统一考虑
分析:
1. 外部网站的请求。可以使用HttpClient,使用开源的技术可以减少一些风险,比如编码问题,SSL支持,POST提交等问题。
2. 网站的信息分析和抓取,得看具体网站的返回信息,需要支持各种协议的数据。比如xml , json , 普通的html。
3. 外部网站页面变化的应对策略:
- 外部网站的抓取策略是可以配置型,比如xml或者数据库。这里比较倾向于数据库,通过一个后台页面,进行CRUD的管理。这样可以做到及时的响应,而不需要重新发布代码。
- 对外部网站的抓取结果定时进行监控。 比如模拟请求一次,监控具体的返回结果,如有问题进行手机报警。
大的技术内容已经明确,这里主要考虑的是如何设计灵活的抓取策略的规则。
抓取策略大致内容:
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。
1.resource.loader = ds
2.ds.resource.loader.public.name = DataSource
3.ds.resource.loader.description = Velocity DataSource Resource Loader
4.ds.resource.loader.class = org.apache.velocity.runtime.resource.loader.DataSourceResourceLoader
5.ds.resource.loader.resource.datasource = java:comp/env/jdbc/Velocity ##依赖jndi
6.ds.resource.loader.resource.table = tb_velocity_template
7.ds.resource.loader.resource.keycolumn = id_template
8.ds.resource.loader.resource.templatecolumn = template_definition
9.ds.resource.loader.resource.timestampcolumn = template_timestamp
10.ds.resource.loader.cache = false
11.ds.resource.loader.modificationCheckInterval = 60
因为项目使用的是spring database管理,所以这里不用velocity自带的一套。
2. 模板渲染:
构造了自己的Context,进行页面渲染。直接从context获取对应的返回结果,因为velocity默认是进行字符串输出,对于复杂的返回对象不好处理
减简过后,最后的类设计:
对应的时序图:
核心类代码
1.public class VelocityScriptExecutor implements ScriptExecutor {
2. private static final String RESULT = "result";
3. private static final int DEFAULT_CACHE_SIZE = 1000;
4. private RuntimeInstance ri = new RuntimeInstance();
5. private Map<String, SimpleNode> cache;
6. private int cacheSize = DEFAULT_CACHE_SIZE;
7. private Properties velocityProperties;
8. private boolean needCache = true;
9. private static final Writer nop = new Writer() { //自定义空的write对象
10.
11. public void close() throws IOException {
12. }
13.
14. public void flush() throws IOException {
15. }
16.
17. public void write(char[] cbuf, int off, int len) {
18. }
19. };
20.
21.public void initialize() throws ScriptException {
22. if (cacheSize <= 0) {// 不考虑cacheSize为0的情况,强制使用LRU cache机制
23. cacheSize = DEFAULT_CACHE_SIZE;
24. }
25.
26. cache = new LRULinkedHashMap<String, SimpleNode>(cacheSize);
27. // 初始化velocity instance
28. ri.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new NullLogChute());// 设置日志输出
29. if (velocityProperties != null) {
30. try {
31. ri.init(velocityProperties);
32. } catch (Exception e) {
33. throw new ScriptException(e);
34. }
35. }
36. }
37.
38./**
39. * <pre>
40. * 1. 接受VelocityScriptContext上下文,自身并不关心所谓的pullTool的存在
41. * 2. script针对对应name下的script脚本
42. * </pre>
43. */
44. public Object evaluate(ScriptContext context, String script) throws ScriptException {
45. InternalContextAdapterImpl ica = new InternalContextAdapterImpl((AbstractContext) context);
46. SimpleNode nodeTree = null;
47. if (needCache) {
48. nodeTree = get(script);
49. }
50. if (nodeTree == null) {
51. try {
52. nodeTree = ri.parse(new StringReader(script), "");
53. } catch (Exception e) {
54. throw new ScriptException(e);
55. }
56. nodeTree.init(ica, ri);
57. if (needCache) {
58. put(script, nodeTree);
59. }
60. }
61. try {
62. nodeTree.render(ica, nop);
63. } catch (Exception e) {
64. throw new ScriptException(e);
65. }
66.
67. return ica.get(RESULT); //直接从context中获取对应#set(result=xxx),对应的result变量
68. }
1.public class VelocityScriptContext extends AbstractContext implements ScriptContext {
2.
3. private Map context = new HashMap(); // 基于map的上线文实现
4. private Map pullTool = new HashMap(); // pullTool上下文实现
5.
6. @Override
7. public boolean internalContainsKey(Object key) {
8. return context.containsKey(key) || pullTool.containsKey(key);
9. }
10.
11. @Override
12. public Object internalGet(String key) {
13. return context.get(key) != null ? context.get(key) : pullTool.get(key);
14. }
15.
16. @Override
17. public Object[] internalGetKeys() {
18. return context.keySet().toArray();
19. }
20.
21. @Override
22. public Object internalPut(String key, Object value) {
23. return context.put(key, value);
24. }
25.
26. @Override
27. public Object internalRemove(Object key) {
28. return context.remove(key);
29. }
30.
31. // ================== setter / getter =======================
32.
33. public void setContext(Map context) {
34. this.context = context;
35. }
36.
37. public void setPullTool(Map pullTool) {
38. this.pullTool = pullTool;
39. }
40.
41.}
说明:
pulltool的概念就是类似于velocity toolbox提供的一堆工具util,比如HttpClientTool , JsonTool , XmlTool。可以按照自己的需求封装对应的tool.
最后一个script配置实现:(比如抓取google收录某关键字的记录数)
1.##处理参数
2.#set($keyword = $param.keyword)
3.#set($userAgent = $param.userAgent)
4.
5.## 构造url
6.#set($url="http://www.google.com.hk/search?hl=zh-CN&q=${keyword}")
7.## 获取页面
8.#set($param=$httpClient.createParam())
9.$param.config.setReadTimeout(3000)
10.$param.config.setConnectionTimeout(3000)
11.$param.config.setContentEncoding("UTF-8")
12.
13.$param.header.setUserAgent($userAgent)
14.#set($html=$httpClient.request($url,$param))
15.## 处理页面
16.#set($recordNum=$stringTool.substringBetween($html,"找到约","条结果"))
17.#set($cost=$stringTool.substringBetween($html,"(用时","秒)"))
18.
19.#set($record=$stringTool.trim($record))
20.#set($cost=$stringTool.trim($cost))
21.## 生成返回结果
22.#set($result = $resultTool.createMap())
23.$result.put("recordNum",$recordNum)
24.$result.put("cost",$cost)
返回结果,一个map对象,包含了两个key,一个是记录数,一个是耗时。最后在页面上展示时,可以包装成json对象进行控制和显示。
1.{recordNum="4,140,000,000 ",cost="0.13"}
最后
本文并没有太多高深的技术,只是对velocity的另一种使用,用于解决一些特定的业务场景。欢迎大家拍砖!