我刚刚开始了解正则表达式,但是在做了相当多的阅读(并学到了很多东西)之后,我仍然无法找到解决这个问题的好方法。
让我明确一点,我知道这个特定问题可能最好不使用正则表达式来解决,但为了简洁起见,让我说我需要使用正则表达式(相信我,我知道有更好的方法来解决这个问题)。
问题来了。我得到了一个大文件,每行正好有 4 个字符长。
这是一个定义"有效"行的正则表达式:
"/^[AB][CD][EF][GH]$/m"
在英语中,每行的 A 或 B 位于位置 0,C 或 D 位于位置 1,E 或 F 位于位置 2,G 或 H 位于位置 3。我可以假设每行的长度正好是 4 个字符。
我正在尝试做的是给定其中一行,匹配包含 2 个或更多常见字符的所有其他行。
以下示例假定满足以下条件:
-
$line
始终是有效的格式 -
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:
感谢大家的精彩回复,许多有用的信息,以及你们中的许多人都有有效的解决方案。我之所以选择答案,是因为在运行性能测试后,它是最佳解决方案,平均运行时间与其他解决方案相同。
我赞成这个答案的原因:
- 给出的正则表达式为较长的行提供了出色的可扩展性
- 正则表达式看起来干净得多,对于像我这样的凡人来说更容易解释。
但是,很多功劳也归功于以下答案,因为它们非常彻底地解释了为什么他们的解决方案是最好的。如果你遇到这个问题是因为你想弄清楚的事情,请给他们一读,对我帮助很大。
你为什么不直接使用这个正则表达式$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]$/
正则表达式将通过并检查第一个位置的A
或B
(在字符类中),然后在下一个位置检查C
或D
,依此类推。
--
编辑:
您只想测试这四个字符中的两个匹配。
非常严格地说,从 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";
}