HotXLS 在自己的 Delphi 公式引擎里求值 Microsoft 365 的正则函数 REGEXTEST、REGEXEXTRACT 和 REGEXREPLACE,不需要安装 Excel,也不依赖任何系统 regex DLL。HotXLS 计算器静态链接一个钉死版本的 16 位 PCRE2 10.49 构建,直接匹配 UTF-16 文本,跨数组广播,并把每一次回溯步都计入公式的资源预算
最后这一点正是本文存在的原因。客户送来的工作簿是不可信输入,而公式里的正则表达式是客户写的代码。一个模式加一段文本,就足以靠灾难性回溯把一个 CPU 核钉住几分钟——经典的 ReDoS 形态 (a+)+$ 对着一长串 a 字符加一个结尾的 b。重算上传工作簿的服务器承受不起这个,所以 HotXLS 2.384.112 里真正有意思的工作不是调用 PCRE2,而是让 PCRE2 可停止、可记账、在每个目标平台上行为一致
REGEXTEST、REGEXEXTRACT 和 REGEXREPLACE 在 HotXLS 里返回什么
HotXLS 公式引擎遵循 Microsoft 公布的签名,参数语义与你在 Excel 里熟悉的相同。匹配默认区分大小写。最后的 case_sensitivity 参数取 0 为区分大小写、1 为不区分,其它值返回 #VALUE!。若你更愿意把行为留在模式里,(?i) 或 (*UCP) 这类 PCRE2 内联选项同样有效
REGEXTEST(text, pattern, [case_sensitivity]) 返回布尔值,匹配失败就是 FALSE,绝不是错误。REGEXEXTRACT(text, pattern, [return_mode], [case_sensitivity]) 有三种模式:0 返回第一个完整匹配,1 把每个完整匹配作为纵向数组返回,2 把第一个匹配的捕获组作为横向数组返回。无匹配返回 #N/A;模式 2 遇到没有捕获组的模式返回 #VALUE!;未参与的可选组产出空字符串而不是错误
REGEXREPLACE(text, pattern, replacement, [occurrence], [case_sensitivity]) 使用 PCRE2 替换语法,$1 和 ${name} 引用捕获组。occurrence 为 0 替换所有匹配,正值从头数第 n 个匹配,负值从尾部数。所选匹配不存在时,HotXLS 原样返回原文。数字选择器向零截断,与 Excel 处理小数整数参数的方式一致
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Orders');
Sheet.Cells[2, 1].Value := 'PO-2026-00417 shipped to Graz';
// 单元格装着采购单号时为 TRUE
Sheet.Cells[2, 2].Formula := 'REGEXTEST(A2, "PO-\d{4}-\d{5}")';
// 第一个完整匹配:"PO-2026-00417"
Sheet.Cells[2, 3].Formula := 'REGEXEXTRACT(A2, "PO-\d{4}-\d{5}")';
// 用编号捕获交换年份和流水号:"PO-00417-2026 ..."
Sheet.Cells[2, 4].Formula :=
'REGEXREPLACE(A2, "PO-(\d{4})-(\d{5})", "PO-$2-$1")';
// 用最后一个参数做不区分大小写的测试
Sheet.Cells[2, 5].Formula := 'REGEXTEST(A2, "graz", 1)';
Book.Recalculate;
Book.SaveAs('orders.xlsx');
finally
Book.Free;
end;
end;
Delphi 里的公式文本就是普通字符串字面量,所以 \d 在 Pascal 侧不需要转义。要操心的只有公式内的 Excel 双引号,它们遵循普通 Excel 规则。保存时,XLSX 写入器会给这些名字加上 Excel 期望的 _xlfn. 未来函数前缀,重新打开的工作簿会再次重算它们。经典 BIFF8 引擎能在上下文调用中求值这些函数,但无法把它们写成原生 .xls 公式 token,所以正则公式请留在 XLSX 输出里
正则公式的数组广播如何工作
REGEXTEST、REGEXREPLACE 和 REGEXEXTRACT 的模式 0 会在兼容操作数上广播,这正是它们在真实数据列上有用的原因。每个参数可以是标量、行、列或矩形,HotXLS 取最大的行数和列数作为结果形状。某个操作数的维度既不是 1 也不是该最大值时,整个调用返回 #VALUE!——常见的 Excel 数组不匹配。于是 REGEXTEST(A2:A500, "^[A-Z]{2}\d{9}$") 产出 499 个布尔值,一列模式对单个文本单元格反方向同理
REGEXEXTRACT 的模式 1 和 2 拒绝广播。每个元素会产出长度不同的向量,参差的向量组占不了一个矩形的公式结果。HotXLS 不悄悄截断或填充,而是把该调用报告为不支持。要从许多单元格里取全部匹配,请逐个单元格迭代提取,或者用HotXLS 公式引擎中的 LET 与 LAMBDA 闭包重塑问题,让每次闭包调用只见到一个文本值
广播结果内部的错误位置是刻意设计的。某个元素的 Excel 错误——比如第 37 行的提取一无所获得到的 #N/A——只落在结果的那一个格子里,其余单元格保留自己的值。资源限制或取消则不同:它会终止整个调用,绝不发布填了一半的数组,因为看起来完整的半个结果比明确的失败更糟
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Opts: TXLSFormulaEvaluationOptions;
Eval: TXLSFormulaEvaluationResult;
I: Integer;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Log');
Sheet.Cells[1, 1].Value := 'GET /api/v2/orders 200 18ms';
Opts := XLSDefaultFormulaEvaluationOptions;
Opts.MaxEvaluationSteps := 200000; // 给这个公式能做的工作设上限
// 模式 2:第一个匹配的捕获组作为 1 x 3 数组
Eval := Sheet.EvaluateFormulaAt(1, 2,
'REGEXEXTRACT(A1, "(GET|POST) (\S+) (\d{3})", 2)', xlfrsA1, Opts);
case Eval.Status of
xlfeOk:
if VarIsArray(Eval.Value) then
for I := VarArrayLowBound(Eval.Value, 2) to
VarArrayHighBound(Eval.Value, 2) do
Log(VarToStr(Eval.Value[0, I]));
xlfeResourceLimit:
Log('Pattern exceeded its budget: ' + Eval.Issue.Message);
else
Log('Evaluation failed: ' + Eval.Issue.Message);
end;
finally
Book.Free;
end;
end;
模式来自你控制不了的来源时,TXLSXWorksheet.EvaluateFormulaAt 是正确的入口。它把公式当作位于指定行列来求值,返回完整数组而不是单个缓存单元格的值,并接受带显式步数、数组内存和取消控制的 TXLSFormulaEvaluationOptions。这套求值 API 的其余部分见HotXLS 公式引擎与自定义函数指南
HotXLS 怎么拦住灾难性回溯的模式
HotXLS 让 PCRE2 每走一步都向公式上下文回报,从而拦住失控的模式。后端用 PCRE2_AUTO_CALLOUT 编译每个模式,匹配时 PCRE2 会在每个模式项处调用一个 Pascal 回调。该回调向活动求值状态记一笔工作量,检查取消标志,检查常驻内存。任何一项说停,回调就返回 PCRE2 的 callout 中止码,匹配在 PCRE2 内部干净地退栈。公式浮出为 xlfeResourceLimit 或取消状态,而不是一条挂死的线程
内存用同样的方式计量。每个 PCRE2 上下文都带一个按实例创建的 general context,其分配器是一个 Pascal 函数,于是编译后的模式、匹配数据和 PCRE2 内部的回溯帧全都经 HotXLS 分配。每个块带一个小头部、按 16 字节对齐,其大小累加进一个累计值,预算检查拿它对照公式的数组内存上限。输出缓冲、提取向量和替换字符串计入同一个总数,这意味着匹配出几百万个微小字符串的模式,和永远回溯的模式一样稳稳撞上天花板
预算回调还跑在一层看起来不起眼、实则要紧的保护之下。在 PCRE2 调进来的回调里抛出的 Delphi 异常绝不能向上传播,因为回调和你的代码之间夹着 C 栈帧,那些帧编译时对 Delphi 异常表一无所知。穿过它们退栈,往好里说是未定义行为,往坏里说是栈损坏。HotXLS 桥接层在回调里捕获一切异常,把实例锁进中止状态,然后向 PCRE2 返回普通错误码。你的代码看到的是一次失败的求值,绝不会是穿越外部栈帧的异常
代价是真实的,值得说透。Auto callout 让匹配比裸 PCRE2 解释器慢,而且 HotXLS 对每个求值元素编译一次模式,5 万个单元格的广播就要付 5 万次编译。对可信数据的批量文本处理,工作簿外专门的 Delphi regex 趟会更快;在陌生人发给你的工作簿里,有界且可预测胜过快
为什么是无 JIT 的静态 UTF-16 PCRE2 构建
Excel 字符串是 UTF-16,Delphi 的 WideString 也是,16 位 PCRE2 库因此成了唯一明智的选择。HotXLS 把公式文本交给设了 PCRE2_UTF 的 pcre2_match_16,回来的每个偏移都是 UTF-16 码元索引。全程没有 UTF-8 往返,代理对上不会漂移。空匹配迫使扫描前进时,HotXLS 遵循 PCRE2 的全局匹配规则:同一位置重试一次非空锚定匹配,然后前进一个 Unicode 标量值——跨过代理对要移动两个码元,而不是把它劈开
JIT 是有意关掉的。JIT 编译器需要可执行内存和自己的 JIT 栈,这会引入平台专属代码和绕开按公式分配器的分配。解释器让每个字节都在 HotXLS 预算之内,让每次匹配都处于 callout 之下,并且在全部五个目标上行为一致。每个 TXLSRegex 实例——这些公式背后的内部类——拥有自己的上下文,并行计算 worker 之间不共享任何可变的 PCRE2 状态
链接环节是构建里最不寻常的部分。Delphi 用 {$L} 链接 C 目标文件,它的链接器在 Pascal 单元和 C 目标之间解析符号没什么问题,但不会解析独立 C 目标文件之间任意的循环引用。PCRE2 是二十来个互相从每个方向调用的翻译单元。解法是 unity build:整个 PCRE2 编译成单个翻译单元,每个目标产出一个 pcre2_unity 目标文件,所有内部引用在单一目标内解析,Delphi 链接器只见到导出的 _16 函数
C 运行时是第二个问题。PCRE2 要调 malloc、memcpy、memcmp 等,而链进同一可执行文件的其它 C 后端可能已经带着自己那份同名符号。HotXLS 对 PCRE2 做了预处理,让这些调用改走 hotxls_regex_memcpy 这样的私有名字,并用 Pascal 实现。预处理之后 Clang 仍可能合成普通的 memset 和 memchr 调用,所以这两个也在同一单元里存在。符号修饰必须匹配各目标格式:Win32 OMF 目标期望带下划线前缀的 C 名字,而 Win64 ELF 目标、Win64 COFF 目标和 FPC 构建的 Pascal 声明绑定裸名字
栈探测是最微妙的一个。栈帧大于一页的 C 函数会先调一个探测例程,按顺序触碰每个 4 KB 页,让 Windows 能经由保护页提交栈。一个直接返回的桩链接得上,也能通过所有用小模式的单元测试,然后某个深帧第一次跳过保护页时崩溃。HotXLS 为每种 Windows 目标格式用汇编实现了真探测:Win32 目标用 __chkstk_noalloc,Win64 ELF 格式目标用 __chkstk,Win64 COFF 目标用 ___chkstk_ms。Linux x64 和 macOS ARM64 上,FPC 改链系统 C 库;其它所有 FPC 目标都把该后端报告为不可用,而不是悄悄退回另一种正则方言
边界在哪里
这些正则函数运行在 Delphi 和 C++Builder 的 Windows 构建以及 FPC 的 Linux x64 与 macOS ARM64 上。其它任何目标上,公式经上下文求值报告函数不受支持,而不是用另一个引擎产出不同的答案。这才是正确的失败方式:同一份工作簿在不同平台算出不同结果,是那种直到审计才有人发现的 bug
Excel 等价性也有明说的边界。签名和文档示例遵循 Microsoft 的 REGEXTEST、REGEXEXTRACT 和 REGEXREPLACE 页面,但本地验证用的 Excel 构建尚未暴露这些 Microsoft 365 函数。因此边界情形遵循上文明确的 PCRE2 与数组契约,而不是宣称与每个 Excel 构建逐位一致。如果你的工作簿依赖某个冷僻角落——某个特定的反向引用行为或 Unicode 属性类——请直接测试那个角落
FAQ
HotXLS 计算 REGEXTEST 需要 Excel 或 regex DLL 吗
不需要。HotXLS 把静态 UTF-16 PCRE2 目标链接进你的可执行文件,在自己的 Delphi 公式引擎里求值 REGEXTEST、REGEXEXTRACT 和 REGEXREPLACE,无 Excel、无 COM、无外部 DLL
恶意模式能把我的服务器挂死吗
过不了配置的预算。每个匹配步骤都经过向公式上下文记账的 callout,所以 ReDoS 模式最终以 xlfeResourceLimit 或取消状态收场,且没有部分结果
REGEXEXTRACT 模式 1 遇到范围参数为什么失败
每个单元格可能返回数量不同的匹配,参差的向量组不成矩形。HotXLS 把该调用报告为不支持,而不是填充或截断,所以请逐个单元格提取
HotXLS 的正则函数与 LAMBDA、GROUPBY、PIVOTBY 以及其余计算引擎一道,面向 Delphi、C++Builder 和 Lazarus 提供。详情、版本和试用下载见 HotXLS Delphi spreadsheet component 页面