正则表达式速查手册:工作中真正用得上的模式

10 min2026年5月12日

为什么还需要一份正则速查手册?

网上能找到的正则速查手册基本都在罗列语法——锚点、量词、字符类——但从来不给你工作中真正会复制粘贴的模式。这份不一样。它按任务组织:验证邮箱、提取 URL、校验密码强度、解析日志行。每个模式都附带解释:为什么这样写,更重要的是,在什么情况下会出问题。

正则表达式的语法自 1994 年 Perl 5 以来基本没变过。核心语法在 JavaScript、Python、Go、Java、.NET 中都一样。差异在于标志位(JavaScript 用 /g,Python 用 re.DOTALL)、前后查找支持(Go 的 RE2 引擎不支持后行断言)和 Unicode 处理。这份手册使用 JavaScript 正则语法,因为大多数 Web 开发者最先接触它,但模式在任何 PCRE 兼容引擎中都能用。

先说一个观点:如果你的正则超过 80 个字符,大概率用解析器或者多个简单正则串联起来会更好。我调试过 300 字符的邮箱验证正则,结果还是漏掉了边界情况。可读性比炫技重要。

核心语法速查

字符匹配:. 匹配除换行外的任意字符。\d 匹配数字 [0-9]。\w 匹配单词字符 [a-zA-Z0-9_]。\s 匹配空白(空格、制表符、换行)。大写版本(\D、\W、\S)匹配相反的集合。反斜杠转义特殊字符:\. 匹配字面量点号。

量词:* 零次或多次。+ 一次或多次。? 零次或一次。{3} 恰好 3 次。{2,5} 2 到 5 次。{3,} 3 次以上。量词默认贪婪(尽可能多匹配)。加 ? 变成懒惰模式:.*? 尽可能少匹配。解析 HTML 或提取引号内容时,贪婪和懒惰的区别至关重要。

锚点和边界:^ 匹配字符串开头(加 /m 标志则匹配行首)。$ 匹配字符串结尾(加 /m 则匹配行尾)。\b 匹配单词边界——\w 和 \W 字符之间的位置。用 \b 避免部分匹配:/\bcat\b/ 匹配 "cat" 但不匹配 "concatenate"。

分组和选择:(abc) 捕获一个组。(?:abc) 分组但不捕获(用于选择时不污染匹配数组)。a|b 匹配 a 或 b。(?=abc) 正向前瞻——匹配 "abc" 前面的位置但不消耗字符。(?<=abc) 正向后顾(不是所有引擎都支持)。

// Quick reference examples
/\d{3}-\d{4}/        // "555-1234" — US phone fragment
/\b\w+@\w+\.\w+\b/  // crude email (don't use in production)
/^https?:\/\//       // starts with http:// or https://
/(?<=\$)\d+\.\d{2}/ // "19.99" after a $ sign (lookbehind)
/"([^"]*)"/ .exec(str)[1]  // extract content between quotes

邮箱验证(说实话的版本)

完全按照 RFC 5322 验证邮箱地址的正则超过 6000 个字符。别用它。实际工作中,你需要的是一个能抓住明显拼写错误、又不会误杀合法地址的正则。我用的是:/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/ ——检查 [email protected] 且没有空格。就这样。

为什么这么简单?因为合法邮箱地址可以包含 + 号([email protected])、本地部分任意位置的点号、引号字符串("weird@chars"@example.com),甚至 IP 地址作为域名(user@[192.168.1.1])。任何足够严格到拒绝"坏"地址的正则,也会拒绝一些合法地址。光 Gmail 就有 18 亿用户,他们的地址包含点号和加号。

我的建议:用正则做宽松的格式校验,然后通过发送确认邮件来验证地址是否真实存在。所有正经的服务都这么做。正则只是第一道过滤,抓住 "user@gmailcom"(少了点)或 "user@@gmail.com"(双 @)这类拼写错误。

一个坑:HTML5 的 input type="email" 使用自己的验证正则(定义在 WHATWG 规范中),跟 RFC 5322 不一样。如果你后端的正则比浏览器的更严格,用户会看到表单提交成功但收到服务端错误。两边都要测。

URL 和路径模式

