在PHP中突出显示两个字符串之间的差异

148

在PHP中突出显示两个字符串之间差异的最简单方法是什么?

我想到的是Stack Overflow编辑历史页面的方式,其中新文本为绿色,删除的文本为红色。如果有任何预先编写的函数或类可用,那就太理想了。

17个回答

81

刚刚编写了一个类来计算从一个字符串转换为另一个字符串所需的最小编辑次数(不应字面理解),详情请见:http://www.raymondhill.net/finediff/

它有一个静态函数来呈现差异的HTML版本。

这是第一个版本,可能会改进,但作为现在效果良好,因此我将其提供给需要有效地生成紧凑差异的人。

编辑:它现在在Github上:https://github.com/gorhill/PHP-FineDiff


3
我会尝试使用 https://github.com/xrstf/PHP-FineDiff 中的分支来获得多字节支持! - activout.se
1
@R. Hill - 对我来说也非常好用。这确实是一个比当前看起来已经失效的答案更好的答案。 - Wonko the Sane
有更新吗?它显示无法包含文件“Texts / Diff.php”,并且该文件不在压缩包中。 - SISYN
太棒了!我的意思是在线演示和示例代码。完美的字符级差异。简直惊人!谢谢你! - Filip OvertoneSinger Rydlo
3
现在似乎 https://github.com/BillyNate/PHP-FineDiff 是最前沿的分支,并且它支持多字节和不同编码。https://github.com/xrstf/PHP-FineDiff 页面无法访问,网址为 @activout.se。 - Kangur

46

您以前能够使用 PHP Horde_Text_Diff 软件包。

然而,该软件包现已不可用。


3
网站已失效,但archive.org有该网站的副本:http://web.archive.org/web/20080506155528/http://software.zuavra.net/inline-diff/ - R. Hill
17
很遗憾它需要PEAR。依赖PEAR很糟糕。 - Rudie
7
从新网站上的内容:「更新:行内渲染器现在是 Text_Diff PEAR 包的本地部分,您不再需要使用此处介绍的 hack。」所以现在只需使用 Text_Diff 即可。 - Mat
13
GPL 不仅是免费使用,它还要求您的模块/项目也采用 GPL 许可证。 - Parris
1
在使用MongoDB文档版本控制实现差异化机制的项目中,我偶然发现了这篇文章。正如@Mat所提到的,答案中的链接已经过时,只会将您重定向到此处:http://www.horde.org/libraries/Horde_Text_Diff/download,但如果您使用remi存储库,则可以执行以下操作:`yum install php-horde-Horde-Text-Diff --enablerepo=remi`。 - Mike Purcell
显示剩余3条评论

27

这是一个很好的算法,同时也有相关的PHP实现

解决这个问题并不像看起来那么简单,我花了一年时间烦恼着这个问题,最终用18行代码在PHP中完成了自己的算法。虽然这并不是最高效的diff方法,但它可能是最容易理解的。

它的工作原理是找到两个字符串共同拥有的最长单词序列,并递归地找到剩余字符串的最长序列,直到子字符串没有共同的单词为止。此时,将剩余的新单词作为插入,剩余的旧单词作为删除。

你可以在这里下载源代码:PHP SimpleDiff...


1
我也觉得这很有用!没有Pear那么复杂。 - dgavey
这里出现了一个错误:if($matrix[$oindex][$nindex] > $maxlen){maxlen未定义。 - dynamic
好的,你发了一条评论来解决这个问题。:) 为什么不在初始代码中进行编辑呢?无论如何感谢你的+1...嗯,你不是作者。 - dynamic
1
这是似乎来自2010年的最新版本:https://github.com/paulgb/simplediff/blob/master/simplediff.php - rsk82
1
实际上,简单易懂加一分。 - Parag Tyagi
1
此脚本现在可以在这里找到:https://github.com/paulgb/simplediff/blob/master/php/simplediff.php - Noel Whitemore

26

以下是一个简单的函数,可用于比较两个数组。它实现了LCS算法:

