【问题标题】:PHP Regex Check if two strings share two common charactersPHP Regex 检查两个字符串是否共享两个公共字符
【发布时间】:2012-05-03 14:17:48
【问题描述】:

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

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

这就是问题所在。我得到了一个大文件,每行正好是 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 用户简单地发布一个正则表达式并说“这应该适合你。”

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

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

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

更新 2:

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

我赞成这个答案的原因:

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

但是,以下答案也非常值得称赞,因为他们非常彻底地解释了为什么他们的解决方案是最好的。如果您遇到这个问题是因为这是您想要弄清楚的问题,请给他们全部阅读,这对我有很大帮助。

【问题讨论】:

  • 我绝对同意您在更新中提出的观点。之前问过正则表达式问题,我很少发现“这个有效”的答案是最好的或最有帮助的。当然,这也适用于其他问题。
  • 当你说两个常见的字符时,它们必须在同一个位置吗?例如,您是否将“FBGA”视为与“ACFG”有两个常见字符? (确实如此,但他们处于不同的位置)。
  • @mathematical.coffee 是的,他们需要在同一个位置。
  • 只是对您接受的答案的后续评论。它可以带来很多积极因素,我认为您的其他受访者不会认为您想要。在 ACFG 的情况下,这将匹配 FG12,这不是我认为我们其他人所假设的,因为我们认为 FG 的位置必须位于第 3 和第 4 位。跨度>
  • @MikeRyan 是的,但是如果您仔细阅读问题的粗体部分The below example assumes the following...BigFileOfLines.txt contains only valid lines 并查看他的有效行正则表达式"/^[AB][CD][EF][GH]$/m",您会注意到FG12 不是有效的行,因此不会包含在他的有效行集中。

标签: php regex preg-match-all


【解决方案1】:

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

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

【讨论】:

  • 我不确定我是否理解为什么这会起作用(而且我现在无法对其进行测试)。你能解释一下这是做什么的吗?对我来说这似乎有点太贪心了,我们可以做得比使用“。*”我想的更好。但是话又说回来,我还在学习这些东西:)
  • rubular.com/r/qx2vZ00U7k 这是您想要的结果吗?它首先匹配 0 或更多,然后是 ACFG 之一,然后是 0 或更多,然后是 ACFG,然后是 0 或更多。我假设您的 biglineoftext 包含每个 4 个字符的所有有效字符串?
  • 但是,这也将匹配“AAAA”行,该行仅与“ACFG”共享一个字母。
  • AAAA 不存在。 BigFileOfLines.txt contains only valid lines 是他在问题中所说的。
  • 这太棒了。很好地缩放(我认为)。如果我想将我的文件缩放为只有“m”长度的有效行,并匹配“n”个字符,我是否只需输入您当前拥有 ACFG 的 $line,然后重复部分 .*[ACFG].* "m" 的次数? (不用担心我会将其中一个答案标记为正确,只需花时间先阅读所有答案)
【解决方案2】:

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

/^[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 个字符 长。

这不是那个正则表达式的意思。该正则表达式意味着每行都有 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)$/ - 但这仍然是相同的解决方案,只是采用了更有效的处理形式。)

【讨论】:

  • 感谢您的澄清,不敢相信我忽略了“有效行”正则表达式的问题。
  • ...输入太快首先,感谢您的更正,您是 100% 正确的(我会修复它)。其次,我已经说过,尽管没有正则表达式可以更轻松地完成此任务,但我需要使用正则表达式。第三,您在上一段中发布的解决方案与我提供的解决方案相同(除了您错过了“CE”前面的“.”)。我的问题的关键是如何以更简单的方式实现这一点,并且可以很好地扩展。如果您可以向我证明这是 best 方法,如我的问题中提到的那样,考虑到可伸缩性,将行与正则表达式匹配,我会将其标记为正确。
  • 我没有看到丢失的 '.'在CE面前?有一个 .,而 C 是第二个字符?我无法证明这是使用正则表达式的最佳方法,但我也想不出任何其他使用正则表达式的方法。但这可能仅仅是因为我仍然不明白你为什么要使用正则表达式来解决这个问题。 :)
  • @Mark_Reed,啊,是的,你是对的,只有一个“。”是需要的。学术好奇心是我使用 Regex 的原因。我知道这是一个可怕的原因。它与需要使用正则表达式的学校作业有关。我发布的问题与我正在解决的实际问题相去甚远,所以我不只是让其他人做我的功课:)
