PHP正则表达式:检查两个字符串是否共享两个相同的字符。

12

我刚开始接触正则表达式,但经过大量阅读(并学到了很多东西),我仍然无法找到解决这个问题的好方法。

让我明确一点,我知道这个问题可能更好地使用非正则表达式来解决,但出于简洁起见,让我只说我需要使用正则表达式(相信我,我知道有更好的方法来解决这个问题)。

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

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

每行都有以下格式:第0个位置为A或B,第1个位置为C或D,第2个位置为E或F,第3个位置为G或H。我可以假设每行都恰好有4个字符。

我的目标是,给定其中一行,匹配所有其他包含两个或更多共同字符的行。

下面的示例假设以下内容:

  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用户通常会发布一个正则表达式,并说“这应该适用于你”。
我认为这是一种半成品的答案。我真的想要理解正则表达式,因此,如果您可以在答案中包含详尽的(合理)说明来解释为什么该正则表达式:
A.有效
B.是最有效的(我认为有足够多的关于主题字符串的假设可以进行相当数量的优化)。
当然,如果您给出的答案可行,而其他人没有发布带有解决方案的答案,那么我将把它标记为答案 :)
更新2:
感谢大家的回答,提供了许多有用的信息,并且你们中的很多人都有有效的解决方案。我选择了我选择的答案,因为在运行性能测试后,它是最佳解决方案,平均具有与其他解决方案相等的运行时间。
我喜欢这个答案的原因:
1.所提供的正则表达式非常适用于更长的字符串
2.正则表达式看起来更简洁,易于像我这样的普通人进行解释。
但是,下面的所有答案也都非常详细地解释了为什么他们的解决方案是最佳的。如果您遇到这个问题,因为您正在尝试解决某些问题,请认真阅读所有答案,这对我非常有帮助。

2
我完全同意你在更新中提出的观点。在之前问过正则表达式问题后,我很少发现“这个可以用”这样的答案是最好或最有帮助的。当然,这也适用于其他问题。 - Bojangles
当你说两个常见字符时,它们必须在同一位置吗?例如,'FBGA'和'ACFG'算有两个共同字符吗?(是的,但它们位于不同的位置)。 - mathematical.coffee
@mathematical.coffee 是的,它们需要在同一个位置。 - Paul Hazen
关于你所接受的答案,这是一条跟进评论。我认为它允许了很多正面情况,而其他回答者没有假设你想要这些情况。在ACFG的情况下,它会匹配FG12,这不是我们其他人假设的情况,因为我们以为FG的位置必须在第三和第四个位置。 - Mike Ryan
1
@MikeRyan 是的,但是如果你仔细阅读问题中加粗的部分 下面的示例假定以下情况... BigFileOfLines.txt 只包含有效行 并且查看他的有效行正则表达式 "/^[AB][CD][EF][GH]$/m",你会注意到 FG12 不是一个有效的行,因此不会被包括在他的有效行集合中。 - Jack
@MikeRyan 是正确的,FG12 不是我需要担心的边缘情况。然而,如果是这样的话,我的被接受的答案将是一个糟糕的选择。 - Paul Hazen
7个回答

4
为什么不使用这个正则表达式 $regex = "/.*[$line].*[$line].*/m";
对于你的示例,它将转换为$regex = "/.*[ACFG].*[ACFG].*/m";

这是您预期的结果吗?它匹配0个或多个首先是ACFG之一,然后是0个或多个ACFG,最后是0个或多个字符。我假设您的biglineoftext包含每个有效的4个字符字符串? - Jack
1
然而,这也会匹配行“AAAA”,它只与“ACFG”共享一个字母。 - mathematical.coffee
1
在他的问题中,他说AAAA不存在。BigFileOfLines.txt只包含有效行。 - Jack
这很棒。可以很好地扩展(我想)。如果我想将我的文件缩放为仅具有“m”长度的有效行,并匹配“n”个字符,那么我是否只需在您当前拥有<code>ACFG</code>的位置输入<code>$line</code>值,并重复部分<code>.[ACFG].</code>“m”次?(不要担心,我会标记其中一个答案为正确的,只是花时间先阅读所有答案) - Paul Hazen
那么正则表达式符合您的要求吗?我认为重复使用 .*[ACFG].* 应该可以产生更长行的期望效果。您需要测试它或者给我一些更长的有效行示例来检查。 - Jack
显示剩余2条评论

