はじめに
google-diff-match-patchというライブラリを使って、文章比較を試してみます。
「文章を比較する」と聞くとなにやら大変そうな気がしますが、テキストを関数に入れて結果を加工するだけです。
この記事では、diffそのものの仕組みやアルゴリズムについては触れていませんのでご了承ください
構成
サーバー:Python + Django(+ Django REST Framework)
フロント:React
今回diffを検出するために使う「google-diff-match-patch」には、JavaScript版もあるので、実際はフロントエンドだけでも完結することができますが、今回はサーバーから比較結果を返す形にしました。
ツールの動作フロー
- フロントからサーバーに2つのTextをPostする
- サーバーは2つのTextのdiffを取り、結果をフロントに返す
- フロントは結果を見てTextの差分を画面に表示する
画面構成
WinMergeのように左右にテキスト領域を配置し、左に「編集前」、右に「編集後」のテキストを表示するようにします。
削除された文字列は背景色を赤に、追加された文字列は背景色を緑に塗ることにします。
実装
サーバー
APIでtext1とtext2という二つの文字列を受けて、結果をResponseで返します。
まずRequestの窓口になるviews.pyです。
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import DiffSerializer
from .logics.difflogic import DiffLogic
from logging import getLogger
logger = getLogger(__name__)
class DiffView(APIView):
"""
View to get diff.
"""
def post(self, request, format=None):
serializer = DiffSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
result = DiffLogic.get_diff(serializer.data)
return Response(result)
次にPostされたデータをシリアライズするSerializer。
from rest_framework import serializers
class DiffSerializer(serializers.Serializer):
text1 = serializers.CharField()
text2 = serializers.CharField()
最後にdiff_match_patchを呼び出すlogic部分。
from textanalyzer.lib import diff_match_patch
class DiffLogic:
@staticmethod
def get_diff(diff_data):
logic = diff_match_patch.diff_match_patch()
return logic.diff_main(diff_data['text1'], diff_data['text2'])
RequestのBody部とResponseはこんな感じになります。
{
"text1":"彼は黒い帽子をアイマスク代わりにして、仕事中だというのにぐうぐうといびきをかいて寝ていた。朝からのいらだちもあいまって、私はついに我慢ができなくなった。資本主義は死んだ。私が殺したのだ。",
"text2":"彼は黒い帽子を目隠しにして、真っ昼間から気持ちよさそうにぐうぐうといびきをかいて寝ている。朝からのいらだちもあいまって、私はついに我慢できなくなった。八つ当たりだと言いたければ言えばいい。資本主義は死んだ。私が殺したのだ。"
}
[[0,"彼は黒い帽子を"],[-1,"アイマスク代わり"],[1,"目隠し"],[0,"にして、"],[-1,"仕事中だとい"],[1,"真っ昼間から気持ちよさそ"],[0,"う"],[-1,"の"],[0,"にぐうぐうといびきをかいて寝てい"],[-1,"た"],[1,"る"],[0,"。朝からのいらだちもあいまって、私はついに我慢"],[-1,"が"],[0,"できなくなった"],[1,"。八つ当たりだと言いたければ言えばいい"],[0,"。資本主義は死んだ。私が殺したのだ。"]]
フロント
サーバーにRequestを投げて、受け取ったResponseを良い感じに表示してあげるだけです。
まずは、Postして結果を「編集前」「編集後」に分けて格納する一連の処理の部分です。
const text1 = "彼は黒い帽子をアイマスク代わりにして、仕事中だというのにぐうぐうといびきをかいて寝ていた。朝からのいらだちもあいまって、私はついに我慢ができなくなった。資本主義は死んだ。私が殺したのだ。";
const text2 = "彼は黒い帽子を目隠しにして、真っ昼間から気持ちよさそうにぐうぐうといびきをかいて寝ている。朝からのいらだちもあいまって、私はついに我慢できなくなった。八つ当たりだと言いたければ言えばいい。資本主義は死んだ。私が殺したのだ。";
fetch(`http://localhost:8000/text-analyze/diff/`, {
method: 'POST',
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify({
text1: text1,
text2: text2,
}),
}).then(
res => res.json()
).then( res => {
const newAnotatedTexts1 = [];
const newAnotatedTexts2 = [];
res.forEach(element => {
const tag = element[0];
const text = element[1];
if (tag === 0) {
newAnotatedTexts1.push({
tag: 'normal',
string: text,
});
newAnotatedTexts2.push({
tag: 'normal',
string: text,
});
} else if (tag === -1) {
newAnotatedTexts1.push({
tag: 'deleted',
string: text,
});
} else if (tag === 1) {
newAnotatedTexts2.push({
tag: 'added',
string: text,
});
}
this.setState( {
anotatedTexts1: newAnotatedTexts1,
anotatedTexts2: newAnotatedTexts2,
});
});
});
「newAnotatedTexts1」という配列に結果を格納する部分について見てみましょう。
サーバーからのResponseはこんな感じになっています。
[[0,"彼は黒い帽子を"],[-1,"アイマスク代わり"],[1,"目隠し"] ...]
各配列の頭の「0」「-1」「1」が気になりますが、以下のような意味を持っています。
-1:比較元の文章にしか存在しない文字列(削除された文字列)
0: どちらの文章にも存在する文字列
1: 比較先の文章にしか存在しない文字列(追加された文字列)
なので、0の場合は編集前と編集後の配列に「通常文字列」として格納します。
if (tag === 0) {
newAnotatedTexts1.push({
tag: 'normal',
string: text,
});
newAnotatedTexts2.push({
tag: 'normal',
string: text,
});
}
同様に-1の場合は編集前の配列に「削除された文字列」
1の場合は編集後の配列に「追加された文字列」として格納します。
else if (tag === -1) {
newAnotatedTexts1.push({
tag: 'deleted',
string: text,
});
} else if (tag === 1) {
newAnotatedTexts2.push({
tag: 'added',
string: text,
});
}
後は格納した順に表示するだけです。
renderAnotatedText(anotated) {
switch (anotated.tag) {
case 'deleted':
return <span className="deleted">{anotated.string}</span>;
case 'added':
return <span className="added">{anotated.string}</span>;
case 'normal':
return anotated.string;
default:
break;
}
return "";
}
CSSはこんな感じ。
.deleted {
display: inline-block;
margin-top: -1px;
text-decoration: none;
background-color: #ffb6ba;
border-radius: .2em;
}
.added {
display: inline-block;
margin-top: -1px;
text-decoration: none;
background-color: #97f295;
border-radius: .2em;
}
完成
まとめ
google-diff-match-patchというライブラリを使って、かんたんに文章比較を行うことができました。
ここまで読んでいただければわかるとおり難しいことは何もやっていなくて、Textを二つ入れて返ってきた結果を加工してみただけです。
ちょっと時間を割くだけで形にすることができるので、プロダクトのちょっとした機能として検討してみるのはいかがでしょうか。