Technický článek

REGEXTEST a REGEXEXTRACT v Delphi přes statické PCRE2

HotXLS vyhodnocuje funkce regulárních výrazů z Microsoft 365 REGEXTEST, REGEXEXTRACT a REGEXREPLACE uvnitř vlastního vzorcového enginu v Delphi, bez instalace Excelu a bez systémové regex DLL. Kalkulátor HotXLS staticky linkuje zamčený 16bitový build PCRE2 10.49, matchuje UTF-16 text přímo, broadcastuje přes pole a účtuje každý krok backtrackingu do rozpočtu zdrojů vzorce

Ta poslední část je důvod, proč tenhle článek existuje. Sešit, který přijde od zákazníka, je nedůvěryhodný vstup a regulární výraz uvnitř vzorce je kód, který zákazník napsal. Vzor plus text stačí, aby zabavil jádro CPU na minuty katastrofickým backtrackingem, klasický ReDoS tvar (a+)+$ proti dlouhé řadě znaků a s závěrečným b. Server, který přepočítává nahrávané sešity, si to nemůže dovolit, takže zajímavá práce v HotXLS 2.384.112 nebylo volání PCRE2. Bylo to udělat PCRE2 zastavitelné, zúčtovatelné a identické na každém cíli

Co vrací REGEXTEST, REGEXEXTRACT a REGEXREPLACE v HotXLS?

Vzorcový engine HotXLS následuje publikované signatury Microsoftu a sémantika argumentů je ta, kterou už znáte z Excelu. Matchování je defaultně citlivé na velikost písmen. Závěrečný argument case_sensitivity bere 0 pro citlivost na velikost písmen a 1 pro necitlivost a jakákoli jiná hodnota vrací #VALUE!. Inline volby PCRE2 jako (?i) nebo (*UCP) taky fungují, když radši chcete chování držet uvnitř vzoru

REGEXTEST(text, pattern, [case_sensitivity]) vrací Boolean a nepovedený match je prostě FALSE, nikdy chyba. REGEXEXTRACT(text, pattern, [return_mode], [case_sensitivity]) má tři módy: 0 vrací první kompletní match, 1 vrací každý kompletní match jako svislé pole a 2 vrací capture skupiny prvního matchu jako vodorovné pole. Žádný match vrací #N/A, mód 2 na vzoru bez capture skupin vrací #VALUE! a volitelná skupina, která se nezúčastnila, dá prázdný řetězec místo chyby

REGEXREPLACE(text, pattern, replacement, [occurrence], [case_sensitivity]) používá syntaxi náhrad PCRE2, takže $1 a ${name} odkazují na capture. occurrence rovné 0 nahradí každý match, kladná hodnota vybere n-tý match od začátku a záporná počítá od konce. Neexistuje-li vybraný výskyt, HotXLS vrátí původní text beze změny. Číselné selektory se zaokrouhlují k nule, stejně jako Excel zachází se zlomkovými celočíselnými argumenty

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, když buňka drží číslo objednávky
    Sheet.Cells[2, 2].Formula := 'REGEXTEST(A2, "PO-\d{4}-\d{5}")';

    // První kompletní match: "PO-2026-00417"
    Sheet.Cells[2, 3].Formula := 'REGEXEXTRACT(A2, "PO-\d{4}-\d{5}")';

    // Vyměň rok a pořadové číslo číslovanými capture: "PO-00417-2026 ..."
    Sheet.Cells[2, 4].Formula :=
      'REGEXREPLACE(A2, "PO-(\d{4})-(\d{5})", "PO-$2-$1")';

    // Test necitlivý na velikost písmen závěrečným argumentem
    Sheet.Cells[2, 5].Formula := 'REGEXTEST(A2, "graz", 1)';

    Book.Recalculate;
    Book.SaveAs('orders.xlsx');
  finally
    Book.Free;
  end;
end;

