最近由于在学校和老师做项目,需要做一个快速过滤url的小的项目,说白了就是存一些规则,到时候用url匹配,要求代码量足够少,效率足够高。说实话其实我一开始我挺迷茫的,因为在我们学校(东北大学)特别是计算机的本科生是比较注重基础的,所以实际的项目经历和平常的代码量都比较少,可能我们学校出来的本科生基础比较好,但是项目经历就要少很多,这个在公司面试过程中是很吃亏,但是一进公司真的是不差,因为现在来我们学校招聘的公司有好多的架构师都是东大本科生就出去了(哈哈,在这里打一个广告,不要介意啊)。言归正传,java这个过滤检索效率的提高就是我这个没有什么经验的人也知道这个项目八成要用哈希表,所以我一开始我设计了一个根据哈希表来检索的,但是由于底层的实现可能比较笨拙,老师看了不是很满意(其实我自己认为已经很接近Adblock plus算法了,哈哈),他就建议我可以参考一下Adblock plus的算法,因为火狐用户基本都用Adblock plus插件,而且效率还行,没办法,老师吩咐,只能照做。
网上有一篇关于Adblock plus原理的博客,但他是翻译版,说实话,翻译的一般般吧,后来我们找到英文版的又读了一遍,写的真是太泛泛了,读完之后就知道一个shotcut ,推荐用8个字符长度,用来做哈希表的键,剩下的就什么都没有了,没办法,看源码吧。。。
说实话,之前我真的是没用过Firefox,没办法,下了一个Firefox,又装上了Adblock plus插件,又进了Adblock plus社区去找源代码,这中间的坎坷就不说了,没接触过的东西开荒真的是会遇到各种莫名其妙的错误啊,有一些甚至老师都非常诧异。不过最后还是找到了Adblock plus的源代码:完整版本源代码链接:https://hg.adblockplus.org/ github镜像:https://github.com/adblockplus
说到读代码这可真是一门学问啊,由于我们目前做的这个是java的项目,看到有Android的源代码,所以就选择看Android的,结果是各种蒙啊,不知道是不是手机的原因,我的手机对Adblock plus不怎么感冒,但是也是硬着头皮把他这个Android的版本的大致的构造弄清楚了,不知道是不是作者懒啊,这个Android版本的源代码,最上层自然而然的用了java源代码,读了半天发现只是做了一个架子,没有什么具体的实现,java的下一层是C++代码,然后又花了半天读c++代码,最后苦逼的发现竟然还有下一层!js!!!我哭啊,后来又读js代码,感觉虽然开源,但是这个代码的实现真的是好高端,用了js里面类似于继承的结构,套了五六层。。。一层一层的查看,(建议读源代码的同人不要把所有的代码都读了,因为很有可能读半天发现没有用,先过滤一遍,看看都是干什么的再读,我那时候就是实在搞不清楚模块了,索性又下了一个Firefox的,通过源代码运行软件,然后再通过软件看源代码执行情况弄清楚软件的逻辑结构的,感觉这个方法挺好用的,有需要的可以试试),最后读到关键地方发现其实就是3类规则:①这部分规则大多数是屏蔽那些小广告插件的,当然也有链接的,但是大部分都是通过后缀来区别的,后缀好像都是高于8个的那种,通过在后缀中选取8个连续的字符当键,然后把对应的规则写到哈希表的值里面,查的时候算法很简单,就是连续抽取8个连续的字符串查找哈希表;②这部分一般是使用者自己书写的规则,就是在规则前面加上@@…………,表示遇到对应这些规则的链接不阻挡,至于查询算法同上。③这部分就是通配符匹配(我所需要的就是这部分的算法),这部分主要处理的就是类似于域名的通配符规则,例如:www.baidu.*.com 然后对这些规则通过域名切割,切割成一个一个subdomain:www ,baidu ,com ,subdomain是“ * ”的或者含有的都删除,然后再把第一个域名还有最后一个域名去掉,我给的例子只剩下一个“baidu”然后用这个字符串当做键,查询对应的哈希表中是否已经含有,如果含有查看其一个键对应的N个值得N是否已经达到上限,如果没有直接写入,如果达到上限了就用下一个字符串当做键继续按照上述查找(所给的例子通过域名切割之后只剩下一个字符串当做键,正常情况下可能有多于一个的情况),如果所得到的字符串当做键所对应的N都已经达到上限,那就把这条规则写入特殊规则处理的地方,这个可以因人而异,我的处理是写入一个动态数组到时候遍历查找(Adblock plus和我的实现差不多,区别是它使用js实现这些的)。
Adblock plus还涉及到一个算法是Rabin-Karp算法,这个是两个字符串匹配的算法,由于我用的是java语言,所以这部分就没有做深入研究,不过还是看了一下,这个算法是判断字符串A中是否有字符串B的一个算法,不过由于js或者别的字符串匹配效率不高的语言处理可能复杂度比较高,所以这个算法是通过把字符串匹配转换成整数比较大小的形式来判断的。例如ABCDEFGHJ转换成123456789,然后进行比较。例如字符串a为“ABCFE”转换成整数12365 字符串b为“BC”转换成23,匹配的时候依次从第一个整数最低位取得两位数和23比较,当然,我的这种举例是最简单最基础的形式,这个算法还有改进的空间就是通过用记忆算法来实现,具体就补多少了。
由于我跟的这个项目也是开源的,所以也不怕泄露什么秘密,下面会附上我写的代码,由于还没有经过修改,如有错误请提出来谢谢了,还有如果哪位也想研究一下Adblock plus的话如果遇到什么问题可以邮箱给我,一起探讨,一起进步! 邮箱:[email protected] 嘿嘿 qq邮箱,如果加我的话请写明备注!!我可是不轻易加人的。
package citexplore.foundation.util; import java.util.*; /** * Url过滤工具类。 * * @author Xu, Zewen; Zhang, Yin */ public class Filter { // **************** 公开变量 // **************** 私有变量 /** * 常规规则集 */ private static HashMap<String,ArrayList<String>> subDomainToRules=new HashMap<String,ArrayList<String>>(); /** * 特殊规则集 */ private static ArrayList<String> specialRules=new ArrayList<>(); /** * 常规规则集subDomain对应的规则上限 */ private static int maxNumOfRules=100; // **************** 继承方法 // **************** 公开方法 /** * 查看url是否被包含在规则集 * @param url * @return */ public static boolean Check(String url){ if(Check(specialRules,url)==true){ return true; } else{ ArrayList<String> subDomain=UrlToSubDomain(url); for(int i=0;i<subDomain.size();i++){ if(subDomainToRules.containsKey(subDomain.get(i))){ if(Check(subDomainToRules.get(subDomain.get(i)),url)) return true; } } } return false; } /** * 插入规则 * @param rule * @return */ public static boolean InsertRules(String rule){ rule=UrlToDomain(rule); ArrayList<String> subDomains=UrlToSubDomain(rule); if(subDomains.size()==0){ return false; } else{ int i; for(i=0;i<subDomains.size();i++){ if(subDomainToRules.containsKey(subDomains.get(i))==false){ subDomainToRules.put(subDomains.get(i),new ArrayList<String>()); subDomainToRules.get(subDomains.get(i)).add(rule); return true; } if(subDomainToRules.get(subDomains.get(i)).contains(rule)) return true; if(subDomainToRules.get(subDomains.get(i)).size()>maxNumOfRules) continue; subDomainToRules.get(subDomains.get(i)).add(rule); return true; } if(i==subDomains.size()) specialRules.add(rule); } return true; } // **************** 私有方法 /** * 查看url是否被包含在rules中 * @param rules * @param url * @return */ private static boolean Check(ArrayList<String> rules,String url){ String domain=UrlToDomain(url); String[] subDomains=domain.split("\\."); for(int i=0;i<rules.size();i++){ String[] subDomainOfRules=rules.get(i).split("\\."); if(subDomainOfRules.length!=subDomains.length) continue; int j; for(j=0;j<subDomains.length;j++){ if(subDomainOfRules[j].equals(subDomains[j])||subDomainOfRules[j].equals("*")) continue; else{ break; } } if(j==subDomains.length) return true; } return false; } /** * 分离url * @param url * @return domain */ private static String UrlToDomain(String url){ int domainOfStart=0,domainOfEnd=url.length(); if(url.indexOf("http")!=-1){ domainOfStart=url.indexOf("//")+2; } if(url.indexOf("/",domainOfStart)!=-1){ domainOfEnd=url.indexOf("/",domainOfStart); } String domain=url.substring(domainOfStart, domainOfEnd); return domain; } /** * 切割domain * @param url * @return */ private static ArrayList<String> UrlToSubDomain(String url){ ArrayList<String> result=new ArrayList<>(); int domainOfStart=0,domainOfEnd=url.length(); if(url.indexOf("http")!=-1){ domainOfStart=url.indexOf("//")+2; } if(url.indexOf("/",domainOfStart)!=-1){ domainOfEnd=url.indexOf("/",domainOfStart); } String domain=url.substring(domainOfStart, domainOfEnd); String[]subDomains=domain.split("\\."); for(int i=1;i<subDomains.length-1;i++){ if(!subDomains[i].contains("*")) result.add(subDomains[i]); } return result; } }