Qt扫盲篇之QRegularExpression正则匹配总结
作者:太阳风暴
一、概述
正则表达式(regep)是处理字符串和文本的强大工具。在许多情况下使用的非常多,就比如在QString 的一些函数API接口参数都是可以传入 正则表达式的,正则表达式的使用场景有如下几种,例如:
- 验证
regexp可以测试子字符串是否满足某些条件,例如是整数或不包含空格。 - 搜索
regexp提供了比简单子字符串匹配更强大的模式匹配,例如,匹配单词mail、letter或correspondence中的一个,但不匹配单词email、mailman、mailer、letterbox等。 - 搜索和替换
regexp可以用不同的子字符串替换所有出现的子字符串,例如,用&替换所有出现的&;除了&后面已经跟着一个amp; - 字符串分割
regexp可用于标识字符串应该在何处分割,例如分割制表符分隔的字符串。
本文档并不是使用正则表达式进行模式匹配的完整参考,以下部分将要求读者对类perl正则表达式及其模式语法有一些基本知识。
基础的知识可以看 这里
二、介绍
QRegularExpression实现了perl兼容的正则表达式。它完全支持Unicode。有关QRegularExpression支持的正则表达式语法的概述,请参阅前面提到的pcrepattern(3)手册页。正则表达式由两部分组成:一个模式字符串和一组改变模式字符串含义的模式选项。
你可以通过向QRegularExpression构造函数传递一个字符串来设置模式字符串:
QRegularExpression re("a pattern");
这将模式字符串设置为一个模式。还可以使用setPattern()函数在一个已存在的QRegularExpression对象上设置模式:
QRegularExpression re; re.setPattern("another pattern");
注意,根据c++字符串字面量规则,必须用另一个反斜杠转义模式字符串中的所有反斜杠:
// matches two digits followed by a space and a word QRegularExpression re("\\d\\d \\w+"); // matches a backslash QRegularExpression re2("\\\\");
pattern() 函数返回QRegularExpression对象当前设置的模式:
QRegularExpression re("a third pattern"); QString pattern = re.pattern(); // pattern == "a third pattern"
三、模式选项
可以通过设置一个或多个模式选项来修改模式字符串的含义。例如,可以通过设置QRegularExpression:: caseinsensitive选项来设置模式不区分大小写。
你可以将这些选项传递给QRegularExpression构造函数,如下所示:
// matches "Qt rocks", but also "QT rocks", "QT ROCKS", "qT rOcKs", etc. QRegularExpression re("Qt rocks", QRegularExpression::CaseInsensitiveOption);
或者,你可以在一个已经存在的QRegularExpressionObject上使用setPatternOptions()函数:
QRegularExpression re("^\\d+$"); re.setPatternOptions(QRegularExpression::MultilineOption); // re matches any line in the subject string that contains only digits (but at least one)
使用patternOptions()函数可以获得QRegularExpression对象当前设置的模式选项:
QRegularExpression re = QRegularExpression("^two.*words$", QRegularExpression::MultilineOption | QRegularExpression::DotMatchesEverythingOption); QRegularExpression::PatternOptions options = re.patternOptions(); // options == QRegularExpression::MultilineOption | QRegularExpression::DotMatchesEverythingOption
有关每个模式选项的更多信息,请参阅QRegularExpression::PatternOption enum文档。
四、匹配类型和匹配选项
match() 和 globalMatch() 函数的最后两个参数设置了匹配类型和匹配选项。match类型是QRegularExpression::MatchType enum的值;使用NormalMatch匹配类型(默认值)选择“传统”匹配算法。还可以启用正则表达式对主题字符串的部分匹配:详细信息请参阅部分匹配部分。
匹配选项是一个或多个QRegularExpression::MatchOption值的集合。它们改变了正则表达式与主题字符串的特定匹配方式。请参阅QRegularExpression::MatchOption枚举文档了解更多细节。
五、正常的匹配
要进行匹配,只需调用match()函数,传入一个要匹配的字符串。我们把这个字符串称为主题字符串。match()函数的结果是一个QRegularExpressionMatch对象,可用于检查匹配的结果。例如:
// match two digits followed by a space and a word QRegularExpression re("\\d\\d \\w+"); QRegularExpressionMatch match = re.match("abc123 def"); bool hasMatch = match.hasMatch(); // true
如果匹配成功,(隐式的)捕获组编号0可以用来检索整个模式匹配的子字符串(参见提取捕获子字符串部分):
QRegularExpression re("\\d\\d \\w+"); QRegularExpressionMatch match = re.match("abc123 def"); if (match.hasMatch()) { QString matched = match.captured(0); // matched == "23 def" // ... }
也可以将偏移量作为参数传递给match()函数,让匹配从主题字符串中的任意偏移量开始。在下例中,不匹配“12abc”,因为匹配从偏移量1开始:
QRegularExpression re("\\d\\d \\w+"); QRegularExpressionMatch match = re.match("12 abc 45 def", 1); if (match.hasMatch()) { QString matched = match.captured(0); // matched == "45 def" // ... }
1. 提取捕获的子字符串
QRegularExpressionMatch对象还包含模式字符串中捕获组捕获的子字符串的信息。captured()函数将返回第n个捕获组捕获的字符串:
QRegularExpression re("^(\\d\\d)/(\\d\\d)/(\\d\\d\\d\\d)$"); QRegularExpressionMatch match = re.match("08/12/1985"); if (match.hasMatch()) { QString day = match.captured(1); // day == "08" QString month = match.captured(2); // month == "12" QString year = match.captured(3); // year == "1985" // ... }
模式中的捕获组从1开始编号,隐式捕获组0用于捕获与整个模式匹配的子串。
也可以使用capturedStart()和capturedEnd()函数来取得每个捕获的子字符串的起始和结束偏移量(在主题字符串中):
QRegularExpression re("abc(\\d+)def"); QRegularExpressionMatch match = re.match("XYZabc123defXYZ"); if (match.hasMatch()) { int startOffset = match.capturedStart(1); // startOffset == 6 int endOffset = match.capturedEnd(1); // endOffset == 9 // ... }
所有这些函数都有一个重载,以QString作为参数,以便提取命名的捕获子字符串。例如:
QRegularExpression re("^(?<date>\\d\\d)/(?<month>\\d\\d)/(?<year>\\d\\d\\d\\d)$"); QRegularExpressionMatch match = re.match("08/12/1985"); if (match.hasMatch()) { QString date = match.captured("date"); // date == "08" QString month = match.captured("month"); // month == "12" QString year = match.captured("year"); // year == 1985 }
六、全局匹配
全局匹配在查找给定正则表达式在主题字符串中的所有出现情况时非常有用。假设我们想从给定字符串中提取所有的单词,其中单词是匹配模式\w+的子字符串。
QRegularExpression::globalMatch返回一个QRegularExpressionMatchIterator对象,这是一个类似java的前向迭代器,可用于迭代结果。例如:
QRegularExpression re("(\\w+)"); QRegularExpressionMatchIterator i = re.globalMatch("the quick fox");
由于它是一个类java的迭代器,QRegularExpressionMatchIterator将指向第一个结果的前面。每个结果都以QRegularExpressionMatch对象的形式返回。如果至少有一个结果,hasNext()函数将返回true,而next()将返回下一个结果并推进迭代器。继续前面的例子:
QStringList words; while (i.hasNext()) { QRegularExpressionMatch match = i.next(); QString word = match.captured(1); words << word; } // words contains "the", "quick", "fox"
你也可以使用 peekNext() 来获得下一个结果,而无需推进迭代器。
可以向 globalMatch() 函数传递一个起始偏移量和一个或多个匹配选项,就像普通的match()匹配一样。
七、部分匹配
当到达主题字符串的结尾时,得到了部分匹配,但要成功完成匹配,需要更多的字符。请注意,部分匹配通常比普通匹配效率低得多,因为无法采用匹配算法的许多优化。
在调用 QRegularExpression::match 或 QRegularExpression::globalMatch 时,必须显式地指定匹配类型PartialPreferCompleteMatch 或PartialPreferFirstMatch来请求部分匹配。如果找到了部分匹配项,那么在 match() 返回的QRegularExpressionMatch对象上调用hasMatch()函数将返回false,而 hasPartialMatch() 将返回true。
当找到部分匹配时,不返回捕获子串,整个匹配对应的(隐式)捕获组0捕获主题字符串的部分匹配子串。
请注意,请求部分匹配仍然可以得到完全匹配(如果找到的话);在这种情况下,hasMatch()将返回true, hasPartialMatch()返回false。QRegularExpressionMatch从来不会同时报告部分匹配和完全匹配。
部分匹配主要适用于两种场景:实时验证用户输入和增量/多段匹配。
1. 验证用户输入
假设我们希望用户输入特定格式的日期,例如“MMM dd, yyyy”。我们可以使用如下模式来检查输入的有效性:
- ^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d\d?, \d\d\d\d$
(此模式不会捕获无效日期,但为了示例的目的,我们保留它)。
我们希望在用户输入时用这个正则表达式验证输入,这样我们就可以在提交输入时报告错误(例如,用户输入了错误的键)。为此,我们必须区分以下三种情况。
- 输入不可能与正则表达式匹配;
- 输入确实与正则表达式匹配;
- 输入现在还不匹配正则表达式,但如果添加更多字符,它就会匹配。
注意,这三种情况完全代表了QValidator的可能状态(参见QValidator::State枚举)。
特别是,在最后一种情况下,我们希望正则表达式引擎报告部分匹配:我们成功地将模式与主题字符串匹配,但匹配不能继续,因为遇到了主题的结尾。但是请注意,匹配算法应该继续并尝试所有的可能性,如果找到了一个完整的(非部分的)匹配,则应该报告这个匹配,并接受输入字符串,认为它完全有效。
这种行为由PartialPreferCompleteMatch匹配类型实现。例如:
QString pattern("^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \\d\\d?, \\d\\d\\d\\d$"); QRegularExpression re(pattern); QString input("Jan 21,"); QRegularExpressionMatch match = re.match(input, 0, QRegularExpression::PartialPreferCompleteMatch); bool hasMatch = match.hasMatch(); // false bool hasPartialMatch = match.hasPartialMatch(); // true
如果与主题字符串匹配相同的正则表达式得到完全匹配,则像往常一样报告:
QString input("Dec 8, 1985"); QRegularExpressionMatch match = re.match(input, 0, QRegularExpression::PartialPreferCompleteMatch); bool hasMatch = match.hasMatch(); // true bool hasPartialMatch = match.hasPartialMatch(); // false
另一个不同模式的例子,展示了完全匹配优于部分匹配的行为:
QRegularExpression re("abc\\w+X|def"); QRegularExpressionMatch match = re.match("abcdef", 0, QRegularExpression::PartialPreferCompleteMatch); bool hasMatch = match.hasMatch(); // true bool hasPartialMatch = match.hasPartialMatch(); // false QString captured = match.captured(0); // captured == "def"
在这个例子中,子模式 abc\w+X 部分匹配主题字符串;然而,子模式def完全匹配主题字符串,因此报告了完全匹配。
如果匹配时发现了多个部分匹配(但没有完全匹配),那么QRegularExpressionMatch对象将报告第一个找到的匹配。例如:
QRegularExpression re("abc\\w+X|defY"); QRegularExpressionMatch match = re.match("abcdef", 0, QRegularExpression::PartialPreferCompleteMatch); bool hasMatch = match.hasMatch(); // false bool hasPartialMatch = match.hasPartialMatch(); // true QString captured = match.captured(0); // captured == "abcdef"
2. 增量/为多段匹配
增量匹配是部分匹配的另一种用例。假设我们想找出一个正则表达式在一个大文本中出现的次数(即匹配该正则表达式的子字符串)。为此,我们希望将大型文本以较小的块“馈送”给正则表达式引擎。一个明显的问题是,如果与正则表达式匹配的子字符串跨越两个或多个块,会发生什么。
在这种情况下,正则表达式引擎应该报告部分匹配,以便我们可以再次匹配,添加新数据,并(最终)得到完全匹配。这意味着正则表达式引擎可能会假设在主题字符串末尾之外还有其他字符。这不是字面上的理解——引擎永远不会尝试访问主题中最后一个字符之后的任何字符。
QRegularExpression 在使用 PartialPreferFirstMatch 匹配类型时实现了这种行为。这种匹配类型一旦找到就报告部分匹配,而不会尝试其他匹配方案(即使它们可能导致完全匹配)。例如:
QRegularExpression re("abc|ab"); QRegularExpressionMatch match = re.match("ab", 0, QRegularExpression::PartialPreferFirstMatch); bool hasMatch = match.hasMatch(); // false bool hasPartialMatch = match.hasPartialMatch(); // true
这是因为在匹配交替操作符的第一个分支时,会找到部分匹配,因此匹配会停止,而不会再尝试第二个分支。另一个例子:
QRegularExpression re("abc(def)?"); QRegularExpressionMatch match = re.match("abc", 0, QRegularExpression::PartialPreferFirstMatch); bool hasMatch = match.hasMatch(); // false bool hasPartialMatch = match.hasPartialMatch(); // true
这显示了量词的一种看似反直觉的行为:since ?是贪婪的,那么引擎在匹配"abc"之后首先尝试继续匹配;但随后匹配到达了主题字符串的末尾,因此报告了部分匹配。在下面的例子中,这更令人惊讶:
QRegularExpression re("(abc)*"); QRegularExpressionMatch match = re.match("abc", 0, QRegularExpression::PartialPreferFirstMatch); bool hasMatch = match.hasMatch(); // false bool hasPartialMatch = match.hasPartialMatch(); // true
如果我们还记得引擎只希望主题字符串是我们要匹配的整个文本的一个子字符串(也就是说,如前所述,引擎假设主题字符串的结尾之外还有其他字符),那么就很容易理解这种行为了。
由于 * 量词是贪婪的,那么报告完全匹配可能会出错,因为在当前主题 “abc” 之后可能还有其他 “abc” 出现。例如,完整文本可以是 “abcabcX” ,因此(在完整文本中)报告的正确匹配应该是 “abcabc” ; 通过只匹配开头的 “abc” ,我们得到了部分匹配。
八、错误处理
QRegularExpression对象可能因为模式字符串中的语法错误而无效。如果正则表达式有效,isValid()函数将返回true,否则返回false:
QRegularExpression invalidRe("(unmatched|parenthesis"); bool isValid = invalidRe.isValid(); // false
你可以通过调用errorString()函数来获得有关特定错误的更多信息。此外,patternErrorOffset()函数将返回模式字符串中的偏移量
QRegularExpression invalidRe("(unmatched|parenthesis"); if (!invalidRe.isValid()) { QString errorString = invalidRe.errorString(); // errorString == "missing )" int errorOffset = invalidRe.patternErrorOffset(); // errorOffset == 22 // ... }
如果试图匹配无效的QRegularExpressionMatch对象,那么返回的QRegularExpressionMatch对象也将无效(也就是说,它的isValid()函数将返回false)。这同样适用于尝试全局匹配。
九、不支持perl兼容的正则表达式特性
QRegularExpression不支持perl兼容的正则表达式中的所有特性。最值得注意的是,不支持捕获组的重复名称,并且使用它们可能会导致未定义的行为。在Qt的未来版本中,这种情况可能会改变。
十、QRegExp用户的注意事项
Qt 5中引入的QRegularExpression类在提供的api、支持的模式语法和执行速度方面比QRegExp有了很大的改进。最大的区别是QRegularExpression只是保存一个正则表达式,在请求匹配时不会修改它。相反,它会返回一个QRegularExpressionMatch对象,用于检查匹配的结果并提取捕获的子字符串。这同样适用于global matching和QRegularExpressionMatchIterator。其他差异概述如下。
1. 不同的模式语法
将正则表达式从QRegExp移植到QRegularExpression可能需要修改模式本身。
在某些情况下,QRegExp过于宽松,接受了在使用QRegularExpression时根本无效的模式。这些模式很容易检测,因为用这些模式构建的QRegularExpression对象是无效的(cf. isValid())。
在其他情况下,从QRegExp移植到QRegularExpression的模式可能会悄无声息地改变语义。因此,有必要回顾一下所使用的模式。沉默的不兼容最明显的例子是:
要使用像\xHHHH这样的十六进制转义,且转义的数字超过2位,需要使用大括号。像\x2022这样的模式需要移植到\x{2022},否则它将匹配一个空格(0x20),后面跟着字符串"22"。一般来说,无论指定多少位数,都强烈建议使用带\x转义的大括号。
像{,n}这样的0到n的量化需要移植到{0,n}以保持语义。否则,像\d{,3}这样的模式实际上匹配的是一个数字后面跟着一个精确的字符串"{,3}"。
默认情况下,QRegExp执行unicode感知的匹配,而QRegularExpression需要一个单独的选项。更多细节见下文。
2. 从QRegExp::exactMatch()移植
Qt 4中的QRegExp::exactMatch()有两个用途:一是将正则表达式与主题字符串进行精确匹配,二是实现部分匹配。
从QRegExp的精确匹配进行移植
精确匹配指定正则表达式是否匹配整个主题字符串。例如,这些类会生成主题字符串"abc123":
QRegExp::exactMatch() | QRegularExpressionMatch::hasMatch() | |
---|---|---|
“\d+” | false | true |
“\d+” | true | true |
精确匹配在QRegularExpression中没有体现。如果你想确保主题字符串与正则表达式完全匹配,可以使用anchoredPattern()函数来包装模式:
QString p("a .*|pattern"); // re matches exactly the pattern string p QRegularExpression re(QRegularExpression::anchoredPattern(p));
在使用QRegExp::exactMatch()时,如果没有找到精确匹配,仍然可以通过调用QRegExp::matchedLength()来找出有多少主题字符串与正则表达式匹配。如果返回的长度等于主题字符串的长度,那么可以断定找到了部分匹配。
QRegularExpression通过适当的MatchType显式地支持部分匹配。
3. Global匹配
由于QRegExp API的限制,无法正确实现全局匹配(也就是说,像Perl那样)。特别是,可以匹配0个字符的模式(如“a*”)是有问题的。
QRegularExpression::globalMatch()正确地实现了Perl全局匹配,并且返回的迭代器可以用来检查每个结果。
4. 支持Unicode属性
在使用QRegExp时,字符类如\w、\d等会匹配具有相应Unicode属性的字符:例如,\d匹配具有Unicode Nd (decimal digit,十进制数字)属性的任何字符。
在使用QRegularExpression时,这些字符类默认只匹配ASCII字符:例如,\d精确匹配0-9 ASCII范围内的字符。使用UseUnicodePropertiesOption模式选项可以改变这种行为。
5. 通配符匹配
在QRegularExpression中没有直接的方法来进行通配符匹配。不过,提供了wildcardToRegularExpression方法来将通配模式转换为perl兼容的正则表达式。
6. 其他模式语法
QRegularExpression仅支持perl兼容的正则表达式。
7. 最小的匹配
QRegExp::setMinimal()通过简单地反转量词的贪心实现最小匹配(QRegExp不支持惰性量词,如*?, + ?等)。而QRegularExpression则支持贪心、懒惰和占有量词。invertedgreedessoption模式选项可以用来模拟QRegExp::setMinimal()的效果:如果启用,它会反转量词的贪婪程度(贪婪的量词会变成懒惰的量词,反之亦然)。
插入符号模式
AnchoredMatchOption匹配选项可以用来模拟QRegExp::CaretAtOffset行为。其他的QRegExp::CaretMode模式则没有对应的选项。
十一、调试使用QRegularExpression的代码
QRegularExpression内部使用JIT (just in time compiler,即时编译器)来优化匹配算法的执行。JIT大量使用了自修改代码,这可能会导致Valgrind等调试工具崩溃。如果想使用QRegularExpression(例如,Valgrind的——smc-check命令行选项)调试程序,必须启用所有自修改代码的检查。启用这种检查的缺点是程序运行速度会慢得多。
为了避免这种情况,如果你在调试模式下编译Qt, JIT默认是禁用的。通过将QT_ENABLE_REGEXP_JIT环境变量分别设置为非零或零值,可以覆盖默认值并启用或禁用JIT使用(调试模式或释放模式)。
总结
到此这篇关于Qt扫盲篇之QRegularExpression正则匹配总结的文章就介绍到这了,更多相关Qt QRegularExpression正则匹配内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!