在解析 Markdown 文档时,图片或链接的尺寸信息往往以修饰符形式附加在目标地址之后。这类修饰符并非标准 CommonMark 的一部分,而是由一些渲染器或静态站点生成器扩展而来,例如在括号内追加 =300x200 或 ?width=300&height=200。如果正则表达式只按固定模式提取 URL 和标题,遇到带尺寸的内容就可能截断错误;反之,如果强制要求尺寸字段存在,又会漏掉不带尺寸的常规写法。本文从正则表达式的条件捕获机制入手,展示如何让同一个表达式兼容两种形态,并准确捕获尺寸数据。

Markdown 链接与尺寸修饰符的几种形态
标准 Markdown 中,图片写作 ,链接写作 [显示文本](目标地址 "可选标题")。这种语法本身没有为图片或链接提供宽度、高度字段,因此一些静态站点生成器和编辑器会扩展出尺寸修饰符。最常见的形式是在目标地址之后附加 =300x200,例如 。也有的方案把尺寸放在标题之后,如 。此外,部分渲染器支持查询参数形式 ,或者使用竖线分隔 。这些写法都不是 CommonMark 标准的一部分,因此在解析时需要根据实际数据格式做出取舍。
当你在编写一个 Markdown 解析器或预处理脚本时,如果希望同时兼容带尺寸和不带尺寸的写法,正则表达式就需要具备条件捕获的能力。所谓条件捕获,并不是要求使用某些引擎特有的条件组语法,而是通过可选分组让某个捕获组在内容存在时参与匹配并返回结果,在内容不存在时跳过且不报错。这样一来,同一个表达式可以覆盖多种输入,提取结果中也能区分 size 字段是否为 undefined 或 None。
条件捕获在正则中的实现方式
正则表达式中,小括号 () 用来创建捕获组,默认情况下匹配到的内容会被保存到结果数组中。如果在右括号后加上问号 ?,就变成了可选捕获组,表示该部分可以出现零次或一次。例如 (=(\d+)x(\d+))? 表示整个尺寸修饰符可以省略,如果存在,则外层组捕获完整的 =300x200,两个内层组分别捕获宽度和高度。这种写法不依赖任何高级语法,几乎所有主流正则引擎都支持。
直接使用数字编号访问捕获组虽然可行,但当表达式较长或需要多次调整顺序时,数字索引很容易出错。很多正则引擎支持命名捕获组,JavaScript 中写作 (?<width>\d+),Python 中同样写作 (?P<width>\d+)。命名捕获让提取逻辑更清晰,也能通过名称判断某个可选字段是否真的存在。例如在 JavaScript 的 match 结果中,如果 size 修饰符缺失,groups.width 的值会是 undefined;在 Python 中则是 None。
条件捕获的另一种实现思路是利用正则引擎的条件组语法,例如某些引擎支持 (?(1)yes-pattern|no-pattern),根据第一个捕获组是否匹配来选择不同的分支。但该语法在 JavaScript 中并不支持,且对可选尺寸这种简单场景来说略显复杂。大多数情况下,可选分组已经足够解决问题,代码可读性也更好。需要注意的是,可选分组必须放在非捕获组 (?:...) 中,避免生成多余的捕获组干扰后续索引。
JavaScript 与 Python 解析示例
先看一个 JavaScript 版本的正则表达式。这个表达式假设尺寸修饰符出现在图片标题之后,格式为等号加宽高,中间用 x 或 X 分隔。代码中使用了两个可选捕获组:一个负责提取标题及其引号,另一个负责提取尺寸信息。
const pattern = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\s*(=(\d+)x(\d+))?\)/;
function parseImage(input) {
const match = input.match(pattern);
if (!match) {
return null;
}
return {
alt: match[1],
src: match[2],
title: match[3] || null,
width: match[5] || null,
height: match[6] || null
};
}
const result = parseImage('');
console.log(result.width); // 300
console.log(result.height); // 200
这个表达式中的 [^)\s]+ 负责匹配 URL,排除了右括号和空白,避免贪婪吞噬。标题部分使用 (?:\s+["'][^"']*["'])? 表示可以出现也可以不出现,出现时需要前后有空白且内容被单双引号包围。尺寸部分 (=(\d+)x(\d+))? 则完整可省略,若存在,外层捕获组包含等号和宽高,内层捕获组分别拿到数字。
Python 版本可以采用类似的思路。下面这个实现使用普通捕获组,并在函数中通过 .group() 访问对应位置。由于 Python 的 re 模块不支持 JavaScript 那样的命名捕获简写,可以用 (?P<name>...) 来命名,但为了保持代码块简洁,这里先用数字索引演示。
import re
pattern = re.compile(
r"!\[([^\]]*)\]"
r"\(([^)\s]+)"
r"(?:\s+([^)]*?))?"
r"(?:\s*=\s*(\d+)\s*[xX]\s*(\d+))?"
r"\)"
)
def parse_markdown_image(text):
m = pattern.search(text)
if not m:
return None
return {
"alt": m.group(1),
"src": m.group(2),
"title": m.group(3),
"width": m.group(4),
"height": m.group(5),
}
sample = ''
info = parse_markdown_image(sample)
print(info["width"]) # 480
print(info["height"]) # 240
两个版本的共同点在于,尺寸捕获组都使用了可选量词 ?。当输入字符串没有尺寸修饰符时,width 和 height 对应的捕获组不会参与匹配,从而返回空值。这样调用方就可以通过判断这些字段是否为空来决定是否应用额外样式或渲染逻辑,而不需要编写两套正则分别处理。
边界情况与性能优化
上述示例主要针对 =300x200 这一种尺寸格式。如果你的 Markdown 源文件中混用了查询参数形式 ?width=300&height=200 或竖线分隔形式 |300x200,单一正则很难优雅地全部覆盖。更好的做法是先统计项目中实际出现的尺寸写法,再决定是编写多个独立正则分支,还是在解析前做简单的字符串标准化。例如可以先用一个正则提取括号内的完整内容,再在代码中对尺寸部分进行二次解析,逻辑更清晰也更容易维护。
正则匹配可能遇到回溯性能问题。表达式中的字符类 [^)] 比万能点号 . 更安全,因为它能避免跨过右括号继续匹配。还应尽量少用嵌套量词,比如 (.*?)+ 这种组合会引发灾难性回溯。如果需要在 Python 中进一步提升性能,可以考虑使用第三方 regex 模块的原子组或占有量词,但需注意兼容性。对于 JavaScript,保持表达式简单线性即可。
另一个容易忽视的问题是标题中的引号。如果标题本身包含单引号或双引号,使用 ["'][^"']*["'] 这种模式可能会提前终止。此时可以改用非贪婪匹配或明确指定转义规则。Markdown 中图片标题较少出现复杂引号嵌套,但如果你在解析用户生成的内容,建议对标题字段做单独转义处理,或直接采用 Markdown 解析库生成 AST 后再提取尺寸。