Article technique

REGEXTEST et REGEXEXTRACT en Delphi via PCRE2 statique

HotXLS évalue les fonctions expressions régulières de Microsoft 365 REGEXTEST, REGEXEXTRACT et REGEXREPLACE au sein de son propre moteur de formules Delphi, sans installation d'Excel et sans DLL regex système. La calculatrice HotXLS lie statiquement une build PCRE2 10.49 16 bits épinglée, apparie directement du texte UTF-16, diffuse sur les tableaux et facture chaque étape de backtracking au budget de ressources de la formule

Ce dernier point est la raison d'être de cet article. Un classeur qui arrive d'un client est une entrée non fiable, et une expression régulière dans une formule est du code qu'a écrit le client. Motif plus texte suffisent pour épingler un cœur de CPU pendant des minutes avec du backtracking catastrophique, la forme ReDoS classique (a+)+$ contre une longue suite de caractères a terminée par un b. Un serveur qui recalcule des classeurs envoyés ne peut pas se le permettre, si bien que le travail intéressant de HotXLS 2.384.112 n'a pas été d'appeler PCRE2. Il a été de rendre PCRE2 arrêtable, redevable et identique sur chaque cible

Que renvoient REGEXTEST, REGEXEXTRACT et REGEXREPLACE dans HotXLS ?

Le moteur de formules HotXLS suit les signatures publiées par Microsoft, et la sémantique des arguments est celle que vous connaissez déjà d'Excel. L'appariement est sensible à la casse par défaut. Le dernier argument case_sensitivity prend 0 pour sensible à la casse et 1 pour insensible, et toute autre valeur renvoie #VALUE!. Les options PCRE2 en ligne comme (?i) ou (*UCP) marchent aussi quand vous préférez garder le comportement dans le motif

REGEXTEST(text, pattern, [case_sensitivity]) renvoie un booléen, et un appariement raté est simplement FALSE, jamais une erreur. REGEXEXTRACT(text, pattern, [return_mode], [case_sensitivity]) a trois modes : 0 renvoie la première correspondance complète, 1 renvoie toutes les correspondances complètes en tableau vertical, et 2 renvoie les groupes de capture de la première correspondance en tableau horizontal. Pas de correspondance renvoie #N/A, le mode 2 sur un motif sans groupes de capture renvoie #VALUE!, et un groupe optionnel qui n'a pas participé donne une chaîne vide plutôt qu'une erreur

REGEXREPLACE(text, pattern, replacement, [occurrence], [case_sensitivity]) utilise la syntaxe de remplacement PCRE2, donc $1 et ${name} référencent les captures. Une occurrence de 0 remplace chaque correspondance, une valeur positive prend la n-ième depuis le début et une valeur négative compte depuis la fin. Quand l'occurrence choisie n'existe pas, HotXLS renvoie le texte original inchangé. Les sélecteurs numériques sont tronqués vers zéro, comme Excel traite les arguments entiers fractionnaires

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 quand la cellule porte un numéro de bon de commande
    Sheet.Cells[2, 2].Formula := 'REGEXTEST(A2, "PO-\d{4}-\d{5}")';

    // Première correspondance complète : "PO-2026-00417"
    Sheet.Cells[2, 3].Formula := 'REGEXEXTRACT(A2, "PO-\d{4}-\d{5}")';

    // Permutez année et séquence avec captures numérotées : "PO-00417-2026 ..."
    Sheet.Cells[2, 4].Formula :=
      'REGEXREPLACE(A2, "PO-(\d{4})-(\d{5})", "PO-$2-$1")';

    // Test insensible à la casse avec le dernier argument
    Sheet.Cells[2, 5].Formula := 'REGEXTEST(A2, "graz", 1)';

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

Le texte de formule en Delphi est une chaîne littérale simple, donc \d n'a pas besoin d'échappement côté Pascal. Seuls les guillemets doubles Excel dans la formule comptent, et ils suivent les règles Excel normales. À l'enregistrement, le rédacteur XLSX stocke ces noms avec le préfixe de fonction future _xlfn. qu'Excel attend, et un classeur rouvert les recalcule à nouveau. Le moteur classique BIFF8 peut évaluer les fonctions dans un appel contextuel, mais il ne peut pas les écrire en tokens de formule .xls natifs ; gardez donc les formules regex dans une sortie XLSX

Comment marche la diffusion de tableaux avec les formules regex ?

REGEXTEST, REGEXREPLACE et le mode 0 de REGEXEXTRACT diffusent à travers des opérandes compatibles, ce qui les rend utiles sur de vraies colonnes. Chaque argument peut être un scalaire, une ligne, une colonne ou un rectangle, et HotXLS prend le plus grand compte de lignes et de colonnes comme forme du résultat. Un opérande dont la dimension n'est ni 1 ni ce maximum fait renvoyer #VALUE! à tout l'appel, l'inhabituelle incohérence de tableau Excel. Ainsi REGEXTEST(A2:A500, "^[A-Z]{2}\d{9}$") donne 499 booléens, et une colonne de motifs contre une seule cellule de texte marche de la même façon dans l'autre sens

