HotXLS liczy funkcje wyrażeń regularnych Microsoft 365 REGEXTEST, REGEXEXTRACT i REGEXREPLACE we własnym silniku formuł Delphi, bez instalacji Excela i bez systemowej biblioteki regex. Kalkulator HotXLS linkuje statycznie przypięty 16-bit build PCRE2 10.49, dopasowuje tekst UTF-16 wprost, robi broadcasting po tablicach i każdy krok backtrackingowy obciąża na budżet zasobów formuły
Ta ostatnia część to powód istnienia tego artykułu. Skoroszyt, który przychodzi od klienta, to niezaufane wejście, a wyrażenie regularne w formule to kod napisany przez klienta. Wzorzec plus tekst wystarczą, żeby przygwoździć rdzeń CPU na minuty katastrofalnym backtrackingiem, klasykowy kształt ReDoS (a+)+$ przeciw długiej serii znaków a z końcowym b. Serwer przeliczający uploadowane skoroszyty nie może sobie na to pozwolić, więc ciekawa praca w HotXLS 2.384.112 nie polegała na wołaniu PCRE2. Chodziło o to, by uczynić PCRE2 zatrzymywalnym, rozliczalnym i identycznym na każdym celu
Co zwracają REGEXTEST, REGEXEXTRACT i REGEXREPLACE w HotXLS?
Silnik formuł HotXLS trzyma się opublikowanych sygnatur Microsoft, a semantyka argumentów jest taka, jaką znasz z Excela. Dopasowanie rozróżnia wielkość liter domyślnie. Końcowy argument case_sensitivity bierze 0 dla rozróżniania wielkości liter i 1 dla braku rozróżniania, a każda inna wartość zwraca #VALUE!. Wbudowane opcje PCRE2, jak (?i) albo (*UCP), też działają, gdy wolisz trzymać zachowanie wewnątrz wzorca
REGEXTEST(text, pattern, [case_sensitivity]) zwraca Boolean, a nieudane dopasowanie to po prostu FALSE, nigdy błąd. REGEXEXTRACT(text, pattern, [return_mode], [case_sensitivity]) ma trzy tryby: 0 zwraca pierwsze pełne dopasowanie, 1 zwraca każde pełne dopasowanie jako tablicę pionową, a 2 zwraca grupy przechwytujące pierwszego dopasowania jako tablicę poziomą. Brak dopasowania zwraca #N/A, tryb 2 na wzorcu bez grup przechwytujących zwraca #VALUE!, a opcjonalna grupa, która nie uczestniczyła, daje pusty string zamiast błędu
REGEXREPLACE(text, pattern, replacement, [occurrence], [case_sensitivity]) używa składni zamienników PCRE2, więc $1 i ${name} odwołują się do przechwyceń. occurrence równe 0 podmienia każde dopasowanie, wartość dodatnia wybiera n-te dopasowanie od początku, a ujemna liczy od końca. Gdy wybrane wystąpienie nie istnieje, HotXLS zwraca oryginalny tekst bez zmian. Selektory liczbowe są obcinane w stronę zera, tak samo jak Excel traktuje ułamkowe argumenty całkowite
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, gdy komórka trzyma numer zamówienia zakupowego
Sheet.Cells[2, 2].Formula := 'REGEXTEST(A2, "PO-\d{4}-\d{5}")';
// Pierwsze pełne dopasowanie: "PO-2026-00417"
Sheet.Cells[2, 3].Formula := 'REGEXEXTRACT(A2, "PO-\d{4}-\d{5}")';
// Zamiana roku i sekwencji przez ponumerowane grupy: "PO-00417-2026 ..."
Sheet.Cells[2, 4].Formula :=
'REGEXREPLACE(A2, "PO-(\d{4})-(\d{5})", "PO-$2-$1")';
// Test bez rozróżniania wielkości liter ostatnim argumentem
Sheet.Cells[2, 5].Formula := 'REGEXTEST(A2, "graz", 1)';
Book.Recalculate;
Book.SaveAs('orders.xlsx');
finally
Book.Free;
end;
end;
Tekst formuły w Delphi to zwykły literał string, więc \d nie potrzebuje escapowania po stronie Pascala. Liczą się tylko podwójne cudzysłowy Excela wewnątrz formuły i one podlegają zwykłym regułom Excela. Przy zapisie writer XLSX przechowuje te nazwy z prefiksem funkcji przyszłej _xlfn., którego Excel oczekuje, a ponownie otwarty skoroszyt przelicza je jeszcze raz. Klasyczny silnik BIFF8 potrafi policzyć te funkcje w wywołaniu kontekstowym, ale nie potrafi zapisać ich jako natywne tokeny formuł .xls, więc trzymaj formuły regex w wyjściu XLSX
Jak działa broadcasting tablic z formułami regex?
REGEXTEST, REGEXREPLACE i tryb 0 REGEXEXTRACT robią broadcasting po zgodnych operandach, co czyni je użytecznymi na prawdziwych kolumnach. Każdy argument może być skalarem, wierszem, kolumną albo prostokątem, a HotXLS bierze największą liczbę wierszy i kolumn jako kształt wyniku. Operand, którego wymiar nie jest ani 1, ani tym maksimum, każe całemu wywołaniu zwrócić #VALUE!, zwykły rozjazd tablic Excela. Więc REGEXTEST(A2:A500, "^[A-Z]{2}\d{9}$") daje 499 Booleanów, a kolumna wzorców przeciw pojedynczej komórce tekstowej działa tak samo w drugą stronę
Tryby 1 i 2 REGEXEXTRACT odmawiają broadcastu. Każdy element produkowałby wektor innej długości, a postrzępiony zestaw wektorów nie zmieści się w prostokątnym wyniku formuły. Zamiast po cichu ucinać albo dociągać, HotXLS raportuje wywołanie jako niewspierane. Jeśli potrzebujesz wszystkich dopasowań z wielu komórek, iteruj komórki i ekstrahuj po jednej, albo przekształć problem przez domknięcia LET i LAMBDA w silniku formuł HotXLS, żeby każde wywołanie domknięcia widziało jedną wartość tekstową
Rozmieszczenie błędów wewnątrz wyniku broadcastu jest świadome. Błąd Excela dla jednego elementu, powiedzmy #N/A z ekstrakcji, która nic nie znalazła w wierszu 37, ląduje w tej jednej komórce wyniku, podczas gdy pozostałe komórki trzymają swoje wartości. Limit zasobów albo anulowanie to co innego: zatrzymują całe wywołanie i nigdy nie publikują częściowo wypełnionej tablicy, bo pół wyniku wyglądające na kompletne jest gorsze niż jawna porażka
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; // limit pracy, jaką ta formuła może wykonać
// Tryb 2: grupy przechwytujące pierwszego dopasowania jako tablica 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 to właściwy punkt wejścia, gdy wzorzec przychodzi z miejsca, którego nie kontrolujesz. Liczy formułę tak, jakby mieszkała w danym wierszu i kolumnie, zwraca pełną tablicę zamiast pojedynczej zcache'owanej wartości komórki i bierze TXLSFormulaEvaluationOptions z jawnymi kontrolami kroków, pamięci tablic i anulowania. Resztę tego API ewaluacji opisuje przewodnik po silniku formuł HotXLS i funkcjach własnych
Jak HotXLS zatrzymuje katastrofalny backtracking?
HotXLS zatrzymuje oszalałe wzorce, każąc PCRE2 raportować do kontekstu formuły przy każdym kroku. Backend kompiluje każdy wzorzec z PCRE2_AUTO_CALLOUT, co każe PCRE2 wołać callback Pascala przy każdym elemencie wzorca w trakcie dopasowania. Ten callback obciąża o jedną jednostkę pracy aktywny stan ewaluacji, sprawdza flagę anulowania i sprawdza pamięć rezydentną. Gdy cokolwiek z tego powie stop, callback zwraca kod przerwania callout PCRE2 i dopasowanie schodzi czysto wewnątrz PCRE2. Formuła wychodzi jako xlfeResourceLimit albo status anulowania, nie jako zawieszony wątek
Pamięć jest mierzona tak samo. Każdy kontekst PCRE2 jest tworzony z per-instancyjnym kontekstem ogólnym, którego alokator jest funkcją Pascala, więc skompilowany wzorzec, dane dopasowania i wewnętrzne ramy backtrackingu PCRE2 są wszystkie alokowane przez HotXLS. Każdy blok niesie mały nagłówek i jest wyrównany do 16 bajtów, a jego rozmiar dopisuje się do bieżącej sumy, którą sprawdzenie budżetu porównuje z limitem pamięci tablic formuły. Bufory wyjściowe, wektory ekstrakcji i string zamiennika są dopisywane do tej samej sumy, co znaczy, że wzorzec dopasowujący miliony malutkich stringów dobije sufit równie pewnie jak ten, który backtrackinguje w nieskończoność
Callback budżetu działa też pod strażnikiem, który znaczy więcej, niż wygląda. Wyjątek Delphi podniesiony wewnątrz callbacka, który PCRE2 wywołało, nigdy nie może się propagować w górę, bo między callbackiem a twoim kodem siedzą ramy stosu C skompilowane bez żadnej wiedzy o tabelach wyjątków Delphi. Rozwijanie się przez nie jest w najlepszym razie niezdefiniowane, a w najgorszym zepsutym stosie. Most HotXLS łapie każdy wyjątek w callbacku, zatrzaskuje instancję w stanie przerwania i zwraca PCRE2 zwykły kod błędu. Twój kod widzi nieudaną formułę, nigdy wyjątku, który przeszedł przez obcą ramę
Koszt jest realny i warto go nazwać. Auto callouts czynią dopasowanie wolniejszym niż goły interpreter PCRE2, a HotXLS kompiluje wzorzec raz na liczony element, więc broadcast po 50 000 komórek płaci 50 000 kompilacji. Do masowej pracy tekstowej na zaufanych danych dedykowany przebieg regex w Delphi poza skoroszytem będzie szybszy. Wewnątrz skoroszytu, który przysyłają ci obcy, ograniczone i przewidywalne bije szybkie
Dlaczego statyczny build PCRE2 UTF-16 bez JIT?
Stringi Excela są UTF-16 i wartości WideString Delphi też, co uczyniło 16-bitową bibliotekę PCRE2 jedynym sensownym wyborem. HotXLS podaje tekst formuły do pcre2_match_16 z ustawionym PCRE2_UTF, a każdy wracający offset to indeks jednostki kodu UTF-16. Nie ma powrotnej drogi przez UTF-8, więc nic nie może zejść na parach surrogate. Tam, gdzie puste dopasowanie zmusza skan do przodu, HotXLS trzyma się globalnej reguły dopasowania PCRE2: powtórz niepuste zakotwiczone dopasowanie na tej samej pozycji, a potem zrób krok o jedną wartość skalarną Unicode, co przesuwa o dwie jednostki kodu za parą surrogate, zamiast ją ciąć
JIT jest wyłączony z zamierzenia. Kompilator JIT potrzebuje pamięci wykonywalnej i własnego stosu JIT, co ciągnie kod i alokacje specyficzne dla platformy poza per-formułowym alokatorem. Interpreter trzyma każdy bajt pod budżetem HotXLS, każde dopasowanie pod calloutem i zachowuje się tak samo na wszystkich pięciu celach. Każda instancja TXLSRegex, wewnętrznej klasy za tymi formułami, posiada własne konteksty, więc równoległe workerzy obliczeń nie dzielą żadnego mutowalnego stanu PCRE2
Linkowanie to miejsce, gdzie build zrobił się nietuzinkowy. Delphi linkuje pliki obiektowe C przez {$L} i jego linker rozwiązuje symbole między jednostkami Pascala a obiektami C dość dobrze, ale nie rozwiązuje dowolnych cyklicznych odwołań między osobnymi plikami obiektowymi C. PCRE2 to dwa tuziny jednostek translacji wołających się w każdą stronę. Rozwiązaniem jest unity build: cały PCRE2 jest kompilowany jako jedna jednostka translacji do jednego obiektu pcre2_unity na cel, więc każde wewnętrzne odwołanie rozwiązuje się wewnątrz pojedynczego obiektu, a linker Delphi widzi tylko eksportowane funkcje _16
Runtime C był drugim problemem. PCRE2 woła malloc, memcpy, memcmp i kolegów, a inne backendy C zlinkowane do tego samego pliku wykonywalnego mogą już nieść własne kopie tych nazw. HotXLS preprocessuje PCRE2 tak, by te wołania szły do prywatnych nazw, jak hotxls_regex_memcpy, i implementuje je w Pascalu. Clang potrafi po preprocessingu zsyntetyzować zwykłe wołania memset i memchr, więc te dwa też istnieją, w tej samej jednostce. Dekoracja symboli musi pasować do każdego formatu obiektu: obiekt OMF Win32 oczekuje nazw C z podkreśleniem wiodącym, podczas gdy deklaracje Pascala dla obiektu ELF Win64, obiektu COFF Win64 i buildów FPC wiążą nazwy gołe
Sondy stosu były subtelne. Funkcja C z ramą większą niż strona woła najpierw procedurę sondy, która dotyka każdą stronę 4 KB po kolei, żeby Windows mógł commitować stos przez swoją stronę strażniczą. Stub, który po prostu zwraca, linkuje się dobrze i działa w każdym teście jednostkowym z małym wzorcem, a potem pada za pierwszym razem, gdy głęboka rama przeskoczy stronę strażniczą. HotXLS implementuje prawdziwe sondy w asemblerze dla każdego formatu obiektu Windows: __chkstk_noalloc dla obiektu Win32, __chkstk dla obiektu w formacie ELF Win64 i ___chkstk_ms dla obiektu COFF Win64. Na Linuksie x64 i macOS ARM64 FPC linkuje systemową bibliotekę C, a każdy inny cel FPC raportuje backend jako niedostępny, zamiast po cichu cofać się do innego dialektu regex
Gdzie są granice?
Funkcje regex działają na buildach Delphi i C++Builder dla Windows oraz na FPC Linux x64 i macOS ARM64. Na każdym innym celu formuła raportuje niewspieraną funkcję przez ewaluację kontekstową, zamiast produkować inne odpowiedzi innym silnikiem. To właściwa porażka: skoroszyt liczący się inaczej na innej platformie to błąd, którego nikt nie znajdzie do audytu
Równoważność z Excelem też ma deklarowany limit. Sygnatury i udokumentowane przykłady trzymają się stron REGEXTEST, REGEXEXTRACT i REGEXREPLACE Microsoft, ale build Excela użyty do lokalnej walidacji nie wystawia jeszcze tych funkcji Microsoft 365. Przypadki brzegowe trzymają się więc jawnych umów PCRE2 i tablic opisanych wyżej, zamiast twierdzenia o zgodności z każdym buildem Excela bit w bit. Jeśli twoje skoroszyty zależą od zakamarka, jak konkretne zachowanie backreference'a albo klasa właściwości Unicode, przetestuj ten zakamarek wprost
FAQ
Czy HotXLS potrzebuje Excela albo biblioteki regex DLL, by liczyć REGEXTEST?
Nie. HotXLS linkuje statyczny obiekt PCRE2 UTF-16 do twojego pliku wykonywalnego i liczy REGEXTEST, REGEXEXTRACT i REGEXREPLACE we własnym silniku formuł Delphi, bez Excela, COM i zewnętrznej DLL
Czy złośliwy wzorzec może zawiesić mój serwer?
Nie dalej niż skonfigurowany budżet. Każdy krok dopasowania przechodzi przez callout obciążający kontekst formuły, więc wzorzec ReDoS kończy jako xlfeResourceLimit albo status anulowania i bez częściowego wyniku
Dlaczego tryb 1 REGEXEXTRACT pada na argumencie zakresu?
Każda komórka mogłaby zwrócić inną liczbę dopasowań, a te postrzępione wektory nie utworzą prostokątnego wyniku. HotXLS raportuje wywołanie jako niewspierane, zamiast dociągać albo ucinać, więc ekstrahuj po jednej komórce
HotXLS dostarcza funkcje regex obok LAMBDA, GROUPBY, PIVOTBY i reszty swojego silnika obliczeń dla Delphi, C++Builder i Lazarus. Szczegóły, edycje i pobieranie wersji próbnej są na stronie komponentu arkuszowego HotXLS Delphi