基于文本相似度算法,分析 Vue 是抄出来的框架吗?

17,476 阅读8分钟

本周一篇指摘 Vue 抄袭 Angular 的文章一石激起千层浪。为此,笔者作为中立吃瓜的 React 用户,分析了 13 个主流前端框架版本上万个变量的命名风格,应用自然语言处理中的文本相似度算法进行了分析,以对这一论点的有效性做出客观的评价。

思路

在分析书籍抄袭、论文查重等场景下,使用算法比较文本相似度的方法是一种有效的技术手段。那么,我们如何通过这一手段,来分析源码层面的抄袭呢?

在对比形如 我喜欢写代码,不喜欢撕逼我不喜欢撕逼,喜欢写代码 的两个句子相似度时,大致的思路是首先分词,而后计算出词频,再将词频向量化,最后比较两个高维向量的夹角,夹角越小则越相似。

在【Vue 是否抄袭了 Angular】这一场景下,我们分析的对象从句子变成了代码。这时主要的区别是这两点:

  1. 代码是高度结构化的文本,分词已经通过词法分析器完成了。
  2. 某种编程语言的代码中,充斥着大量该语言的关键字。如 varfunction 的关键字,这些关键字的重复与相似度无关。

而对于是否抄袭与相似度的关系,我们给出这几个假设:

  1. 解决同样的开放性问题时,独立编写、不存在抄袭的代码,以变量名为代表的编码风格通常有巨大的差别,此时相似度是低的
  2. 存在较大规模抄袭的代码,类似于同一个框架,未经大规模重构的不同版本代码。此时编码风格是类似的,此时相似度是高的
  3. 前后调换模块声明顺序,不影响相似度

给定这几个前提后,我们可以确定出这样的分析策略:

  1. 输入各大框架未经压缩的源码,解析出其语法树。
  2. 舍弃语法树中无关部分,提取出其变量声明以代表其编码风格。
  3. 使用文本相似度算法计算变量名间的相似度,分析结论。

变量名提取

在通过 Webpack 引用框架依赖时,通常导入的都是打包成单一文件且未经混淆的框架源码。这是一个非常好的特性。笔者编写了一个简单的 Webpack Loader 以在这个过程中实现变量名的提取:

// loader/index.js
// 为 loader 传入的 content 即为 JS 源码
module.exports = function (content) {
  return demo(content)
}

demo 函数中获得框架源码后,解析语法树也不是一个困难的问题了。通过 acorn 这一 Parser 我们就能做到:

function demo (content) {
  const ast = acorn.parse(content, { sourceType: 'module' })
  walk.simple(ast, {
    // 在 walk 遍历时,抽取全部变量声明语句中的变量名
    VariableDeclaration (node) {
      const name = node.declarations[0].id.name + '\n'
      fs.appendFileSync(resolve('./result.txt'), name)
    }
  })
  return content
}

这时候我们就能在 result.txt 内获得一个前端框架中的全部变量名了,形如:

p
i
resolved
c
segs
i
...

这都是什么乱七八糟的…这时候我们获得的文本并没有经过初步的处理,我们真正感兴趣的是各个框架变量名的词频。词频的计算是一道不错的面试题,不过在这里我们直接通过 Wordclouds 的服务来实现这一步。这一步中还包括基本的清洗,以去除 i / a / b 这些无意义的变量名。我们的结果是形如这样的格式:

29    value
19    arg
18    result
16    key
14    index
...

以上就是 React / Vue / Angular 三大框架中某一个的 Top 5 变量名,猜猜是哪一个?好吧这几个变量名都十分烂大街…暂时看不出什么端倪。让我们继续做相似度比较吧,答案在后面揭晓。

相似度算法

我们在上文中,实际上已经获得了这样的对象:

const a = {
  'foo': 5,
  'bar': 4,
  'baz': 3
}
const b = {
  'foo': 4,
  'bar': 6,
  'baz': 0
}

我们可以认为,每个变量名是一个独立的维度,每个框架中存在的所有类型变量名组成一个高维空间的向量。从而,我们的问题就简化为了如何比较 a 与 b 这两个向量的相似程度。在此引用阮一峰老师的介绍

我们可以把它们想象成空间中的两条线段,都是从原点([0, 0, ...])出发,指向不同的方向。两条线段之间形成一个夹角,如果夹角为0度,意味着方向相同、线段重合;如果夹角为90度,意味着形成直角,方向完全不相似;如果夹角为180度,意味着方向正好相反。因此,我们可以通过夹角的大小,来判断向量的相似程度。夹角越小,就代表越相似。

theta-1
theta-1