Text vzorce v Delphi je obyčejný string literal, takže \d nepotřebuje na Pascal straně escapování. Jediné, co na tom záleží, jsou dvojité uvozovky Excelu uvnitř vzorce a ty se řídí obyčejnými pravidly Excelu. Při uložení zapisovač XLSX uloží tyto názvy s prefixem budoucích funkcí _xlfn., který Excel očekává, a znovuotevřený sešit je přepočítá znovu. Klasický engine BIFF8 umí funkce vyhodnotit v kontextovém volání, ale nemůže je zapsat jako nativní tokeny vzorců .xls, takže regex vzorce nechávejte v XLSX výstupu

Jak funguje broadcasting polí u regex vzorců?

REGEXTEST, REGEXREPLACE a mód 0 REGEXEXTRACT broadcastují přes kompatibilní operandy, což z nich dělá užitečné nástroje na reálných sloupcích. Každý argument může být skalár, řádek, sloupec nebo obdélník a HotXLS bere největší počet řádků a sloupců jako tvar výsledku. Operand, jehož rozměr není ani 1, ani to maximum, donutí celé volání vrátit #VALUE!, obyčejná neshoda polí Excelu. Takže REGEXTEST(A2:A500, "^[A-Z]{2}\d{9}$") dá 499 Booleanů a sloupec vzorů proti jediné textové buňce funguje stejně v druhém směru

Módy 1 a 2 REGEXEXTRACT broadcast odmítají. Každý element by vyprodukoval vektor jiné délky a roztřepená sada vektorů neumí obsadit obdélníkový výsledek vzorce. Místo potichu useknout nebo doplnit hlásí HotXLS volání jako nepodporované. Potřebujete-li všechny matchy z mnoha buněk, iterujte buňky a extrahujte po jedné, nebo přetvarujte problém pomocí closure LET a LAMBDA ve vzorcovém enginu HotXLS, aby každé volání closure vidělo jednu textovou hodnotu

Umístění chyb uvnitř výsledku broadcastu je záměrné. Chyba Excelu pro jeden element, řekněme #N/A z extrakce, která nic nenašla v řádku 37, přistane v té jedné buňce výsledku, zatímco ostatní buňky si drží své hodnoty. Limit zdrojů nebo zrušení je jiné: zastaví celé volání a nikdy nezveřejní částečně zaplněné pole, protože polovina výsledku, která vypadá kompletně, je horší než explicitní selhání

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;   // strop práce, kterou smí tento vzorec udělat

    // Mód 2: capture skupiny prvního matchu jako pole 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 je správný vstupní bod, když vzor přichází odtud, co nekontrolujete. Vyhodnotí vzorec, jako by žil na zadaném řádku a sloupci, vrátí celé pole místo jedné cacheované hodnoty buňky a bere TXLSFormulaEvaluationOptions s explicitní kontrolou kroků, paměti polí a zrušení. O zbytku té evaluační API píše průvodce vzorcovým enginem a vlastními funkcemi HotXLS

Jak HotXLS zastaví vzor s katastrofickým backtrackingem?

HotXLS zastavuje ujeté vzory tím, že donutí PCRE2 hlásit se zpět do kontextu vzorce v každém kroku. Backend kompiluje každý vzor s PCRE2_AUTO_CALLOUT, což donutí PCRE2 volat Pascal callback u každé položky vzoru během matchování. Ten callback účtuje jednu jednotku práce aktivnímu evaluačnímu stavu, kontroluje příznak zrušení a kontroluje rezidentní paměť. Řekne-li kterýkoli z nich stop, callback vrátí callout-abort kód PCRE2 a match se uklidí uvnitř PCRE2. Vzorec se projeví jako xlfeResourceLimit nebo stav zrušení, ne jako zavěšené vlákno

