2016年1月31日 星期日

Node.js 的中文與英文髒話過濾

在聊天室之類的應用之中,我們時常會需要使用到髒話過濾的功能。這篇文章簡單描述一下個人在後端過濾中文與英文髒話的方法。

Mikey Wilson, 2002

檢測髒話


首先,讓我們試著判斷一個句子是否含有任何髒話。注意中文與英文的判斷方式略有不同,底下將分開討論。

檢測中文髒話


對於中文,我們的目標是從一個完整的句子當中,找出是否有特定的髒話關鍵字。以 javascript 來講,我們可以使用 String 的 indexOf() 方法,判斷某一字串是否含有另外一個字串。範例程式碼如下:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var chineseList = ["笨", "胖", "王八"];
var isProfane = function(string){
  for (var i = 0; i < chineseList.length; i++) {
    if (string.indexOf(chineseList[i]) > -1) {
      return true;
    }
  }
  return false;
};

var string1 = "王老先生有八塊地";
var string2 = "你媽超胖,她的肚臍比她早十五分鐘到家";
console.log(isProfane(string1));  // false
console.log(isProfane(string2));  // true

String.indexOf 方法會在一個字串中搜尋另一個字串的所在位置。如果找不到,則會回傳 -1 。因此判斷中文髒話的方法,就是列出所有你要搜尋的髒話,然後逐一放到 indexOf 中去搜尋。只要有任何一個關鍵字出現,就可以認定此字串中含有髒話。

檢測英文髒話


英文的判斷方法就不同了。因為英文單字是由多個字母組成,有時候一個髒話單字可能被包含在另一個正常的單字當中。比方說, assassin 這個單字雖然含有髒話 ass,但我們並不應該把 assassin 當成髒話。

所以,判斷英文髒話必須以單字為單位做比對,髒話跟比較對象的單字必須要完全相等才行。我們可以用下面的程式碼,先將句子拆解成單字後,再逐一與髒話列表做全文比對:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var englishList = ["ass", "bitch", "cunt"];

var isProfane = function(string){
  var words = string.split(" ");
  for (var i = 0; i < words.length; i++) {
    var word = words[i].toLowerCase();
    if (englishList.indexOf(word) > -1) {
      return true;
    }
  }
  return false;
};

var string1 = "We work in the dark to serve the light. We are assassins.";
var string2 = "I'm CEO, Bitch";
console.log(isProfane(string1));  // false
console.log(isProfane(string2));  // true

在上面的程式碼中,我們先把字串以空白分割成單字,存在陣列 words 當中。再使用 Array.indexOf 方法,判斷單字是否在髒話列表陣列當中。這跟前面用的 String.indexOf 不一樣,它是在陣列中找完全相同的元素,所以可以用來做我們需要的全文比對。

髒話消音


另一個常用的功能是只把句子中的髒話部分消除,或是替換成其他符號,保留其餘部分的文字。底下一樣展示中文與英文的不同做法。

中文髒話消音


我們可以很簡單的用 javascript 原生的 String.replace 方法,把髒話部分替換掉即可。程式碼範例如下:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var chineseList = ["笨", "胖", "王八"];
var placeHolder = "*";

var replaceWord = function(string, target){
  var t = "";
  for(var i = 0; i < target.length; i++){
    t += placeHolder;
  }
  return string.replace(new RegExp(target, 'g'), t);
};

var clean = function(string){
  for (var i = 0; i < chineseList.length; i++) {
    if (string.indexOf(chineseList[i]) > -1) {
      string = replaceWord(string, chineseList[i]);
    }
  }
  return string;
};

var string1 = "你媽超胖,她的肚臍比她早十五分鐘到家";
var string2 = "你王八蛋,你們全家都王八蛋";
console.log(clean(string1));  // 你媽超*,她的肚臍比她早十五分鐘到家
console.log(clean(string2));  // 你**蛋,你們全家都**蛋