Les modes 1 et 2 de REGEXEXTRACT refusent de diffuser. Chaque élément produirait un vecteur de longueur différente, et un ensemble irrégulier de vecteurs ne peut pas occuper un résultat de formule rectangulaire. Plutôt que de tronquer ou de bourrer en silence, HotXLS rapporte l'appel comme non pris en charge. Si vous voulez toutes les correspondances de nombreuses cellules, itérez les cellules et extrayez une à une, ou reformulez le problème avec les closures LET et LAMBDA du moteur de formules HotXLS pour que chaque appel de closure voie une seule valeur de texte

Le placement des erreurs dans un résultat diffusé est délibéré. Une erreur Excel pour un élément, disons un #N/A venu d'une extraction qui n'a rien trouvé ligne 37, atterrit dans cette seule cellule du résultat pendant que les autres gardent leurs valeurs. Une limite de ressources ou une annulation est différente : elle arrête tout l'appel et ne publie jamais un tableau partiellement rempli, parce qu'un demi-résultat qui a l'air complet est pire qu'un échec explicite

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;   // plafonnez le travail que cette formule peut faire

    // Mode 2 : groupes de capture de la première correspondance en tableau 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 est le bon point d'entrée quand le motif vient d'un endroit que vous ne contrôlez pas. Il évalue la formule comme si elle vivait à la ligne et colonne données, renvoie le tableau complet au lieu d'une seule valeur de cellule en cache, et prend TXLSFormulaEvaluationOptions avec des contrôles explicites de pas, de mémoire de tableau et d'annulation. Le guide du moteur de formules HotXLS et des fonctions personnalisées couvre le reste de cette API d'évaluation

Comment HotXLS arrête-t-il un motif de backtracking catastrophique ?

HotXLS arrête les motifs emballés en faisant rapporter PCRE2 au contexte de formule à chaque étape. Le backend compile chaque motif avec PCRE2_AUTO_CALLOUT, ce qui fait appeler un callback Pascal par élément de motif pendant l'appariement. Ce callback facture une unité de travail à l'état d'évaluation actif, vérifie le drapeau d'annulation et vérifie la mémoire résidente. Quand l'un de ces points dit stop, le callback renvoie le code d'abort de callout de PCRE2 et l'appariement se déroule proprement au sein de PCRE2. La formule remonte en xlfeResourceLimit ou en statut d'annulation, pas en thread bloqué

La mémoire est comptée de la même façon. Chaque contexte PCRE2 est créé avec un contexte général par instance dont l'allocateur est une fonction Pascal, si bien que le motif compilé, les données d'appariement et les cadres internes de backtracking de PCRE2 passent tous par HotXLS. Chaque bloc porte un petit en-tête et est aligné sur 16 octets, et sa taille s'ajoute à un total courant que la vérification de budget compare à la limite de mémoire de tableau de la formule. Les buffers de sortie, les vecteurs d'extraction et la chaîne de remplacement s'ajoutent au même total, ce qui veut dire qu'un motif qui apparie des millions de petites chaînes bute au plafond aussi sûrement qu'un motif qui backtrack pour toujours

Le callback de budget tourne aussi sous une garde qui compte plus qu'il n'y paraît. Une exception Delphi levée dans un callback appelé par PCRE2 ne doit jamais se propager vers le haut, parce qu'entre le callback et votre code siègent des cadres de pile C compilés sans aucune connaissance des tables d'exceptions Delphi. Se dérouler à travers eux est indéfini au mieux et une pile corrompue au pire. Le pont HotXLS attrape chaque exception dans le callback, verrouille l'instance en état abandonné et renvoie un code d'erreur ordinaire à PCRE2. Votre code voit une formule en échec, jamais une exception qui aurait traversé un cadre étranger

Le coût est réel et mérite d'être dit. Les auto callouts rendent l'appariement plus lent qu'un interpréteur PCRE2 nu, et HotXLS compile le motif une fois par élément évalué, si bien qu'une diffusion sur 50 000 cellules paie 50 000 compilations. Pour du travail de texte en masse sur des données fiables, une passe regex Delphi dédiée hors du classeur sera plus rapide. Dans un classeur que des inconnus vous envoient, borné et prévisible bat rapide

Pourquoi une build PCRE2 UTF-16 statique sans JIT ?

Les chaînes Excel sont UTF-16, et les WideString Delphi aussi, ce qui a fait de la bibliothèque PCRE2 16 bits le seul choix sensé. HotXLS remet le texte de formule à pcre2_match_16 avec PCRE2_UTF posé, et chaque décalage qui revient est un indice d'unité de code UTF-16. Aucun aller-retour UTF-8 n'a lieu, donc rien ne peut dériver sur des paires de substitution. Là où une correspondance vide force le scan à avancer, HotXLS suit la règle de correspondance globale de PCRE2 : retenter une correspondance ancrée non vide à la même position, puis avancer d'une valeur scalaire Unicode, ce qui franchit deux unités de code après une paire de substitution au lieu de la couper