Paměť se měří stejně. Každý kontext PCRE2 vzniká s instančním obecným kontextem, jehož alokátor je Pascal funkce, takže zkompilovaný vzor, match data i interní backtracking rámce PCRE2 se všechny alokují přes HotXLS. Každý blok nese malou hlavičku a je zarovnaný na 16 bajtů a jeho velikost se přičítá do běžícího součtu, se kterým kontrola rozpočtu srovnává limit paměti polí vzorce. Výstupní buffery, extrakční vektory a náhradní řetězec se přidávají do téhož součtu, což znamená, že vzor, který matchuje miliony drobných řetězců, trefí strop stejně jistě jako ten, který backtrackuje navždy

Rozpočtový callback běží taky pod pojistkou, která má větší váhu, než vypadá. Delphi výjimka vyhozená uvnitř callbacku, který zavolalo PCRE2, se nesmí šířit nahoru, protože mezi callbackem a vaším kódem sedí C zásobníkové rámce zkompilované bez jakékoli znalosti Delphi tabulek výjimek. Rozvíjet se jimi je nejvýš nedefinované a nejhorší poškozený zásobník. Most HotXLS chytí každou výjimku v callbacku, zamkne instanci do stavu přerušeno a vrátí PCRE2 obyčejný chybový kód. Váš kód vidí neúspěšný vzorec, nikdy výjimku, která překročila cizí rámec

Cena je skutečná a stojí za zmínku. Auto callouty dělají matchování pomalejším než holý PCRE2 interpret a HotXLS kompiluje vzor jednou za vyhodnocený element, takže broadcast přes 50 000 buněk zaplatí 50 000 kompilací. Pro hromadnou práci s textem na důvěryhodných datech bude vyhrazený regex průchod v Delphi mimo sešit rychlejší. Uvnitř sešitu, který vám posílají cizí lidé, zvítězí ohraničené a předvídatelné nad rychlým

Proč statický UTF-16 build PCRE2 bez JIT?

Řetězce Excelu jsou UTF-16 a taky hodnoty WideString v Delphi, což z 16bitové knihovny PCRE2 udělalo jediný smysluplný výběr. HotXLS předá text vzorce pcre2_match_16 s nastaveným PCRE2_UTF a každý offset, který se vrátí, je index v UTF-16 kódových jednotkách. Žádná okliká přes UTF-8 se neděje, takže nic nemůže uklouznout na surrogate párech. Tam, kde prázdný match donutí sken postoupit, následuje HotXLS pravidlo global match PCRE2: zkusit neprázdný ukotvený match na stejné pozici, pak postoupit o jednu skalární hodnotu Unicode, což přeskočí dvě kódové jednotky za surrogate pár místo ho rozpůlit

JIT je vypnutý záměrně. JIT kompilátor potřebuje spustitelnou paměť a vlastní JIT zásobník, což zatáhne platformově specifický kód a alokace mimo alokátor na vzorec. Interpret drží každý bajt pod rozpočtem HotXLS, každý match pod calloutem a chová se stejně na všech pěti cílech. Každá instance TXLSRegex, interní třídy za těmito funkcemi, vlastní své kontexty, takže paralelní výpočetní workery nesdílejí žádný měnitelný stav PCRE2

Linkování je místo, kde build dostal neobvyklý obrat. Delphi linkuje C objektové soubory přes {$L} a jeho linker rozluští symboly mezi Pascal unitami a C objekty dost dobře, ale neumí rozluštit libovolné kruhové odkazy mezi oddělenými C objektovými soubory. PCRE2 je dvě tucty překladových jednotek, které si volají na všechny strany. Řešením je unity build: celé PCRE2 se zkompiluje jako jedna překladová jednotka do jednoho objektu pcre2_unity na cíl, takže každý interní odkaz se rozluští uvnitř jediného objektu a Delphi linker vidí jen exportované funkce _16