从自由文本中匹配 URL 出乎意料地难。"简单"方案 /https?:\/\/[^\s]+/ 大多数情况能用,但遇到带括号的 URL(维基百科链接很常见)会失败,还会匹配到尾部标点("访问 https://example.com。"会把句号也抓进去)。更好的版本:/https?:\/\/[^\s<>\"]+[^\s<>\".,;:!?)]/ ——排除常见的尾部标点。

验证用户在表单里输入的 URL,别用正则。用 URL 构造函数:try { new URL(input) } catch { /* invalid */ }。它能处理端口、认证信息、片段标识符、国际化域名等正则覆盖不了的边界情况。正则适合从文本中提取 URL,不适合验证 URL。

文件路径模式因操作系统而异。Windows:/^[A-Z]:\\(?:[^\\/:*?"<>|]+\\)*[^\\/:*?"<>|]*$/ 验证 C:\Users\docs\file.txt 这样的路径。Unix:/^\/(?:[^\/]+\/)*[^\/]+$/ 简单得多,因为非法字符更少。实际工作中,只需检查空字节和路径穿越(../)——这些才是安全相关的验证。

提取查询参数:/[?&]([^=]+)=([^&]*)/ 配合 /g 标志可以拿到键值对。但在 JavaScript 中,URLSearchParams 做这件事更合适。正则适合解析日志或包含 URL 的文本,不适合处理你已经有的 URL 对象。

// Extract all URLs from a block of text
const urlPattern = /https?:\/\/[^\s<>"]+[^\s<>".,;:!?)]/g;
const text = "Visit https://example.com/path?q=1 or http://test.org.";
text.match(urlPattern);
// ["https://example.com/path?q=1", "http://test.org"]

// Validate URL (don't use regex for this)
function isValidUrl(str) {
  try { new URL(str); return true; }
  catch { return false; }
}

// Extract path segments
"/api/v2/users/123/posts".match(/\/([^\/]+)/g);
// ["/api", "/v2", "/users", "/123", "/posts"]

密码强度规则与正则

密码验证是正则真正发光的场景——你在检查字符类的存在,不是在解析结构。经典的"至少 8 位,含大写、小写、数字、特殊字符"翻译成四个前瞻断言:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z\d]).{8,}$/

我更倾向于用多个独立检查而不是一个巨型正则。这样更容易给出具体反馈("需要一个数字")而不是笼统的"密码太弱"。分别检查每个要求:/[a-z]/.test(pw) 检查小写,/[A-Z]/.test(pw) 检查大写,/\d/.test(pw) 检查数字,pw.length >= 12 检查长度。在应用逻辑中组合结果。

NIST 800-63B 指南(2024 年更新)实际上建议不要强制复杂度规则。他们建议最少 8 个字符、对照泄露密码库检查、允许最多 64 个字符。不强制特殊字符,不强制大写。研究表明复杂度规则导致可预测的模式("Password1!"),而单纯的长度要求反而产生更好的熵值。我们的密码生成器工具能创建满足任何策略的随机字符串,不会落入这些模式。

正则做不到的一件事:检查密码是否出现在泄露数据库中。你需要对密码做哈希然后对照 Have I Been Pwned 的 API 检查(k-anonymity 模型,只发送 SHA-1 哈希的前 5 个字符)。世界上没有任何正则能判断 "Correct Horse Battery Staple" 是弱密码——只有字典检查能做到。

// Option A: Single regex with lookaheads
const strongPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z\d]).{8,}$/;
strongPassword.test("Tr0ub4dor&3"); // true

// Option B: Separate checks (recommended — better UX)
function checkPassword(pw) {
  return {
    hasLower: /[a-z]/.test(pw),
    hasUpper: /[A-Z]/.test(pw),
    hasDigit: /\d/.test(pw),
    hasSpecial: /[^a-zA-Z\d]/.test(pw),
    longEnough: pw.length >= 12,
    // NIST recommends 8 minimum, I prefer 12
  };
}

// Check against common patterns (not a substitute for breach DB)
const commonPatterns = /^(password|123456|qwerty|admin)/i;

日志解析与数据提取

这是正则最能体现价值的场景。解析结构化日志行是完美的用武之地——格式可预测,数据是文本,你需要提取特定字段。一行 Apache 访问日志:/^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+) \S+" (\d+) (\d+)/ 一次匹配就能拿到 IP、时间戳、方法、路径、状态码和响应大小。

