기술 문서

Delphi의 REGEXTEST·REGEXEXTRACT: 정적 PCRE2 기반

HotXLS는 자체 Delphi 수식 엔진 안에서 Microsoft 365 정규식 함수 REGEXTEST, REGEXEXTRACT, REGEXREPLACE를 평가합니다. Excel 설치도 시스템 regex DLL도 없이요. HotXLS 계산기는 고정된 16비트 PCRE2 10.49 빌드를 정적으로 링크하고, UTF-16 텍스트를 직접 매칭하고, 배열에 걸쳐 브로드캐스트하며, 모든 백트래킹 단계를 수식의 리소스 예산에 청구합니다

마지막 부분이 이 글이 존재하는 이유입니다. 고객에게서 도착한 워크북은 신뢰할 수 없는 입력이고, 수식 안의 정규식은 고객이 쓴 코드입니다. 패턴과 텍스트만 있으면 재앙적 백트래킹, 그러니까 a 문자의 긴 런 뒤에 b 하나를 붙인 형태에 대한 전형적인 ReDoS 모양 (a+)+$로 CPU 코어를 몇 분씩 붙잡아 둘 수 있습니다. 업로드된 워크북을 재계산하는 서버는 그걸 감당할 수 없으므로, HotXLS 2.384.112의 흥미로운 작업은 PCRE2를 호출하는 것이 아니었습니다. PCRE2를 정지 가능하고, 책임 소재가 명확하고, 모든 대상에서 동일하게 만드는 것이었습니다

HotXLS에서 REGEXTEST, REGEXEXTRACT, REGEXREPLACE는 무엇을 반환하나요?

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는 원본 텍스트를 그대로 반환합니다. 숫자 선택자는 0을 향해 잘리는데, 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 수식 토큰으로 쓸 수는 없으므로 regex 수식은 XLSX 출력에 담아 두세요

regex 수식에서 배열 브로드캐스팅은 어떻게 동작하나요?

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의 콜아웃 중단 코드를 반환하고 매치는 PCRE2 안에서 깨끗하게 풀립니다. 수식은 매달린 스레드가 아니라 xlfeResourceLimit나 취소 상태로 표면화됩니다

메모리도 같은 방식으로 계량됩니다. 모든 PCRE2 컨텍스트는 할당자가 Pascal 함수인 인스턴스별 제너럴 컨텍스트로 만들어지므로, 컴파일된 패턴, 매치 데이터, PCRE2의 내부 백트래킹 프레임이 전부 HotXLS를 통해 할당됩니다. 각 블록은 작은 헤더를 달고 16바이트로 정렬되며, 크기는 예산 검사가 수식의 배열 메모리 한도와 비교하는 누적 합에 더해집니다. 출력 버퍼, 추출 벡터, 치환 문자열도 같은 합에 더해집니다. 그래서 수백만 개의 자그마한 문자열에 매칭되는 패턴이 영원히 백트래킹하는 패턴만큼 확실하게 천장에 부딪힙니다

예산 콜백은 겉보기보다 중요한 가드 아래에서 돕니다. PCRE2가 부른 콜백 안에서 발생한 Delphi 예외는 절대 위로 전파되면 안 됩니다. 콜백과 여러분의 코드 사이에는 Delphi 예외 테이블을 전혀 모르는 채 컴파일된 C 스택 프레임들이 앉아 있기 때문입니다. 그 사이로 풀어 내는 것은 최선의 경우 정의되지 않은 동작이고 최악의 경우 깨진 스택입니다. HotXLS 브리지는 콜백 안의 모든 예외를 잡아 인스턴스를 중단 상태로 고정하고 PCRE2에 평범한 에러 코드를 돌려줍니다. 여러분의 코드가 보는 것은 실패한 수식이지, 외부 프레임을 넘은 예외가 아닙니다

비용은 실재하고 말해 둘 가치가 있습니다. 오토 콜아웃은 맨 PCRE2 인터프리터보다 매칭을 느리게 만들고, HotXLS는 평가되는 요소마다 패턴을 한 번씩 컴파일하므로, 50,000셀 브로드캐스트는 50,000번의 컴파일을 치릅니다. 신뢰하는 데이터의 벌크 텍스트 작업이라면 워크북 밖의 전용 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의 글로벌 매치 규칙을 따릅니다: 같은 위치에서 비지 앵커드 매치를 재시도한 뒤, 유니코드 스칼라 값 하나를 전진합니다. 서로게이트 페어를 쪼개는 대신 두 코드 유닛을 건너뛰는 것이죠

