基於文字相似度演算法,分析 Vue 是抄出來的框架嗎?

doodlewind發表於2017-08-05

本週一篇指摘 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 的觀點:【一流的人做事,二流的人去評論,三流的人去評論別人的評論】,希望大家能把口水戰的時間放在更務實的事情上,推動技術水平、社群氛圍和平均工資的上升……

相關文章