我刚刚开始了解正则表达式,但经过相当多的阅读(并且学习了很多)后,我仍然无法找到解决这个问题的好方法。
让我说清楚,我明白这个特殊问题可能会更好地解决而不是使用正则表达式,但为了简洁起见,我只想说我需要使用正则表达式(相信我,我知道有更好的方法来解决这个问题。)
这是问题所在。我给了一个大文件,每行的长度恰好是4个字符。
这是一个定义“有效”行的正则表达式:
"/^[AB][CD][EF][GH]$/m"
在英语中,每一行在位置0处有A或B,在位置1处为C或D,在位置2处为E或F,在位置3处为G或H.我可以假设每一行都是正好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用户只需发布一个正则表达式并说“这应该适合你。”
我认为这是一个半途而废的答案。我真的想理解正则表达式,所以如果你能在答案中包含一个彻底的(在合理范围内)解释为什么那个正则表达式:
当然,如果你给出一个有效的答案,并且没有其他人用*解决方案发布答案,我会将其标记为答案:)
更新2:
感谢大家的好评,很多有用的信息,以及很多有效的解决方案。我选择了我的答案,因为在运行性能测试之后,它是最好的解决方案,与其他解决方案的运行时间相等。
我赞成这个答案的理由:
然而,很多功劳归功于以下答案以及解释为什么他们的解决方案是最好的。如果你遇到这个问题,因为这是你想要弄清楚的东西,请给他们一个阅读,帮助我。
答案 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}}在第一个位置,或D
或E
或|
位于第一个位置,或F
或'| G
H`位于第一个位置。
这是因为or
表示具有三个给定字符之一的字符类(包括[A|B]
。并且因为|
表示一个字符(它也完全是多余的,可能是因为外面的{1}
在它周围的一切之间交替出现。在我的英文表达中,每个大写的OR代表你的一个交替的|
。(我开始计算位置为1,而不是0 - 我不想输入第0个位置。)
要将您的英语描述作为正则表达式,您需要:
|
正则表达式会检查/^[AB][CD][EF][GH]$/
或A
的第一个位置(在字符类中),然后在下一个位置检查B
或C
,等
-
编辑:
您只想测试这四个匹配字符中的两个。
非常严格地说,从@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";
}