Text_Diffでテキストの差分を取る

データの修正時に変更箇所が差分表示できたらいいよねー、という話が出ていたのでPHPでできるか試してみた。
一年ぐらい前にPEARText_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を継承してやることで、上記のように長ったらしいコードを書かなくて良くなるとか!


あとは行中の文字単位での差分とかになってくると、もう一工夫必要かもしれない。てかマルチバイトを考慮すると面倒くさそう。