2
这是一个定义“有效”行的正则表达式: /^([AB][CD][EF][GH])$/m 换句话说,每一行在第0个位置只能是A或B,在第1个位置只能是C或D,在第2个位置只能是E或F,在第3个位置只能是G或H。可以假设每一行恰好有4个字符。
原文中的正则表达式并不符合上述含义,它的意思是每一行在第0个位置只能是A、B或管道符号,第1个位置只能是C、D或管道符号等等;[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)$/更高效处理的形式,但仍然是相同的解决方案。)


我感谢您的澄清,没想到我竟然忽略了“有效行”正则表达式的问题。 - Paul Hazen
首先,感谢您的纠正,您是100%正确的(我会修复它)。其次,我已经说明了,尽管可以更容易地完成这个任务而不使用正则表达式,但我需要使用正则表达式。第三,您在最后一段中发布的解决方案与我提供的解决方案完全相同(除了您忘记在“CE”前面加上一个“.”)。我的问题的关键是如何以更简单的方式实现这一点,并具有良好的可扩展性。如果您能向我证明这是使用正则表达式匹配行的最佳方法,并考虑到我在问题中提到的可扩展性,我将把它标记为正确。 - Paul Hazen
我看不到CE前面缺少'.'吗?有一个'.',C是第二个字符?我不能证明这是使用正则表达式的最佳方法,但我也想不出其他使用正则表达式解决此问题的方法。但这可能仅仅是因为我仍然不理解为什么您会想要使用正则表达式来解决此问题。 :) - Mark Reed
@Mark_Reed,啊,是的,你说得对,只需要一个“.”。学术好奇心是我使用正则表达式的原因。我知道这是一个非常糟糕的理由。这与一项需要使用正则表达式的学校作业有关。我发布的问题与我正在解决的实际问题相去甚远,所以我不是让其他人帮我做作业 :) - Paul Hazen

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;
}

你说得对,再怎么折腾也没用哈哈 :) 然而,我使用正则表达式的原因更多是出于学术研究而非实际应用。如果我是为了写好代码而得到报酬,我肯定会采用你的方法。(注:这与一项学校作业有关(但仅仅是“有关”,并不接近直接的作业问题或其他什么)) - Paul Hazen
我真的不认为使用正则表达式来实现这个功能是一个好主意。当然,它是可行的,但它会成为一个噩梦... - Niet the Dark Absol
你是100%正确的,使用正则表达式实现是一个坏主意。我这样做的原因是因为它是我的家庭作业任务规范的一部分(我发布的问题与真正的家庭作业问题相去甚远,所以不用担心,我不会让其他人帮我完成家庭作业...) - Paul Hazen
确实是噩梦,但作业规范就是作业规范,这门课程并不允许太多灵活性去做事情的“正确”方式。我知道这很令人沮丧,但解决噩梦有助于我更多地了解正则表达式,更重要的是知道何时以及为什么不使用正则表达式。 - Paul Hazen

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,要么是|,要么是D;第三个位置要么是E,要么是|,要么是F;第四个位置要么是G,要么是|,要么是H

这是因为[A|B]表示一个字符类,其中包含三个给定的字符之一(包括|)。而{1}表示一个字符(它也是完全多余的,可以省略),外部的|在它周围的所有内容之间交替。在我上面的英文表达中,每个大写的OR代表你交替使用的|。(我从1开始计算位置,而不是0——我不想打第0个位置。)
要将您的英语描述作为正则表达式获取,您需要:
/^[AB][CD][EF][GH]$/