【解决方案3】:

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

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

【讨论】:

  • 你是对的,没有必要鞭打一匹死马哈哈 :) 但是,我尝试使用正则表达式执行此操作的原因比应用程序更具学术性。如果我得到报酬来写这个作为好的代码,我肯定会使用你的方法。 (阅读:这与学校作业有关(但仅相关,这与直接的家庭作业问题或其他问题不接近))
  • 我真的不认为用正则表达式实现它是一个好主意。这是可行的,当然,但这将是一场噩梦......
  • 你是 100% 正确的,用正则表达式实现是个坏主意。我这样做的原因是因为它是我家庭作业规范的一部分(我发布的问题与真正的家庭作业问题并不接近,所以不用担心,我不会让其他人做我的家庭作业。 ..)
  • 确实是噩梦,但作业规范就是作业规范,它不是一个真正允许以“正确”方式做事的灵活性的类。我知道令人沮丧,但解决噩梦有助于我更多地了解正则表达式,更重要的是知道何时以及为什么不使用正则表达式。
【解决方案4】:

有 6 种可能性,至少 4 个字符中有两个匹配:MM..、MM、M..M、.MM.、.MM 和 ..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;
}

这个函数的作用是在你的输入字符串前面加上你想要匹配的行,然后使用一个模式来比较每一行之后第一行(那是@之后的+ 987654326@working) 回到第一行的 4 个字符。

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

【讨论】:

  • 这很棒。我认为这可能是答案,但我仍在阅读其他所有人发布的所有内容。我的问题中没有解决的一个次要方面(我很好奇您是否有意见)是可伸缩性问题。假设我需要一个函数,该函数可以缩放以匹配不同文件中的行,其中每行长度为 16 个字符,和/或我需要匹配 5 个字符而不是仅匹配两个字符。您是否建议使用循环来简单地展开/extend/create the regex based on a given line of length "n" and a need to match "m" number of characters?
【解决方案5】:

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

"/^[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 在第一个位置,或 @987654329 @ 或 |F 在第一位,或 G 或 '|orH` 在第一位。

这是因为[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 在第 3 和第 4 位。

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

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

--

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

【讨论】:

  • 关于“最快”的好笔记我的意思是最快的失败。当谈到正则表达式时,通常是否假设“最快”意味着“最快找到所有匹配项”?例如,我需要尽快找到主题中的所有匹配项吗? (我一直假设这是真的,但我希望澄清术语)
  • 关于提高我目前用于查找匹配项的算法的效率的很好的说明。
  • 最快失败通常是人们想要的。哦,对于大多数这些解决方案,如果你想获得绝对最快的速度,你应该使用非分组括号。我没有把它们放进去,因为它会使它变得更难读,而且它们通常是进入的最后一步。
【解决方案6】:

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

【讨论】:

  • 再次,寻找正则表达式。虽然这很酷而且很聪明,但这不是问题所要求的。
  • 感谢您的编辑!正要解决这个问题,哈哈 :) 顺便说一句,我并不是要傲慢,我真的很欣赏这个聪明的解决方案,它只是没有回答我的问题。
【解决方案7】:

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

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

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

可以使用:

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

【讨论】:

  • 这实际上非常聪明。由于前面所述的原因,我选择的答案将仍然是答案,但这对我学习正则表达式真的很有帮助。令人惊讶的是,有多少种不同的方式可以完成同一件事。此解决方案是否比我当前选择的答案更快? (如果是,为什么?)
猜你喜欢
  • 2022-11-04
  • 2023-03-29
  • 2013-09-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多