Text_Diffでテキストの差分を取る
データの修正時に変更箇所が差分表示できたらいいよねー、という話が出ていたのでPHPでできるか試してみた。
一年ぐらい前にPEARのText_Diffというモジュールをちらっと見たことがあったのを思い出して使ってみた。
まー、なかったとしても裏でdiffコマンド叩いて出力を加工するってのでもそんなに難しくなさそうだけど。
用意したデータ
text1.txt
line 1 line 2 line 3 line 4 line 5 line 6 line 7 line 8 line 9 line 10
text2.txt
line 1 line 2 line 4 line 5 line 6.1 line 6.5 line 7 line 8 line 9 line 9.2 line 10
text1.txtからtext2.txtへの変更点は、
- "line 3"を削除。
- "line 6"を"line 6.1"にして、更に"line 6.5"を追加。
- "line 9.2"を追加。
プログラム
<?php require_once 'Text/Diff.php'; if ($argc < 3) { fprintf(STDERR, "USAGE: diff.php <file1> <file2>\n\n"); exit(1); } if (!is_readable($argv[1])) { fprintf(STDERR, "$argv[1] is not a readable file\n"); exit(1); } if (!is_readable($argv[2])) { fprintf(STDERR, "$argv[2] is not a readable file\n"); exit(1); } // ファイルを行単位の配列にして読み込む $file1 = file($argv[1]); $file2 = file($argv[2]); // Text_Diffの生成 // engineはautoにしておけばベストなものを選んでくれるらしい $diff = new Text_Diff('auto', array($file1, $file2)); // diff結果を取得 $diffResult = $diff->getDiff(); //var_dump($diffResult); // diff結果がText_Diff_Opのサブクラスの配列で格納されている // copyは差異がない部分、addは追加、deleteは削除、changeは変更(追加&削除) foreach ($diffResult as $index => $op) { if ($op instanceof Text_Diff_Op_copy) { // 差異がない箇所 // origもfinalも同じ内容が入っている foreach ($op->orig as $line) { printf(" %-35s | %s\n", $line, $line); } } else if ($op instanceof Text_Diff_Op_add) { // 追加された箇所 // origは空で、finalに追加された内容が入っている foreach ($op->final as $line) { printf(" %-35s |+ %s\n", '', $line); } } else if ($op instanceof Text_Diff_Op_delete) { // 削除された箇所 // origに削除された内容が入っていて、finalは空 foreach ($op->orig as $line) { printf("- %-35s |\n", $line); } } else if ($op instanceof Text_Diff_Op_change) { // 変更された箇所 // origが変更前の内容で、finalが変更後の内容 // 削除+追加という判定っぽいので、origとfinalで行数が異なるケースもある $origSize = count($op->orig); $finalSize = count($op->final); $max = max($origSize, $finalSize); for ($i = 0; $i < $max; $i++) { $origLine = ($i < $origSize) ? $op->orig[$i] : null; $finalLine = ($i < $finalSize) ? $op->final[$i] : ''; $leftMarker = isset($origLine) ? '-' : ' '; printf("%s %-35s |+ %s\n", $leftMarker, $origLine, $finalLine); } } else { printf("WARNING: unknown operation\n"); } } ?>
実行結果
$ php diff.php text1.txt text2.txt line 1 | line 1 line 2 | line 2 - line 3 | line 4 | line 4 line 5 | line 5 - line 6 |+ line 6.1 |+ line 6.5 line 7 | line 7 line 8 | line 8 line 9 | line 9 |+ line 9.2 line 10 | line 10
ちなみにコメントアウトしてあるgetDiffをvar_dumpしたものはこんな感じ。
array(7) { [0]=> object(Text_Diff_Op_copy)#3 (2) { ["orig"]=> array(2) { [0]=> string(6) "line 1" [1]=> string(6) "line 2" } ["final"]=> array(2) { [0]=> string(6) "line 1" [1]=> string(6) "line 2" } } [1]=> object(Text_Diff_Op_delete)#4 (2) { ["orig"]=> array(1) { [0]=> string(6) "line 3" } ["final"]=> bool(false) } [2]=> object(Text_Diff_Op_copy)#5 (2) { ["orig"]=> array(2) { [0]=> string(6) "line 4" [1]=> string(6) "line 5" } ["final"]=> array(2) { [0]=> string(6) "line 4" [1]=> string(6) "line 5" } } [3]=> object(Text_Diff_Op_change)#6 (2) { ["orig"]=> array(1) { [0]=> string(6) "line 6" } ["final"]=> array(2) { [0]=> string(8) "line 6.1" [1]=> string(8) "line 6.5" } } [4]=> object(Text_Diff_Op_copy)#7 (2) { ["orig"]=> array(3) { [0]=> string(6) "line 7" [1]=> string(6) "line 8" [2]=> string(6) "line 9" } ["final"]=> array(3) { [0]=> string(6) "line 7" [1]=> string(6) "line 8" [2]=> string(6) "line 9" } } [5]=> object(Text_Diff_Op_add)#8 (2) { ["orig"]=> bool(false) ["final"]=> array(1) { [0]=> string(8) "line 9.2" } } [6]=> object(Text_Diff_Op_copy)#9 (2) { ["orig"]=> array(1) { [0]=> string(7) "line 10" } ["final"]=> array(1) { [0]=> string(7) "line 10" } } }
※長くなるので改行位置を変更
とてもわかりやすい。
サンプルスクリプトは直接foreachで回したけど、行数を揃えた配列に加工した方がテンプレートに渡して処理しやすいかな。
2008-12-29追記
コメントで頂いた情報によると、最近のバージョンではText_Diff_Renderクラスを使って処理するらしい。
出力を変えたい場合はText_Diff_Renderを継承してやることで、上記のように長ったらしいコードを書かなくて良くなるとか!
あとは行中の文字単位での差分とかになってくると、もう一工夫必要かもしれない。てかマルチバイトを考慮すると面倒くさそう。