假定 a 向量是 [x1, y1],b向量是 [x2, y2],那么可以将余弦定理改写成下面的形式,计算出的 cosθ 代表相似度:

theta-2
theta-2

推广到高维向量的一般情形:

theta-3
theta-3

根据算法编写出简化的示例代码:

function getTheta () {
  let x = 0
  Object.keys(dictAll).forEach(key => {
    if (dictA[key] && dictB[key]) x += dictA[key] * dictB[key]
  })
  let yA = getY(dictA)
  let yB = getY(dictB)
  const result = x / (yA * yB)
  console.log(result)
}

最后运行我们的分析算法处理上一步的变量名即可:

➜ node analyse vue@2.4.1 vue@2.4.2
0.9436438155995188

实验结果与总结

一系列铺垫以后,终于到了检验真理的时候了。我们首先基于【相似版本相似度高】的假设,验证 Vue 是否符合这一假设:

➜ node analyse vue@2.4.1 vue@2.4.2
0.9436438155995188

可以看到,目前最新的 vue 2.4.2 与 2.4.1 之间,确实存在着很高的相似度。接下来比较 vue 最新版与 2.0.0 同一个 Major 版本之间的相似度:

➜ node analyse vue@2.0.0 vue@2.4.2
0.8838059164881868

相似度有所降低,说明最新版比起去年的 V2,已经有了不小的改动了。再来比较 V2 与 V1 系列的相似度:

➜ node analyse vue@2.0.0 vue@1.0.28
0.5883193867742227

相似度明显降低,显然重构之言非虚。最后比较 Vue 的最新版与第一个版本:

➜ node analyse vue@2.4.2 vue@0.6.0
0.4590386014371645

这是 Vue 家族中最低的相似度,也达到了 0.45 的水平。接下来是正戏,比较 Vue 和 Angular 的最新版:

➜ node analyse vue@2.4.2 angular@4.3.3
0.19322280449484375

区区 0.19 的相似度!好吧,Angular 最新版也是重构过的,我们不妨直接比较最早【照着 Angular 抄的】的 Vue 和 Angular 1.x 系列:

➜ node analyse vue@0.6.0 angular@1.2.32
0.294527560626686

这个相似度也大幅低于 Vue 全系列纵向对比的相似度!为了更有效地对比,我们让隔壁 React 躺个枪(未加版本号代表最新版):

➜ node analyse vue@2.4.2 react 
0.27592736925848194

0.27 与 0.29 的对比,说明即便是最早阶段(与 Angular 相似度最高)的 Vue,相似度也仅仅相当于现在的 Vue 和 React 而已!为了保证公平,我们让 jQuery 也来凑个热闹:

➜ node analyse jquery angular@1.2.32
0.2508302720623658

这也是不到 0.3 的相似度,据此我们甚至可以得出一个大胆的结论:Vue 和 Angular 的相似度,和 Angular 与 jQuery 之间的相似度接近!没有人会认为 jQuery 与 Angular 之间存在抄袭吧?

当然,Vue 和 Angular 的相似度是客观存在的。我们在前端领域,可以找到另一对这样的例子:jQuery VS Zepto,它们之间的相似度如何呢?

➜ node analyse jquery zepto
0.25994377334635854

这个相似度和 Angular VS jQuery 几乎相同,这说明即便设计理念相近,具体实现不同的原创框架之间,相似度也是很低的。Vue VS Angular 也完全符合这一结论。

hmmm 目前我们的论据已经比较充分了。最后,我们比较一种情形:设计理念完全不同的原创框架之间,相似度如何?我们拉出 jQuery 和 React:

➜ node analyse jquery react        
0.1007248324385447

全场最低相似度…所以我们可以理解 jQuery 时代的前端转向 React 时有多么不习惯了吧😂

到此为止,我们的结论有:

  • Vue 系列迭代间相似度较高。
  • 即便是最早的 Vue,与经典 Angular 的相似度也很低。
  • 最新 Vue 与最新 Angular 之间,相似度更低,说明二者的发展道路早已更加独立。
  • 即便设计理念相近,具体实现不同的原创框架之间,相似度也很低。
  • React 与 jQuery 的相似度特别特别低(离题了)。

据此,笔者有理由认为【Vue 抄袭了 Angular】的论点是站不住脚的。

本文的实验数据托管在 Github 上,欢迎感兴趣的同学验证并改进这些结论。最后,框架毕竟只是工具,相互撕逼并不利于社区的发展。引用我司 Boss 的观点:【一流的人做事,二流的人去评论,三流的人去评论别人的评论】,希望大家能把口水战的时间放在更务实的事情上,推动技术水平、社区氛围和平均工资的上升……