现代Qt开发教程新手篇1.15——正则与文本处理相关仓库仍然已经开源正在积极火热的建设之中欢迎各位大佬提Issue和PR链接地址https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeQt1. 前言说实话在 Qt5 时代我也还在用 QRegExp毕竟老习惯难改。但后来项目里遇到一个复杂的文本解析需求正则写了一百多字符调试起来简直要命。那时才发现 QRegularExpression 不仅性能更好语法也更接近现代正则标准而且早已不是什么新东西了——Qt6 里 QRegExp 直接被扔到了 Qt5Compat 模块这意味着官方已经明确告诉你别用了。正则表达式这东西学会之前觉得玄学学会之后简直是文本处理的瑞士军刀。无论是验证邮箱格式、从日志中提取 IP 地址还是批量重命名文件一行正则能顶几十行 if-else。但正则也因为其写时爽读时火的恶名被人诟病——尤其是那些一长串符号没有注释的魔法字符串三个月后连你自己都看不懂。所以这篇入门文章我会尽量把基础打扎实不仅告诉你怎么写更重要的是怎么写得能看懂、怎么调试、怎么避免那些坑。我们不会讲所有正则语法——那够写一本书——但会覆盖日常开发中 80% 的场景。2. 环境说明本文基于 Qt 6.10所有示例使用 CMake 3.26 构建系统。QRegularExpression 属于 Qt Core 模块不需要额外链接其他组件。如果你还在使用 QRegExp现在是时候迁移了——Qt6 中 QRegExp 已被移至 Qt5Compat 模块。3. 核心概念3.1 从 QRegExp 到 QRegularExpression先说清楚这个历史包袱。QRegExp 是 Qt 早期的产物它的正则引擎自己实现了一套功能和性能都有限。QRegularExpression 从 Qt5 引入底层用的是 PCREPerl Compatible Regular Expressions库功能更强大语法更标准性能也更好。Qt6 之后官方直接把 QRegExp 踢出了核心模块。如果你还在用#includeQRegExp// Qt6 下这个头文件不在 Core 里了你需要链接 Qt5Compat 模块或者直接改用 QRegularExpression。我强烈建议后者因为早晚会改不如趁早。3.2 QRegularExpression 基础用法最简单的用法就是创建一个正则对象然后用它匹配字符串#includeQRegularExpression// 创建正则对象匹配数字QRegularExpressionre(\\d);if(re.isValid()){qDebug()正则表达式有效;}else{qDebug()错误:re.errorString();}// 匹配字符串QString text价格123元;QRegularExpressionMatch matchre.match(text);if(match.hasMatch()){qDebug()匹配到:match.captured(0);// 123}这里有个细节要注意正则字符串里的反斜杠需要转义所以\\d实际上是正则引擎看到的\d一个或多个数字。如果你觉得两层反斜杠很烦可以用 C11 的原始字符串字面量QRegularExpressionre(R(\d));// 原始字符串不需要转义反斜杠说到 QRegularExpression 和 QRegExp 的区别核心在于两点底层引擎不同QRegExp 用的是 Qt 自研引擎QRegularExpression 用的是 PCRE后者的语法更标准、功能更完整比如支持命名捕获组、向前/向后断言等性能也更好。Qt6 把 QRegExp 移出核心模块已经是很明确的信号了。3.3 匹配模式match()方法默认是从字符串开头开始查找第一个匹配项globalMatch()则会找出字符串中所有匹配项。QString textabc123def456;QRegularExpressionre(\\d);// 单次匹配QRegularExpressionMatch match1re.match(text);qDebug()match1.captured(0);// 123第一个匹配// 全局匹配 - 找所有匹配项QRegularExpressionMatchIterator itre.globalMatch(text);while(it.hasNext()){QRegularExpressionMatch matchit.next();qDebug()match.captured(0);// 输出 123 然后 456}globalMatch()这个方法特别有用比如从日志里提取所有时间戳、所有 IP 地址这类场景。我们看一个实际的练习从文本中提取所有邮箱地址。QString text联系我aliceexample.com 或 bobtest.org;QRegularExpressionemailRe(R([a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,}));QRegularExpressionMatchIterator itemailRe.globalMatch(text);while(it.hasNext()){QRegularExpressionMatch matchit.next();qDebug()找到邮箱:match.captured(0);}关键方法就是globalMatch()做全局迭代、it.hasNext()判断是否还有下一个匹配、match.captured(0)取完整的匹配文本。3.4 捕获组捕获组是正则表达式最强大的功能之一允许你从匹配结果中提取特定部分。捕获组用圆括号()定义// 匹配日期格式 YYYY-MM-DDQRegularExpressiondateRe(R((\d{4})-(\d{2})-(\d{2})));QString text今天是2025-03-17;QRegularExpressionMatch matchdateRe.match(text);if(match.hasMatch()){qDebug()完整日期:match.captured(0);// 2025-03-17qDebug()年份:match.captured(1);// 2025qDebug()月份:match.captured(2);// 03qDebug()日期:match.captured(3);// 17}captured(0)永远是整个匹配的字符串captured(1)、captured(2)依次是各个捕获组。如果你觉得记数字很麻烦可以给捕获组命名// 命名捕获组QRegularExpressiondateRe(R((?year\d{4})-(?month\d{2})-(?day\d{2})));QRegularExpressionMatch matchdateRe.match(今天是2025-03-17);if(match.hasMatch()){qDebug()年份:match.captured(year);// 比 captured(1) 清晰多了qDebug()月份:match.captured(month);qDebug()日期:match.captured(day);}命名捕获组虽然在入门层显得有点高级但我还是强烈推荐一开始就养成这个习惯。一周后你再看代码captured(year)比captured(1)友好太多了。3.5 常用正则模式速查这里列几个开发中常用的模式建议收藏// 邮箱地址简化版QRegularExpressionemailRe(R([\w.%-][\w.-]\.[a-zA-Z]{2,}));// IPv4 地址QRegularExpressionipv4Re(R((\d{1,3}\.){3}\d{1,3}));// 注意这个不验证每个数字是否在 0-255 范围内// URLhttp/httpsQRegularExpressionurlRe(R(https?://[^\s/$.?#].[^\s]*));// 手机号中国大陆简化版QRegularExpressionphoneRe(R(1[3-9]\d{9}));// 十六进制颜色代码 (#RGB 或 #RRGGBB)QRegularExpressioncolorRe(R(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})));// 匹配空白行纯空白或空行QRegularExpressionblankLineRe(R(^\s*$));// 匹配双引号字符串支持转义QRegularExpressionquotedStringRe(R(([^\\]|\\.)*));这些模式覆盖了大部分日常文本处理需求。但记住正则表达式不是万能的比如解析 HTML/XML 这种结构化文本用专门的解析器更好。4. 踩坑预防正则表达式有几个经典的坑新手几乎都会踩一遍。第一个是忘记检查正则表达式有效性。QRegularExpression的构造函数不会抛异常——如果你的正则语法写错了比如括号不匹配它只是默默变成一个无效对象后续的匹配全部失败没有任何明显错误信息。调试的时候你会一脸懵以为是数据的问题结果是正则本身就坏了。所以每次创建正则对象后特别是模式来自用户输入的场景务必检查isValid()有问题的话用errorString()和errorOffset()拿到具体的错误描述和位置。第二个坑是混淆「子串匹配」和「全字符串匹配」。match()检查的是字符串中是否包含匹配模式的内容而不是整个字符串是否完全符合模式。比如你用\d{3}-\d{4}匹配电话号码我的电话是123-4567谢谢也会匹配成功因为字符串里确实包含了这个模式。如果你要验证整个字符串的格式需要用^...$锚定首尾写成^\d{3}-\d{4}$这样只有全字符串符合格式才算匹配。第三个坑是贪婪匹配。正则默认是贪婪的会尽可能多地匹配内容。比如用div.*/div匹配div内容1/divdiv内容2/div贪婪模式下.*会一直吃到最后一个/div结果一个匹配就把两段 div 都吞进去了。解决方法是在量词后面加?变成非贪婪模式div.*?/div这样匹配到第一个/div就停下来。需要精确控制匹配范围时优先用非贪婪量词*?、?、??。我们再看一个调试场景。下面这段代码程序输出密码格式错误但开发者困惑为什么QString passwordabc123;QRegularExpressionpasswordRe([A-Za-z0-9]{8,});// 至少8位字母数字if(passwordRe.match(password).hasMatch()){qDebug()密码格式正确;}else{qDebug()密码格式错误;}问题出在match()是子串匹配。abc123虽然只有 6 个字符不满足{8,}但如果你仔细看——实际上match()在这里确实找不到 8 个连续的字母数字字符所以返回格式错误是正确的行为。但真正的坑在于如果你把密码换成abc12345678913 个字符即使外面加了别的字符比如abc123456789!!!match()也会返回成功。这又回到了前面说的验证完整格式要用^...$锚定。正则写成^[A-Za-z0-9]{8,}$才能保证整个密码字符串都符合要求。5. 练习项目做一个日志分析工具——实现一个简单的命令行程序可以从日志文件中提取特定信息。功能包括提取所有时间戳、提取所有 IP 地址、统计错误日志数量、根据正则表达式过滤日志行。完成标准程序能够读取一个文本格式的日志文件使用 QRegularExpression 实现上述提取功能将结果打印到控制台。时间戳格式至少支持两种常见格式如[2025-03-17 10:30:00]和17/Mar/2025:10:30:00IP 地址提取能够识别 IPv4 格式错误日志通过包含 “ERROR” 或 “WARN” 关键字来识别。几个提示用 QFile 和 QTextStream 逐行读取日志文件为每种格式创建专门的 QRegularExpression 对象globalMatch()配合循环处理每一行中的多个匹配项可以用QHashQString, int统计不同错误类型的出现次数记得检查每个正则对象的isValid()。6. 官方文档参考链接Qt 文档 · QRegularExpression Class —— QRegularExpression 完整 API 参考包含所有匹配选项和枚举类型Qt 文档 · QRegularExpressionMatch Class —— 匹配结果类讲解如何访问捕获组和匹配位置相关阅读通用GUI编程技术——Win32 原生编程实战五十三——子类化与超类化 - 相似度 58%