PHP 正则表达式 检查两个字符串是否共享两个常用字符


PHP Regex Check if two strings share two common characters

我刚刚开始了解正则表达式,但是在做了相当多的阅读(并学到了很多东西)之后,我仍然无法找到解决这个问题的好方法。

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

问题来了。我得到了一个大文件,每行正好有 4 个字符长。

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

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

在英语中,每行的 A 或 B 位于位置 0,C 或 D 位于位置 1,E 或 F 位于位置 2,G 或 H 位于位置 3。我可以假设每行的长度正好是 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 用户简单地发布一个正则表达式并说"这应该适合你"。

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

  • A. 作品
  • 二.是最有效的(我觉得可以对主题字符串做出足够数量的假设,可以进行相当数量的优化)。

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

更新 2:

感谢大家的精彩回复,许多有用的信息,以及你们中的许多人都有有效的解决方案。我之所以选择答案,是因为在运行性能测试后,它是最佳解决方案,平均运行时间与其他解决方案相同。

我赞成这个答案的原因:

  1. 给出的正则表达式为较长的行提供了出色的可扩展性
  2. 正则表达式看起来干净得多,对于像我这样的凡人来说更容易解释。

但是,很多功劳也归功于以下答案,因为它们非常彻底地解释了为什么他们的解决方案是最好的。如果你遇到这个问题是因为你想弄清楚的事情,请给他们一读,对我帮助很大。

你为什么不直接使用这个正则表达式$regex = "/.*[$line].*[$line].*/m";

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

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

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

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

这不是正则表达式的意思。 该正则表达式意味着每行在位置 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)$/一样会更好 - 但这仍然是相同的解决方案,只是以更有效的处理形式。

您已经回答了如何使用正则表达式执行此操作,并指出了它的缺点和无法扩展,因此我认为没有必要鞭打死马。相反,这是一种无需正则表达式即可工作的方法:

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

有 6 种可能性,其中至少有两个字符匹配 4 个字符:MM..、M.M.、M.。米,毫米,.M.M和..MM("M"表示匹配,"."表示不匹配)。

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

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

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

关键问题是正则表达式不是一种用于比较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]等)。

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

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

然后说:

在英语中,每行的 A 或 B 位于位置 0,C 或 D 位于位置 1,E 或 F 位于位置 2,G 或 H 位于位置 3。我可以假设每行的长度正好是 4 个字符。

但这根本不是正则表达式的意思。

这是因为|运算符在此处具有最高优先级。 所以,正则表达式在英语中真正说的是:要么A,要么|,要么B在第一个位置,要么C|D在第一个位置,要么E,要么|F在第一个位置,要么G或"|or H' 在第一个位置。

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

要获得您的英文描述作为正则表达式,您需要:

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

正则表达式将通过并检查第一个位置的AB(在字符类中),然后在下一个位置检查CD,依此类推。

--

编辑:

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

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

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

与以下相比:

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

这是因为正则表达式实现如何逐步通过文本。 您首先测试A是否处于第一个位置。 如果成功,则测试子案例。 如果失败了,那么您就完成了所有这些可能的情况(或有 3 种情况)。 如果您还没有匹配项,则测试 C 是否位于第 2 位。 如果成功,则测试两个子情况。 如果这些都没有成功,你测试,'EG 在第 3 和第 4 位。

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

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

--

编辑:关于附加点。 最快的正则表达式是一个有点模棱两可的术语。 最快失败? 最快成功? 并给定成功和失败行的样本数据的可能范围? 所有这些都必须澄清,才能真正确定您所说的最快标准。

以下是使用 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);

昨天晚上为这个问题添加了书签,今天发布了答案,但似乎我有点晚了^^ 无论如何,这是我的解决方案:

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

它查找被任何其他字符包围的ACFG字符之一的两次出现。循环展开并使用所有格量词,以稍微提高性能。

可以使用以下方法生成:

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