我认为我有一个非常简单快捷的方法来做到这一点。
我最近编写并发布了用于计算树编辑距离近似值的 jqgram,它使用易于使用的 API 来比较类似 DOM 的结构、JSON 结构或您自己设计的树结构:
https://github.com/hoonto/jqgram
基于原始论文: http ://www.vldb2005.org/program/paper/wed/p301-augsten.pdf
最初移植自 Python 实现:https ://github.com/Sycondaman/PyGram
jqgram树编辑距离近似模块实现了服务器端和浏览器端应用的PQ-Gram算法;O(n log n) 时间和 O(n) 空间性能,其中 n 是节点数。PQ-Gram 近似比通过 Zhang & Shasha、Klein、Guha 或其他人获得真正的编辑距离要快得多,他们提供的真正编辑距离算法最多执行 O(n^2) 或 O(n^3),具体取决于你看的是哪种算法。
这是我将如何使用 jqgram 来应对您的特定挑战的开始,我直接从 github 上的 README 中取出。要回答您的问题之一,您可以在 jQuery 等库(如下所示)中使用 DOM 作为其自己的树结构,或者复制它或从 Cheerio 或其底层 HTML 解析库或其任何组合中的 html 字符串生成一个(jqgram 为您提供了这种灵活性)。此处的示例将当前页面中的 DOM 与从字符串生成的 Cheerio 表示进行比较 - 您已知的参考。
// This could probably be optimized significantly, but is a real-world
// example of how to use tree edit distance in the browser.
// For cheerio, you'll have to browserify,
// which requires some fiddling around
// due to cheerio's dynamically generated
// require's (good grief) that browserify
// does not see due to the static nature
// of its code analysis (dynamic off-line
// analysis is hard, but doable).
//
// Ultimately, the goal is to end up with
// something like this in the browser:
var cheerio = require('./lib/cheerio');
// But you could use jQuery for both sides of this comparison in which case your
// lfn and cfn callback functions become the same for both roots.
// The easy part, jqgram:
var jq = require("../jqgram").jqgram;
// Make a cheerio DOM:
var html = '<body><div id="a"><div class="c d"><span>Irrelevent text</span></div></div></body>';
var cheeriodom = cheerio.load(html, {
ignoreWhitespace: false,
lowerCaseTags: true
});
// For ease, lets assume you have jQuery laoded:
var realdom = $('body');
// The lfn and cfn functions allow you to specify
// how labels and children should be defined:
jq.distance({
root: cheeriodom,
lfn: function(node){
// We don't have to lowercase this because we already
// asked cheerio to do that for us above (lowerCaseTags).
return node.name;
},
cfn: function(node){
// Cheerio maintains attributes in the attribs array:
// We're going to put id's and classes in as children
// of nodes in our cheerio tree
var retarr = [];
if(!! node.attribs && !! node.attribs.class){
retarr = retarr.concat(node.attribs.class.split(' '));
}
if(!! node.attribs && !! node.attribs.id){
retarr.push(node.attribs.id);
}
retarr = retarr.concat(node.children);
return retarr;
}
},{
root: realdom,
lfn: function(node){
return node.nodeName.toLowerCase();
},
cfn: function(node){
var retarr = [];
if(!! node.attributes && !! node.attributes.class && !! node.attributes.class.nodeValue){
retarr = retarr.concat(node.attributes.class.nodeValue.split(' '));
}
if(!! node.attributes && !! node.attributes.id && !! node.attributes.id.nodeValue) {
retarr.push(node.attributes.id.nodeValue);
}
for(var i=0; i<node.children.length; ++i){
retarr.push(node.children[i]);
}
return retarr;
}
},{ p:2, q:3, depth:10 },
function(result) {
console.log(result.distance);
});
请注意,lfn 和 cfn 参数指定每棵树应如何独立确定每个树根的节点标签名称和子数组,以便您可以将 DOM 与 JSON 对象或其他使用不同语义来指定子节点和什么是节点标签。另请注意,在此示例中,我利用了 DOM 实体类属性,将其拆分为各个类,并将 DOM 节点本身的 id 作为节点的直接子节点,以便更清楚地了解两棵树是非常相似还是非常不同. 您可以扩展它以包含您自己的属性。或者,您也可以修改每棵树的 lfn 函数以在标签中包含 id,例如“tagname:id”
所以总结一下,你需要做的就是提供这些 lfn 和 cfn 函数以及每个根,jqgram 将完成其余的工作,调用 lfn 和 cfn 函数来构建树。
jqgram 实现的 PQ-Gram 算法将编辑距离作为一个介于 0 和 1 之间的数字提供,并且应该注意,零值并不一定表示绝对相等,只是两棵树非常相似。如果您需要继续确定 jqgram 确定的两个非常相似的树是否确实相同,您可以使用 Zhang 和 Shasha,但是使用 jqgram 获取指标将为您节省大量计算,这在客户端浏览器中变得非常重要终端用户性能显然很重要的应用程序。
希望有帮助!