PHP Regex检查两个字符串是否共享两个公共字符

时间:2012-04-22 22:16:51

标签: php regex preg-match-all

我刚刚开始了解正则表达式,但经过相当多的阅读(并且学习了很多)后,我仍然无法找到解决这个问题的好方法。

让我说清楚,我明白这个特殊问题可能会更好地解决而不是使用正则表达式,但为了简洁起见,我只想说我需要使用正则表达式(相信我,我知道有更好的方法来解决这个问题。)

这是问题所在。我给了一个大文件,每行的长度恰好是4个字符。

这是一个定义“有效”行的正则表达式:

"/^[AB][CD][EF][GH]$/m" 

在英语中,每一行在位置0处有A或B,在位置1处为C或D,在位置2处为E或F,在位置3处为G或H.我可以假设每一行都是正好4个字符。

我正在尝试做的是给出其中一行,匹配包含2个或更多常见字符的所有其他行。

以下示例假定以下内容:

  1. $line始终是有效格式
  2. BigFileOfLines.txt仅包含有效行

示例:

// Matches all other lines in string that share 2 or more characters in common
// with "$line"
function findMatchingLines($line, $subject) {
    $regex = "magic regex I'm looking for here";
    $matchingLines = array();
    preg_match_all($regex, $subject, $matchingLines);
    return $matchingLines;
}

// Example Usage
$fileContents = file_get_contents("BigFileOfLines.txt");
$matchingLines = findMatchingLines("ACFG", $fileContents);

/*
 * Desired return value (Note: this is an example set, there 
 * could be more or less than this)
 * 
 * BCEG
 * ADFG
 * BCFG
 * BDFG
*/

我知道工作的一种方法是使用如下的正则表达式(以下正则表达式仅适用于“ACFG”:

"/^(?:AC.{2}|.CF.|.{2}FG|A.F.|A.{2}G|.C.G)$/m"

这项工作正常,性能可以接受。令我困扰的是,我必须基于$line生成这个,我宁愿让它不知道具体参数是什么。此外,如果稍后修改代码以匹配说,3个或更多字符,或者如果每行的大小从4增加到16,则此解决方案不能很好地扩展。

感觉就像我忽略了一些非常简单的东西。似乎这可能是一个重复的问题,但我所看到的其他问题似乎都没有解决这个特殊问题。

提前致谢!

更新

似乎使用正则表达式答案的规范是SO用户只需发布一个正则表达式并说“这应该适合你。”

我认为这是一个半途而废的答案。我真的想理解正则表达式,所以如果你能在答案中包含一个彻底的(在合理范围内)解释为什么那个正则表达式:

  • 甲。作品
  • B中。效率最高(我觉得可以对主题字符串做出足够数量的假设,可以进行大量的优化)。

当然,如果你给出一个有效的答案,并且没有其他人用*解决方案发布答案​​,我会将其标记为答案:)

更新2:

感谢大家的好评,很多有用的信息,以及很多有效的解决方案。我选择了我的答案,因为在运行性能测试之后,它是最好的解决方案,与其他解决方案的运行时间相等。

我赞成这个答案的理由:

  1. 给定的正则表达式为更长的行提供了出色的可伸缩性
  2. 正则表达式看起来更清晰,对于像我这样的凡人来说更容易理解。

然而,很多功劳归功于以下答案以及解释为什么他们的解决方案是最好的。如果你遇到这个问题,因为这是你想要弄清楚的东西,请给他们一个阅读,帮助我。

7 个答案:

答案 0 :(得分:4)

为什么不使用此正则表达式$regex = "/.*[$line].*[$line].*/m";

对于您的示例,转换为$regex = "/.*[ACFG].*[ACFG].*/m";

答案 1 :(得分:2)

  

这是一个定义“有效”行的正则表达式:

     

/^[A|B]{1}|[C|D]{1}|[E|F]{1}|[G|H]{1}$/m

     

在英语中,每一行在位置0都有A或B,C或D.   在位置1处,位置2处的E或F,以及G处的G或H.   我可以假设每一行都是4个字符   长。

这不是正则表达式的含义。该正则表达式意味着每一行都有A或B或位于0,C或D的管道或位置1的管道等; [A|B]表示“要么是'A',要么是'|'或'B'“。 '|'仅表示字符类的'或'

此外,{1}是无操作的;缺乏任何量词,一切都必须出现一次。所以对于上述英语的正确正则表达是这样的:

/^[AB][CD][EF][GH]$/

或者,或者:

/^(A|B)(C|D)(E|F)(G|H)$/

第二个具有在每个位置捕获字母的副作用,因此第一个捕获的组将告诉您第一个字符是A还是B,依此类推。如果您不想捕获,可以使用非捕获分组:

/^(?:A|B)(?:C|D)(?:E|F)(?:G|H)$/

但是字符级版本是迄今为止通常的写作方式。

关于你的问题,它不适合正则表达式;当你解构字符串时,用适当的正则表达式语法将它粘在一起,编译正则表达式,并进行测试,你可能会做一个逐字符比较更好。

我会改写你的“ACFG”正则表达式:/^(?:AC|A.F|A..G|.CF|.C.G|..FG)$/,但那只是外观;我想不出使用正则表达式的更好的解决方案。 (虽然正如Mike Ryan所指出的那样,它会更好地作为/^(?:A(?:C|.E|..G))|(?:.C(?:E|.G))|(?:..EG)$/ - 但这仍然是相同的解决方案,只是采用更有效的处理形式。)

答案 2 :(得分:1)