命名捕获组(ES2018 起可用)让日志解析的可读性大幅提升。不再是 match[1]、match[2],而是 match.groups.ip、match.groups.timestamp。语法是 (?<name>pattern)。超过 2 个捕获组时就用它——未来的你会感谢现在的你。

多行日志条目(比如 Java 堆栈跟踪)需要用 /s 标志(dotAll)让 . 匹配换行,或者用 [\s\S] 作为跨引擎的替代方案。配合懒惰量词:/ERROR[\s\S]*?(?=\n\d{4}-|$)/ 从 ERROR 匹配到下一条日志的时间戳。

性能警告:回溯可能让正则在某些输入上指数级变慢。模式 /(a+)+b/ 在字符串 "aaaaaaaaaaaaaaaaac" 上需要几秒钟,因为引擎尝试了所有可能的方式来分配 a 到内外两层组。如果你在解析不可信输入(用户提交的文本、来源未知的日志文件),使用 RE2 兼容的模式或设置超时。

// Apache access log parsing with named groups
const logPattern = /^(?<ip>\S+) \S+ \S+ \[(?<time>[^\]]+)\] "(?<method>\S+) (?<path>\S+) \S+" (?<status>\d+) (?<size>\d+)/;

const line = '192.168.1.1 - - [21/May/2026:10:15:32 +0000] "GET /api/users HTTP/1.1" 200 1234';
const { groups } = line.match(logPattern);
// groups.ip = "192.168.1.1"
// groups.status = "200"
// groups.path = "/api/users"

// Extract all key=value pairs from a log line
const kvPattern = /(?<key>[\w.]+)=(?<value>"[^"]*"|\S+)/g;
const entries = [...line.matchAll(kvPattern)].map(m => m.groups);

正则不是万能的:什么时候该换工具

HTML 解析:别用正则。Stack Overflow 上那个关于用正则解析 HTML 会召唤 Zalgo 的经典回答之所以好笑,是因为它说的是事实。HTML 不是正则语言——嵌套标签、自闭合元素、属性里的引号嵌套、CDATA 段、注释,这些都会让正则崩溃。浏览器端用 DOMParser,Node.js 用 cheerio 或 jsdom。

JSON 验证:正则无法验证嵌套的 JSON,因为它不能计数匹配的花括号。你可以检查某个东西看起来像 JSON(/^\s*[{\[]/),但无法验证它是否合法。用 try/catch 包裹 JSON.parse()。我们的 JSON 格式化工具能做到这一点并提供正确的错误报告。

算术表达式:任何涉及嵌套括号或运算符优先级的东西都需要解析器,不是正则。如果你在做计算器或公式求值器,看看递归下降解析器或解析器组合子(比如 nearley.js 或 PEG.js)。

自然语言处理:正则可以分词和找简单模式,但无法理解语法、处理歧义或应对上下文。句子分割看起来简单,直到你遇到"张博士去了北京市朝阳区。他下午3点到的。"——每个句号都可能有歧义。超出简单模式匹配的需求,用专业的 NLP 库(spaCy、jieba)。

正则性能优化与常见陷阱

编译一次,多次使用。JavaScript 中把正则定义在循环外面:const pattern = /\d+/g; 而不是放在循环里面。Python 中用 re.compile()。编译开销不大,但在百万次迭代中会累积。我跑过一个基准测试,预编译正则在 1000 万次匹配中比内联正则快 3 倍。

避免灾难性回溯。像 /(.+)+@/ 或 /(a|a)+b/ 这样的模式在不匹配的输入上可能花费指数级时间。规则:永远不要在重叠模式上嵌套量词。如果不确定,用病态输入(几乎匹配的长重复字符串)测试你的正则。regex101.com 的调试器能显示回溯步数。

如果引擎支持,使用原子组或占有量词。Java 和 .NET 中 (?>pattern) 阻止回溯进入该组。JavaScript 目前不支持(提案在 2026 年处于 Stage 3),但你可以重构模式来避免需要它。把 (.*)\d 替换为 ([^\d]*)\d——字符类不能匹配数字,所以没有回溯的余地。

大规模文本处理(扫描 GB 级日志)时,考虑基于有限自动机而非回溯引擎的工具。ripgrep 使用 Rust 的 regex crate(基于 RE2 的方法),处理速度达到 2-5 GB/s。对于复杂模式,它比 grep -P 快 100 倍,因为它永远不回溯。