Le JIT est désactivé exprès. Le compilateur JIT demande de la mémoire exécutable et sa propre pile JIT, ce qui tire du code et des allocations spécifiques à la plateforme hors de l'allocateur par formule. L'interpréteur garde chaque octet sous le budget HotXLS, garde chaque appariement sous le callout et se comporte pareil sur les cinq cibles. Chaque instance TXLSRegex, la classe interne derrière ces formules, possède ses propres contextes, si bien que les workers de calcul parallèles ne partagent aucun état PCRE2 mutable

L'histoire de l'édition de liens est là où la build est devenue inhabituelle. Delphi lie des fichiers objets C avec {$L}, et son éditeur de liens résout assez bien les symboles entre unités Pascal et objets C, mais il ne résout pas les références circulaires arbitraires entre fichiers objets C séparés. PCRE2, c'est deux douzaines d'unités de traduction qui s'appellent dans tous les sens. La parade est une unity build : tout PCRE2 est compilé comme une seule unité de traduction en un objet pcre2_unity par cible, si bien que chaque référence interne se résout au sein d'un objet unique et que l'éditeur de liens Delphi ne voit que les fonctions _16 exportées

Le runtime C était le second problème. PCRE2 appelle malloc, memcpy, memcmp et compagnie, et d'autres backends C liés dans le même exécutable apportent peut-être déjà leurs propres copies de ces noms. HotXLS prétraite PCRE2 pour que ces appels aillent vers des noms privés comme hotxls_regex_memcpy et les implémente en Pascal. Clang peut toujours synthétiser des appels memset et memchr simples après prétraitement, donc ces deux-là existent aussi, au sein de la même unité. La décoration des symboles doit correspondre à chaque format d'objets : l'objet OMF Win32 attend les noms C à souligné de tête, tandis que les déclarations Pascal pour l'objet Win64 ELF, l'objet Win64 COFF et les builds FPC lient les noms nus

Les sondes de pile étaient le point subtil. Une fonction C avec un cadre plus grand qu'une page appelle d'abord une routine de sonde, qui touche chaque page de 4 Ko dans l'ordre pour que Windows puisse engager la pile à travers sa page de garde. Un stub qui se contente de renvoyer se lie bien et marche dans chaque test unitaire avec un petit motif, puis plante la première fois qu'un cadre profond saute par-dessus la page de garde. HotXLS implémente de vraies sondes en assembleur pour chaque format d'objets Windows : __chkstk_noalloc pour l'objet Win32, __chkstk pour l'objet au format ELF Win64 et ___chkstk_ms pour l'objet COFF Win64. Sur Linux x64 et macOS ARM64, FPC lie la bibliothèque C système à la place, et toute autre cible FPC rapporte le backend comme indisponible au lieu de retomber en silence sur un autre dialecte regex

Où sont les frontières ?

Les fonctions regex tournent sur les builds Windows Delphi et C++Builder et sur FPC Linux x64 et macOS ARM64. Sur toute autre cible, la formule rapporte une fonction non prise en charge via l'évaluation contextuelle au lieu de produire d'autres réponses avec un autre moteur. C'est la bonne panne : un classeur qui calcule autrement sur une autre plateforme est un bug que personne ne trouve avant un audit

L'équivalence Excel a aussi une limite déclarée. Les signatures et exemples documentés suivent les pages Microsoft de REGEXTEST, REGEXEXTRACT et REGEXREPLACE, mais la build Excel utilisée pour la validation locale n'expose pas encore ces fonctions Microsoft 365. Les cas limites suivent donc les contrats PCRE2 et tableaux explicites décrits plus haut plutôt qu'une prétention à égaler chaque build Excel bit à bit. Si vos classeurs dépendent d'un coin obscur, comme un comportement de référence arrière particulier ou une classe de propriété Unicode, testez ce coin directement

FAQ

HotXLS a-t-il besoin d'Excel ou d'une DLL regex pour calculer REGEXTEST ?

Non. HotXLS lie un objet PCRE2 UTF-16 statique dans votre exécutable et évalue REGEXTEST, REGEXEXTRACT et REGEXREPLACE dans son propre moteur de formules Delphi, sans Excel, COM ni DLL externe

Un motif malveillant peut-il bloquer mon serveur ?

Pas au-delà du budget configuré. Chaque étape d'appariement passe par un callout qui facture le contexte de formule, si bien qu'un motif ReDoS finit en xlfeResourceLimit ou en statut d'annulation sans résultat partiel

Pourquoi le mode 1 de REGEXEXTRACT échoue-t-il sur un argument de plage ?

Chaque cellule pourrait renvoyer un nombre différent de correspondances, et ces vecteurs irréguliers ne peuvent pas former un résultat rectangulaire. HotXLS rapporte l'appel comme non pris en charge au lieu de bourrer ou tronquer ; extrayez donc une cellule à la fois

HotXLS livre les fonctions regex aux côtés de LAMBDA, GROUPBY, PIVOTBY et du reste de son moteur de calcul pour Delphi, C++Builder et Lazarus. Détails, éditions et téléchargement d'essai sont sur la page du composant tableur HotXLS Delphi