function computeDiff($from, $to)
{
    $diffValues = array();
    $diffMask = array();

    $dm = array();
    $n1 = count($from);
    $n2 = count($to);

    for ($j = -1; $j < $n2; $j++) $dm[-1][$j] = 0;
    for ($i = -1; $i < $n1; $i++) $dm[$i][-1] = 0;
    for ($i = 0; $i < $n1; $i++)
    {
        for ($j = 0; $j < $n2; $j++)
        {
            if ($from[$i] == $to[$j])
            {
                $ad = $dm[$i - 1][$j - 1];
                $dm[$i][$j] = $ad + 1;
            }
            else
            {
                $a1 = $dm[$i - 1][$j];
                $a2 = $dm[$i][$j - 1];
                $dm[$i][$j] = max($a1, $a2);
            }
        }
    }

    $i = $n1 - 1;
    $j = $n2 - 1;
    while (($i > -1) || ($j > -1))
    {
        if ($j > -1)
        {
            if ($dm[$i][$j - 1] == $dm[$i][$j])
            {
                $diffValues[] = $to[$j];
                $diffMask[] = 1;
                $j--;  
                continue;              
            }
        }
        if ($i > -1)
        {
            if ($dm[$i - 1][$j] == $dm[$i][$j])
            {
                $diffValues[] = $from[$i];
                $diffMask[] = -1;
                $i--;
                continue;              
            }
        }
        {
            $diffValues[] = $from[$i];
            $diffMask[] = 0;
            $i--;
            $j--;
        }
    }    

    $diffValues = array_reverse($diffValues);
    $diffMask = array_reverse($diffMask);

    return array('values' => $diffValues, 'mask' => $diffMask);
}

它生成两个数组:

  • values数组:按照差异出现的顺序列出元素的列表。
  • mask数组:包含数字。0:未更改,-1:已移除,1:已添加。

如果您使用字符填充数组,则可以使用它来计算内联差异。现在只需要一步即可突出显示差异:

function diffline($line1, $line2)
{
    $diff = computeDiff(str_split($line1), str_split($line2));
    $diffval = $diff['values'];
    $diffmask = $diff['mask'];

    $n = count($diffval);
    $pmc = 0;
    $result = '';
    for ($i = 0; $i < $n; $i++)
    {
        $mc = $diffmask[$i];
        if ($mc != $pmc)
        {
            switch ($pmc)
            {
                case -1: $result .= '</del>'; break;
                case 1: $result .= '</ins>'; break;
            }
            switch ($mc)
            {
                case -1: $result .= '<del>'; break;
                case 1: $result .= '<ins>'; break;
            }
        }
        $result .= $diffval[$i];

        $pmc = $mc;
    }
    switch ($pmc)
    {
        case -1: $result .= '</del>'; break;
        case 1: $result .= '</ins>'; break;
    }

    return $result;
}

Eg.:

echo diffline('StackOverflow', 'ServerFault')

将输出:

S<del>tackO</del><ins>er</ins>ver<del>f</del><ins>Fau</ins>l<del>ow</del><ins>t</ins> 

栈服务器故障

额外说明:

  • 差异矩阵需要(m+1)*(n+1)个元素。如果您尝试比较长的序列,则可能遇到内存不足错误。在这种情况下,先对更大的块(例如行)进行比较,然后在第二次比较它们的内容。
  • 如果从开头和结尾修剪匹配元素,然后仅在不同的中间运行算法,则可以改进算法。一个稍后版本(更臃肿)也包含这些修改。

这是一个简单、有效且跨平台的技巧;我使用explode()函数在不同的边界(行或单词)上来获取适当的输出。非常好的解决方案,谢谢! - Uncle Code Monkey
计算差异未找到。 - KD.S.T.
@ichimaru 你把两个函数都复制粘贴了吗? - Calmarius
@Calmarius 没有看到另一个函数...我发誓!现在它能工作了,谢谢! - KD.S.T.
谢谢,这个对于查找与被接受的答案不同的地方非常有用。 - Karan Sharma

25
如果你需要一个强大的库,那么Text_Diff(一个PEAR包)似乎是非常不错的选择。它有一些很酷的功能。