JIT는 의도적으로 꺼 두었습니다. JIT 컴파일러는 실행 가능한 메모리와 자체 JIT 스택을 필요로 하는데, 이는 플랫폼 고유 코드와 수식별 할당자 밖의 할당을 끌어들입니다. 인터프리터는 모든 바이트를 HotXLS 예산 아래에, 모든 매치를 콜아웃 아래에 유지하고, 다섯 대상 모두에서 똑같이 동작합니다. 이 함수들 뒤에 있는 내부 클래스인 TXLSRegex 인스턴스마다 자체 컨텍스트를 소유하므로, 병렬 계산 워커는 가변 PCRE2 상태를 공유하지 않습니다

빌드가 특이해진 곳은 링킹 이야기입니다. Delphi는 C 오브젝트 파일을 {$L}로 링크하고, 그 링커는 Pascal 유닛과 C 오브젝트 사이의 심볼을 충분히 풀지만, 서로 다른 C 오브젝트 파일 사이의 임의의 순환 참조는 풀지 못합니다. PCRE2는 사방에서 서로를 부르는 두다스의 번역 유닛입니다. 해법은 유니티 빌드입니다: 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 대상은 조용히 다른 regex 방언으로 폴백하는 대신 백엔드를 사용 불가로 보고합니다

경계는 어디인가요?

regex 함수는 Delphi와 C++Builder Windows 빌드, 그리고 FPC Linux x64와 macOS ARM64에서 동작합니다. 다른 대상에서는 수식이 다른 엔진으로 다른 답을 만드는 대신 문맥 평가를 통해 미지원 함수를 보고합니다. 그것이 올바른 실패입니다. 플랫폼마다 다르게 계산되는 워크북은 감사가 올 때까지 아무도 못 찾는 버그입니다

Excel 등가성에도 명시된 한계가 있습니다. 시그니처와 문서화된 예제는 Microsoft의 REGEXTEST, REGEXEXTRACT, REGEXREPLACE 페이지를 따르지만, 로컬 검증에 쓴 Excel 빌드에는 아직 이 Microsoft 365 함수들이 없습니다. 따라서 엣지 케이스는 모든 Excel 빌드와 비트 단위로 일치한다는 주장이 아니라 위에서 기술한 명시적인 PCRE2 및 배열 계약을 따릅니다. 워크북이 특정 역참조 동작이나 Unicode 프로퍼티 클래스 같은 잘 알려지지 않은 구석에 의존한다면 그 구석을 직접 테스트하세요

FAQ

HotXLS가 REGEXTEST를 계산하려면 Excel이나 regex DLL이 필요한가요?

아니요. HotXLS는 정적 UTF-16 PCRE2 오브젝트를 실행 파일에 링크하고 자체 Delphi 수식 엔진에서 REGEXTEST, REGEXEXTRACT, REGEXREPLACE를 평가합니다. Excel도, COM도, 외부 DLL도 없이요

악의적인 패턴이 서버를 멈춰 세울 수 있나요?

설정된 예산을 넘어서는 안 됩니다. 모든 매칭 단계는 수식 문맥에 청구하는 콜아웃을 거치므로, ReDoS 패턴은 xlfeResourceLimit나 취소 상태로 끝나고 부분 결과도 없습니다

범위 인수에서 REGEXEXTRACT 모드 1이 실패하는 이유는 무엇인가요?

각 셀이 서로 다른 개수의 매치를 반환할 수 있고, 그 삐뚤빼뚤한 벡터들은 사각형 결과를 이룰 수 없습니다. HotXLS는 채우거나 자르는 대신 호출을 미지원으로 보고하므로, 한 번에 한 셀씩 추출하세요

HotXLS는 regex 함수를 LAMBDA, GROUPBY, PIVOTBY와 함께 Delphi, C++Builder, Lazarus용 계산 엔진의 나머지와 함께 제공합니다. 세부 사항, 에디션, 체험판 다운로드는 HotXLS Delphi 스프레드시트 컴포넌트 페이지에 있습니다