要過濾的句子先進入到 clean 函式中,會先用與前面相同的檢測方式,判斷句子中是否含有中文髒話。如果有,句子會進入 replaceWord 函式進行處理。

在 replaceWord 函式中,我們先建立一個跟原先髒話相同長度,僅由星號 * 所組成的字串。最後呼叫 string.replace 來將句子中的髒話以星號字串取代掉。由於同一句髒話可能在一個句子中出現不只一次,因此在 replace 中的正規表達式要加上 g 這參數,確保整個句子都會被比對一遍。

英文髒話消音


比對英文時,還是要先把句子拆開成多個單字,再逐一比對。範例程式碼如下:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var englishList = ["ass", "bitch", "cunt"];
var placeHolder = "*";

var cleanWord = function(word){
  var t = "";
  for(var i = 0; i < word.length; i++){
    t += placeHolder;
  }
  return t;
};

var clean = function(){
  var words = string.split(" ");
  for (i = 0; i < words.length; i++) {
    var word = words[i].toLowerCase();
    if (englishList.indexOf(word) > -1) {
      words[i] = cleanWord(words[i]);
    }
  }
  return words.join(' ');
};

var string1 = "We work in the dark to serve the light. We are assassins.";
var string2 = "I'm CEO, Bitch";
console.log(clean(string1));  // We work in the dark to serve the light. We are assassins.
console.log(clean(string2));  // I'm CEO, *****


要過濾的句子先進入到 clean 函式中,會先用與前面相同的方式,把句子拆開成多個單字,再逐一判斷此單字是否為英文髒話。如果是,單字會進入 cleanWord 函式進行處理。

在 cleanWord 函式中,我們建立一個跟原先髒話相同長度,僅由星號 * 所組成的字串,就直接回傳,取代原先單字。

最後,我們把所有被拆解開的單字,利用 Array.join 方法,重新組回一個句子即可。


變種


前面的中文髒話判斷方式有一個比較明顯的缺點,在於它無法分辨以標點、空白或特殊字元分開的髒話。比方說,「王八蛋」是一個髒話,但「王 八 蛋」就無法被判斷成髒話。如果想要連這樣的字串都檢測出來,可以先用 String.replace 方法,把所有不是中文字的符號從句子中移除:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var chineseList = ["笨", "胖", "王八蛋"];
var isProfane = function(string){
  for (var i = 0; i < chineseList.length; i++) {
    if (string.indexOf(chineseList[i]) > -1) {
      return true;
    }
  }
  return false;
};

var string = "你 這 王 八 蛋";
console.log(isProfane(string));  // false
console.log(isProfane(string.replace(/[^\u4e00-\u9fff]/g, "")));  // true

正規表達式 /[^\u4e00-\u9fff]/g 會比對所有不是中文的字母或符號,然後 String.replace 就會把它們以空字串取代,也就是刪除的意思,這樣句子中的空白就全部消失了。

英文也可以如法炮製,把英文字母、數字以外的符號都先移除。可以使用 /[^a-zA-Z0-9]/g 做為正規表達式。

不過,這種方法比較難以用在消音的功能上面,畢竟它會破壞掉原本句子中的符號,而且就算檢測到髒話,也不容易找出要消音的範圍。我建議如果要在聊天室使用這類的變種,不如就不做消音了,改成一旦偵測到髒話就將整句移除或禁止發言,較為省事。

結論


本文介紹了我在 Node.js 環境中實做中英文髒話偵測與過濾的方法。藉由 javascript 的幾種原生方法,我們可以很容易的做出髒話過濾器,並得以將它用在聊天室等各種應用程式中。

整份程式碼可以到我的 GitHub 觀看,之後應該會將它打包發布到 npm 上。有任何疑問、建議或希望增加的髒話,也歡迎隨時在上面提出。

2016年2月1日:
此 Module 已發布到 npm 上,請到 https://www.npmjs.com/package/bad-words-chinese 上觀看使用說明。

參考資料


bad-words, by webmech 一個英文的髒話過濾模組。