6
上面提到的PHP Inline-Diff," ..使用PEAR的Text_Diff计算差异"。 :) - M.N
链接已损坏。找不到软件包。这是最新版本WordPress使用的相同Diff软件包。 - Basil Musa

6

1
xdiff PECL扩展已不再维护,根据http://pecl.php.net/package/xdiff的记录,显然自2008-07-01以来就没有发布稳定版本。因此,我最终采用了被接受答案中提到的建议,因为它更加新颖,可以在http://www.horde.org/libraries/Horde_Text_Diff/download下载。 - Mike Purcell
PHP的XDiff有一个简单的安装程序吗?(适用于Debian Linux) - Peter Krauss
@MikePurcell,事实上,它仍在维护中。最新的稳定版本2.0.1支持PHP 7已于2016年5月16日发布。 - user2513149
@PeterKrauss,是的,有的。看一下这个问题:https://serverfault.com/questions/362680/installing-xdiff-locally-with-apache-php - user2513149

5

我在使用基于PEAR和简单替代方案时遇到了很大的问题。因此,在此提供一种解决方案,利用Unix diff命令(显然,您必须在Unix系统上或拥有可用的Windows diff命令才能使其正常工作)。选择您喜欢的临时目录,并在需要时更改异常以返回代码。

/**
 * @brief Find the difference between two strings, lines assumed to be separated by "\n|
 * @param $new string The new string
 * @param $old string The old string
 * @return string Human-readable output as produced by the Unix diff command,
 * or "No changes" if the strings are the same.
 * @throws Exception
 */
public static function diff($new, $old) {
  $tempdir = '/var/somewhere/tmp'; // Your favourite temporary directory
  $oldfile = tempnam($tempdir,'OLD');
  $newfile = tempnam($tempdir,'NEW');
  if (!@file_put_contents($oldfile,$old)) {
    throw new Exception('diff failed to write temporary file: ' . 
         print_r(error_get_last(),true));
  }
  if (!@file_put_contents($newfile,$new)) {
    throw new Exception('diff failed to write temporary file: ' . 
         print_r(error_get_last(),true));
  }
  $answer = array();
  $cmd = "diff $newfile $oldfile";
  exec($cmd, $answer, $retcode);
  unlink($newfile);
  unlink($oldfile);
  if ($retcode != 1) {
    throw new Exception('diff failed with return code ' . $retcode);
  }
  if (empty($answer)) {
    return 'No changes';
  } else {
    return implode("\n", $answer);
  }
}

4

3
UTF-8兼容性有问题。它在字符串上使用数组访问,将每个字符视为一个字节宽度。不过可以很容易地通过mb_split进行修复。 - Gellweiler
1
这里提供一个快速修复。只需将 $sequence1 = $string1; $sequence2 = $string2; $end1 = strlen($string1) - 1; $end2 = strlen($string2) - 1; 替换为 $sequence1 = preg_split('//u', $string1, -1, PREG_SPLIT_NO_EMPTY); $sequence2 = preg_split('//u', $string2, -1, PREG_SPLIT_NO_EMPTY); $end1 = count($sequence1) - 1; $end2 = count($sequence2) - 1; - Gellweiler
在函数computeTable中使用字符模式时,该类会耗尽内存。 - Andrew
1
当前链接为http://code.iamkate.com/php/diff-implementation/。我已经测试过它,它不支持UTF-8。 - Kangur

3

不确定这是否仍然是2023年的最佳答案,但它肯定运行良好。您需要在if (!function_exists()) {}调用中包装mb_ordmb_char函数(或删除它们),因为它们现在是内置函数,但它运行得很好。Apache 2.0许可证也非常宽松。 - undefined

2
您需要的是“差异算法”。快速的谷歌搜索给我带来了这个解决方案。我没有测试过,但也许它能满足您的需求。

我刚刚测试了那个脚本,它运行良好 - diff操作非常快(处理我测试的短段落大约需要10毫秒),并且能够检测到添加换行符。按原样运行代码会生成一些PHP注意事项,您可能需要修复它们,但除此之外,如果您需要显示内联差异而不是使用传统的并排差异视图,则这是一个非常好的解决方案。 - Noel Whitemore

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接