你已经用正则表达式回答了如何做到这一点,并注意到它的缺点和无法扩展,所以我认为没有必要鞭打死马。相反,这是一种无需正则表达式即可工作的方式:

function findMatchingLines($line) {
    static $file = null;
    if( !$file) $file = file("BigFileOfLines.txt");

    $search = str_split($line);
    foreach($file as $l) {
        $test = str_split($l);
        $matches = count(array_intersect($search,$test));
        if( $matches > 2) // define number of matches required here - optionally make it an argument
            return true;
    }
    // no matches
    return false;
}

答案 3 :(得分:1)

人们可能会对你的第一个正则表达式感到困惑。你给:

"/^[A|B]{1}|[C|D]{1}|[E|F]{1}|[G|H]{1}$/m" 

然后说:

  

在英语中,每一行在位置0处有A或B,在位置1处为C或D,在位置2处为E或F,在位置3处为G或H.我可以假设每一行都是正好4个字符。

但这并不是正则表达式的含义。

这是因为|运算符的优先级最高。那么,正则表达式真正用英语说的是:第一个位置A|B,或C|或{ {1}}在第一个位置,或DE|位于第一个位置,或F或'| G H`位于第一个位置。

这是因为or表示具有三个给定字符之一的字符类(包括[A|B]。并且因为|表示一个字符(它也完全是多余的,可能是因为外面的{1}在它周围的一切之间交替出现。在我的英文表达中,每个大写的OR代表你的一个交替的|。(我开始计算位置为1,而不是0 - 我不想输入第0个位置。)

要将您的英语描述作为正则表达式,您需要:

|

正则表达式会检查/^[AB][CD][EF][GH]$/ A的第一个位置(在字符类中),然后在下一个位置检查BC,等

-

编辑:

您只想测试这四个匹配字符中的两个。

非常严格地说,从@Mark Reed的回答中,最快的正则表达式(在解析之后)很可能是:

D

与:相比:

/^(A(C|.E|..G))|(.C(E)|(.G))|(..EG)$/

这是因为正则表达式实现如何逐步执行文本。首先测试/^(AC|A.E|A..G|.CE|.C.G|..EG)$/ 是否在第一个位置。如果成功,则测试子案例。如果失败了,那么你已经完成了所有可能的情况(或者有3个)。如果还没有匹配,则测试C是否在第2位。如果成功,则测试两个子类。如果没有一个成功,你测试,'EG在第3和第4位。

此正则表达式专门创建为尽快失败。单独列出每个案例,意味着失败,你将测试6个不同的案例(六个案例中的每一个),而不是3个案例(至少)。如果A不是第一个位置,你会立即去测试第二个位置,而不是再次击中它。等

(请注意,我并不确切知道PHP如何编译正则表达式 - 它们可能会编译为相同的内部表示,但我怀疑不是。)

-

编辑:另外一点。最快的正则表达式是一个有点含糊不清的术语。最快失败?成功最快?并且考虑到成功和失败行的样本数据的可能范围?所有这些都必须澄清,以确定最快的标准。

答案 4 :(得分:1)

这是使用Levenshtein distance而不是正则表达式的东西,并且应该可以根据您的要求进行扩展:

$lines = array_map('rtrim', file('file.txt')); // load file into array removing \n
$common = 2; // number of common characters required
$match = 'ACFG'; // string to match

$matchingLines = array_filter($lines, function ($line) use ($common, $match) {
    // error checking here if necessary - $line and $match must be same length
    return (levenshtein($line, $match) <= (strlen($line) - $common));
});

var_dump($matchingLines);

答案 5 :(得分:1)

有6种可能至少两个字符匹配4:MM ..,MM,M..M,.MM。,。MM和..MM(“M”意思是匹配,“。”意思是不匹配。)

因此,您只需要将输入转换为匹配任何可能性的正则表达式。对于ACFG的输入,您可以使用:

"/^(AC..|A.F.|A..G|.CF.|.C.G|..FG)$/m"

这当然是你已经得出的结论 - 到目前为止一直很好。

关键问题是Regex不是用于比较two strings的语言,它是将字符串与模式进行比较的语言。因此,您的比较字符串必须是模式的一部分(您已经找到),或者它必须是输入的一部分。后一种方法允许您使用通用匹配,但确实需要您修改输入。

function findMatchingLines($line, $subject) {
  $regex = "/(?<=^([AB])([CD])([EF])([GH])[.\n]+)"
      + "(\1\2..|\1.\3.|\1..\4|.\2\3.|.\2.\4|..\3\4)/m";
  $matchingLines = array();
  preg_match_all($regex, $line + "\n" + $subject, $matchingLines);
  return $matchingLines;
}

这个函数的作用是将您的输入字符串与您要匹配的行预先挂起,然后使用一个模式比较第一行后的每行+[.\n]工作之后)回到第一行的4个字符。

如果您还想根据“规则”验证那些匹配的行,只需将每个模式中的.替换为相应的字符类(\1\2[EF][GH]等。 )。

答案 6 :(得分:1)

我昨天在昨天给这个问题添加了书签今天发布了一个答案,但似乎我有点迟了^^这是我的解决方案:

/^[^ACFG]*+(?:[ACFG][^ACFG]*+){2}$/m

它查找其中一个由任何其他字符包围的ACFG个字符的两次出现。循环展开并使用占有量词,以提高性能。

可以使用以下方式生成:

function getRegexMatchingNCharactersOfLine($line, $num) {
    return "/^[^$line]*+(?:[$line][^$line]*+){$num}$/m";
}