正则表达式将遍历并检查第一个位置的AB(在字符类中),然后在下一个位置检查CD等。

--

编辑:

您想测试仅有这四个字符中的两个是否匹配。

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

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

相比之下:

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

这是由于正则表达式实现如何遍历文本。首先测试是否在第一个位置中有A。如果成功,则测试子情况。如果失败,则完成所有可能的情况(其中有3种)。如果还没有匹配,然后测试是否在第二个位置中有C。如果成功,则测试两个子情况。如果这些都不成功,则测试`EG是否在第三和第四个位置。
此正则表达式专门创建为尽快失败。将每种情况单独列出,意味着要失败,您必须测试6种不同的情况(每个六个替代方案),而不是3种情况(至少)。在A不是第一个位置的情况下,您将立即转到测试第二个位置,而不会再次命中它两次。等等。
(请注意,我不知道PHP如何编译正则表达式-它们可能编译为相同的内部表示形式,但我怀疑不是这样。)

--

编辑:另一个要点。最快的正则表达式是一个有些模糊的术语。最快失败?最快成功?以及在成功和失败行的可能样本数据的哪个范围内?所有这些都必须澄清才能真正确定你所说的最快的标准。


关于“最快”的好注释,我指的是最快失败。当谈到正则表达式时,“最快”通常意味着“最快找到所有匹配项”吗?也就是说,我需要尽可能快地在主题中找到所有匹配项吗?(我一直认为是这样,但我很想澄清术语) - Paul Hazen
目前我用于查找匹配的算法的效率提高的好笔记。 - Paul Hazen
通常人们希望能最快失败。如果要获得绝对最快的解决方案,大多数情况下应使用非分组括号。我没有加入它们,因为那会使它难以阅读,并且它们通常是最后一步进入的。 - Mike Ryan

1

这里有一个使用Levenshtein距离而不是正则表达式的东西,应该足够可扩展以满足您的要求:

$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);

再次寻找正则表达式。虽然这很酷,很聪明,但这不是问题所要求的。 - Paul Hazen
谢谢您的编辑!我正要修复那个问题哈哈 :)顺便说一下,我并不是想粗鲁,我真的很欣赏这个聪明的解决方案,只是它并没有回答我的问题。 - Paul Hazen

1

有6种可能性,其中至少有两个字符匹配4个字符中的任意一种情况:MM..、M.M.、M..M、.MM.、.M.M 和 ..MM(“M”表示匹配,“.”表示不匹配)。

因此,您只需要将输入转换为与这些可能性之一匹配的正则表达式。对于输入ACFG,您可以使用以下内容:

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

当然,这已经是你所得出的结论了——到目前为止还不错。

关键问题在于正则表达式不是用来比较两个字符串的语言,而是用来比较一个字符串与模式的语言。因此,要么你的比较字符串必须是模式的一部分(你已经发现了),要么它必须是输入的一部分。后一种方法允许你使用通用匹配,但需要你修改输入。

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])。

这太棒了。我认为这可能是答案,但我还在阅读其他人发布的所有内容。我的问题中这个解决方案没有考虑到的一点是可扩展性问题(我很好奇您是否有建议)。假设我需要一个函数,它可以扩展到匹配不同文件中每行都是16个字符长的行,和/或者我需要匹配5个字符而不仅仅是匹配两个字符。您是否建议使用循环来基于给定长度“n”的行和需要匹配“m”个字符的情况下进行扩展/延伸/创建正则表达式? - Paul Hazen

1

昨天晚上我收藏了这个问题,准备今天回答,但似乎有点晚了^^ 不过这是我的解决方案:

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

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

可以使用以下方式生成:

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

这其实相当聪明。我选择的答案将根据先前说明的原因保持不变,但这对我学习正则表达式非常有帮助。同一件事情可以用多种方式完成,真是太神奇了。这个解决方案比我当前选择的答案更快吗?(如果是,为什么?) - Paul Hazen

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