C runtime byl druhý problém. PCRE2 volá malloc, memcpy, memcmp a kamarády a jiné C backendy linkované do téhož spustitelného souboru už můžou nést vlastní kopie těch názvů. HotXLS preprocessuje PCRE2 tak, aby ta volání šla na privátní názvy jako hotxls_regex_memcpy, a implementuje je v Pascalu. Clang umí i po preprocessingu syntetizovat obyčejná volání memset a memchr, takže i ta dvě existují, uvnitř téže unity. Dekorace symbolů se musí přizpůsobit každému formátu objektu: objekt Win32 OMF očekává C názvy s úvodním podtržítkem, zatímco Pascal deklarace pro objekt Win64 ELF, objekt Win64 COFF a buildy FPC váží holé názvy

Stack probe byly ten nejjemnější oříšek. Funkce C s rámem větším než stránka volá nejdřív probe rutinu, která se dotkne každé stránky 4 KB v pořadí, aby Windows mohly commitovat zásobník přes svou guard stránku. Stub, který prostě jen vrátí, se zlinkuje dobře a projde každým unit testem s malým vzorem, pak spadne poprvé, když hluboký rámec přeskočí guard stránku. HotXLS implementuje skutečné probe v assembleru pro každý formát Windows objektu: __chkstk_noalloc pro objekt Win32, __chkstk pro objekt ve formátu Win64 ELF a ___chkstk_ms pro objekt Win64 COFF. Na Linux x64 a macOS ARM64 linkuje FPC místo toho systémovou C knihovnu a každý jiný cíl FPC hlásí backend jako nedostupný místo potichu spadnout do jiného regex dialektu

Kde jsou hranice?

Regex funkce běží na Windows buildech Delphi a C++Builder a na FPC Linux x64 a macOS ARM64. Na jakémkoli jiném cíli hlásí vzorec nepodporovanou funkci přes kontextové vyhodnocení místo produkovat jiné odpovědi jiným enginem. To je správné selhání: sešit, který počítá jinak na jiné platformě, je chyba, kterou nikdo nenajde, dokud nepřijde audit

I rovnocennost s Excelem má vyhlášenou mez. Signatury a zdokumentované příklady následují stránky Microsoftu pro REGEXTEST, REGEXEXTRACT a REGEXREPLACE, ale build Excelu použitý pro lokální validaci tyto funkce Microsoft 365 zatím nevystavuje. Okrajové případy proto následují explicitní kontrakty PCRE2 a polí popsané výše místo tvrzení, že odpovídá každý build Excelu bit za bit. Závisí-li vaše sešity na ztemnělém koutku, jako specifické chování backreference nebo třída Unicode vlastnosti, testujte ten koutek přímo

FAQ

Potřebuje HotXLS k výpočtu REGEXTEST Excel nebo regex DLL?

Ne. HotXLS linkuje statický objekt UTF-16 PCRE2 do vašeho spustitelného souboru a vyhodnocuje REGEXTEST, REGEXEXTRACT a REGEXREPLACE ve vlastním vzorcovém enginu v Delphi, bez Excelu, COM a externí DLL

Může mi škodlivý vzor zavěsit server?

Ne za nakonfigurovaným rozpočtem. Každý krok matchování prochází calloutem, který účtuje kontext vzorce, takže ReDoS vzor skončí s xlfeResourceLimit nebo stavem zrušení a bez částečného výsledku

Proč selhává mód 1 REGEXEXTRACT na argumentu rozsahu?

Každá buňka může vrátit jiný počet matchů a ty roztřepené vektory nedokážou vytvořit obdélníkový výsledek. HotXLS hlásí volání jako nepodporované místo doplňovat nebo usekávat, takže extrahujte po jedné buňce

HotXLS dodává regex funkce po boku LAMBDA, GROUPBY, PIVOTBY a zbytku svého výpočetního enginu pro Delphi, C++Builder a Lazarus. Detaily, edice a zkušební stažení najdete na stránce komponenty HotXLS Delphi spreadsheet