HotXLS는 .xls, .xlsx, .xlsm, .ods, CSV와 TSV 소스를 하나의 풀 행 커서 TXLSRowCursor로 읽고, 그 FindFirst와 FindNext는 한 번에 한 논리 행씩 전진하면서 그 행만 메모리에 둡니다. 여섯 값 상태 머신은 before-first를 EOF, 취소, 실패와 구분하며, 낡은 콜백 리더는 이제 같은 커서 위의 어댑터입니다
이 시나리오는 가져오기 기능을 한 번이라도 출시해 본 사람에게 익숙합니다. 200 MB .xlsx가 도착하고, OnCell 핸들러를 연결하고, "읽어라" 다음의 첫 요구는 "처음 백 건의 역분개를 지나면 멈춰라"입니다. 이제 코드의 모양이 여러분과 싸웁니다. 루프는 라이브러리 안에 살고, 핸들러는 플래그를 세워야 하고, 파서가 알아차릴 때까지 이후의 모든 콜백은 계속 발화하며, 누적된 상태 — 지금까지 몇 건인지, 어느 열이 맞았는지, 다음엔 뭘 할지 — 는 콜백에게 앉을 자리를 주려고만 존재하는 클래스의 필드 위에 살아야 합니다. 그중 파싱 문제는 하나도 없습니다. 제어 흐름 문제이고, 풀 커서가 제거하는 바로 그것입니다
200 MB에서 푸시 콜백이 실제로 드는 비용
푸시는 제어를 뒤집고, 그 뒤집힘이 바로 필터링이나 조인하는 호출자가 감당할 수 없는 것입니다. 콜백 API에서는 라이브러리가 루프를 소유하므로 호출자는 Break를 쓸 수 없고, 두 소스를 끼워 넣을 수 없고, 구동되기를 기대하는 루틴에 리더를 넘길 수 없으며, 버퍼링 없이 "결정하기 전에 다음 행을 훔쳐보라"를 표현할 수 없습니다. 비용은 처리량이 아닙니다 — 잘 쓴 SAX 콜백 경로는 스트리밍이 충분합니다 — 문제는 사소하지 않은 모든 소비자가 쓰게 허락되지 않은 루프를 흉내 내려고 자기만의 작은 상태 머신을 키운다는 것입니다. 여기에 역사적으로 각자의 스캐닝 진입점을 가진 네 파일 포맷을 곱하면, 필터링과 수식과 오류 의미론이 포맷 사이에서 벌어지기 시작합니다. 바로 HotXLS가 닫으려 한 그 드리프트입니다
풀 커서는 호출 코드를 어떻게 바꾸는가
루프를 여러분에게 돌려주고, 함께 평범한 파스칼 제어 흐름도 돌려줍니다. TXLSRowCursor.Open은 파일 이름이나 TStream을 받아 포맷을 감지하고, 공유 문자열과 날짜 스타일 메타데이터를 한 번 적재하고, 시트 1을 선택합니다. SelectSheet(1 기반)나 SelectSheetByName은 다른 워크시트를 재지정하고 커서를 before-first로 되돌립니다. 이어 FindFirst와 FindNext는 다음 채워진 행에 위치합니다 — 디코딩 가능한 셀이 없는 행은 건너뛰므로 RowIndex는 뛸 수 있습니다 — 그리고 현재 행은 CellCount, Cells[], ValueByCol[]로 노출되고, 열 축에서는 모두 1 기반입니다. 루프를 빠져나가는 것은 Break입니다
var
Cursor: TXLSRowCursor;
Hits: Integer;
begin
Cursor := TXLSRowCursor.Create;
try
Cursor.FirstRow := 2; // 헤더 밴드 건너뛰기
Cursor.IncludeColumn(1); // 이 두 열만 디코딩
Cursor.IncludeColumn(7);
if not Cursor.Open('postings-200mb.xlsx') then
Exit;
if not Cursor.SelectSheetByName('Ledger') then
Exit;
Hits := 0;
if Cursor.FindFirst then
repeat
if VarToStr(Cursor.ValueByCol[7]) = 'REVERSED' then
begin
Inc(Hits);
if Hits = 100 then
Break; // 평범한 Break; 중단 플래그도 보초값도 없다
end;
until not Cursor.FindNext;
finally
Cursor.Free; // 소멸자가 패스를 끝낸다
end;
end;
프로젝션과 범위는 패스 전에 정해지지, 나중에 걸러지지 않습니다. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates, DetectTextTypes는 모두 백엔드 안에서 존중되므로, 선택되지 않은 열은 애초에 자기 값, 수식 문자열, 리치 텍스트 페이로드를 할당하지 않습니다 — 회귀 스위트는 투영되지 않은 열의 16 KiB 수식과 캐시 문자열이 결코 실체화되지 않음으로 이를 증명합니다. 그 옵션들은 패스가 활성인 동안 일부러 얼리고, EOF에서, SelectSheet에서, 또는 Close 이후에 다시 쓰기 가능해집니다. 그래서 한 스캔이 두 디코딩 계약을 섞을 일은 없습니다. 행이 아니라 시트 목록만 필요하다면 메타데이터 전용 및 선택적 시트 적재가 더 싼 진입점입니다
포맷마다 하나의 백엔드, 각자 하나의 스캐닝 루프
모든 포맷은 HotXLS 안에 정확히 하나의 포워드 스캐너를 두고, 풀 커서와 콜백 리더 둘 다 같은 스캐너를 구동합니다. TXLSXForwardRowBackend는 ECMA-376 Part 1 §18.3 시트 파트를 위한 유일한 워크시트 SAX 상태 머신으로, XML 리더와 공유 수식 테이블과 리치 텍스트 파서를 붙들고, 호출마다 정확히 하나의 물리적 <row> 경계로 전진합니다. TXLSBiffForwardParser는 [MS-XLS] 레코드 스트림의 전역 상태, 시트 선택, 행 전진을 소유합니다. 일시정지 가능하게 만든 것이 설계 전체에서 가장 날카로운 제약을 낳았는데, 캐시된 문자열 수식은 Formula 레코드 바로 뒤에 String 레코드가 오는 것이므로, 행별 정지점이 절대 그 둘 사이에 착지하면 안 됩니다. TXLSForwardTextBackend는 BOM을 인식하는 리더와 활성 구분자와 하나의 논리 레코드를 유지합니다 — CSV는 첫 레코드에서 따옴표 안 문자를 무시하면서 쉼표, 세미콜론, 탭, 파이프를 탐지하고, 여러 줄 따옴표 필드는 #10으로 합쳐져서 행 번호가 물리적 줄바꿈이 아니라 논리 레코드를 따르게 됩니다. TXLSForwardOdsBackend는 OpenDocument §9 표를 위한 단일 물리 행 템플릿을 유지하고, table:number-rows-repeated를 확장이 아니라 남은 개수로 다루며, 값 발산 없이 덮인 셀을 지나 전진합니다. 스트리밍 다이렉트 리더는 같은 공유 문자열과 날짜 스타일 로더를 공유합니다
Eof 플래그 하나가 아니라 왜 여섯 상태인가
하나의 불리언은 네 가지 다른 상황을 구분 불가로 만들고, 호출자는 그 모두에 대해 틀리게 추측하기 때문입니다. TXLSRowCursorState는 그것들을 명시적으로 이름 짓습니다
xrcsClosed— 열려 있는 소스가 없습니다xrcsBeforeFirst— 열렸거나 재지정되었으며 아직 행을 읽지 않았습니다xrcsActive— 유효한 행 위에 서 있습니다xrcsEof— 시트가 끝까지 소비되었습니다xrcsCancelled— 호출자가 패스를 일부러 멈췄습니다xrcsFaulted— 패스가 실패했고 원래 예외가 발생했습니다
마지막 구분이 실전에서 중요한 하나입니다. 빠진 워크시트 파트나 실패한 패스 시작은 EReadError를 유지하며 커서를 xrcsFaulted로 옮깁니다. 호출자가 "이 시트는 비어 있었다"로 읽을 평범한 False로 격하되는 일은 결코 없습니다. Cancel은 일부러 Close보다 좁습니다. 현재 워크시트 백엔드와 그 inflate 하위 스트림을 닫고 현재 행을 무효화하지만, ZIP 아카이브나 소스 스트림을 놓아주지 않고, 두 번 부르는 것은 no-op입니다. 취소 뒤에는 SelectSheet를 명시적으로 불러 재개합니다 — 커서가 여러분 대신 조용히 패스를 재시작하지 않습니다. 스트림 소유권도 같은 방어 규칙을 따릅니다. xsoBorrowed가 기본이고 닫을 때 스트림 위치를 복원하며, xsoOwned는 Open이 이미 성공한 뒤에야 소유권을 넘겨서, 실패한 열기가 호출자가 여전히 쥔 스트림을 놓아주는 일이 없습니다
var
Cursor: TXLSRowCursor;
Src: TFileStream;
begin
Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
try
Cursor := TXLSRowCursor.Create;
try
// xsoBorrowed: 커서는 Src를 절대 놓아주지 않고, Close는
// Open이 불렸을 때 스트림이 가진 위치를 복원한다
if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
Exit;
if Cursor.FindFirst then
repeat
if UserPressedStop then
begin
Cursor.Cancel; // 워크시트 백엔드와 그 inflate
Break; // 하위 스트림만 닫는다; 멱등하다
end;
until not Cursor.FindNext;
case Cursor.State of
xrcsEof: Log('sheet consumed to the end');
xrcsCancelled: Log('stopped by the operator');
xrcsFaulted: Log('pass failed; the EReadError was already raised');
end;
finally
Cursor.Free;
end;
finally
Src.Free; // 여전히 우리 것, 여전히 유효, 위치 복원됨
end;
end;
복사하지 않고 현재 행 빌려 쓰기
IXLSRowCursorView는 셀 배열을 복제하지 않고 행을 다른 루틴에 넘깁니다. 뷰는 커서 포인터와 UInt64 세대 카운터를 붙든 공유 가드를 저장합니다. 전진, 시트 선택, 취소, 닫기, 커서 파괴가 모두 그 세대를 증가시키고, 파괴는 추가로 가드 소유자를 지웁니다. 그래서 낡은 뷰는 해제된 메모리를 읽을 수 없습니다. Valid는 언제든 부를 수 있는 예외 없는 탐침이고, 다른 모든 멤버는 먼저 검증한 뒤 EXLSRowCursorViewInvalidated를 던집니다. 이 계약이 무엇인지 정직합시다 — 수명 fail-fast이지 스레드 안전 보장이 아니고, 첫 스레드가 커서를 전진시키는 동안 둘째 스레드에서 행을 읽는 것을 허가하지도 않습니다
var
View: IXLSRowCursorView;
Cell: TXLSRowCursorCell;
I: Integer;
begin
if Cursor.FindFirst then
repeat
View := Cursor.CurrentRowView; // 빌린다; 셀 배열은 복사되지 않는다
for I := 0 to View.CellCount - 1 do
begin
Cell := View.Cells[I];
if Cell.HasFormula and not Cell.FormulaTextAvailable then
UseCachedResult(Cell.Value) // BIFF 포워드 읽기는 토큰이 아니라
else if Cell.Kind = xdkEmpty then // 캐시된 결과를 유지한다
UseStyleOnly(Cell.StyleIndex) // Blank / MulBlank는 진짜 셀이다
else
UseValue(Cell.Col, Cell.Value);
end;
until not Cursor.FindNext;
// 인터페이스는 루프보다 오래 살지만, 그 뒤의 행은 그렇지 않다
if not View.Valid then // Valid는 절대 던지지 않는다; 이제 Cells[]는 던진다
View := nil; // EXLSRowCursorViewInvalidated
end;
PeakRowBufferedBytes와 그것이 증명하도록 허락된 것
PeakRowBufferedBytes는 메모리가 행 개수가 아니라 행 너비를 따른다는 것을 보이기 위해 존재합니다. 현재 출력 행의 셀 레코드, Variant, 수식 문자열, 리치 텍스트 페이로드를 누적하고, 포맷별 작업 세트 — CSV 논리 레코드, ODS 물리 행 템플릿, BIFF 레코드 최고점, 또는 지금 디코딩 중인 XLSX 날 셀 — 를 접어 넣습니다. 실제로 시작된 워크시트 패스 수를 세는 SheetPassesStarted와 함께 읽으십시오. 두 가지 단서가 이것을 정직하게 유지합니다. 그 수치는 정확한 힙 회계가 아니라 추정이고, 가장 최근 Open 이후 단조적이므로 실시간 게이지가 아니라 디버깅과 회귀 도구입니다. 아주 큰 통합 문서에서 시간과 바이트가 어디로 가는지 넓은 그림은 델파이 대형 통합 문서 성능을 보십시오
푸시 리더는 어댑터가 되었고, 커서가 하지 않을 일
TXLSForwardReader는 더 이상 별도의 XLSX, BIFF, 텍스트 스캐닝 진입점을 싣지 않습니다. 커서를 설정하고, 걷고, 현재 행을 OnSheet와 OnCell 이벤트로 번역합니다. 그래서 두 파사드가 필터링, 수식 상태, 오류 처리에서 더 이상 벌어질 수 없습니다. 업그레이드 전에 알아둘 두 가지 결과. 콜백 SheetIndex는 이제 TXLSForwardReader에서 통일되게 1 기반이고(TXLSDirectReader는 기존의 0 기반 이벤트 계약을 유지합니다), OnSheet는 SelectSheet보다 먼저 발화하므로 SkipSheet를 설정하면 워크시트 파트는 아예 열리거나 풀리지 않습니다. 경계도 똑같이 명시적입니다. 패스가 활성인 동안 통합 문서를 수정해서는 안 되고, 취소는 명시적 재시작을 요구하며, BIFF 포워드 경로는 수식 토큰을 절대 디컴파일하지 않아서, 클래식 수식 셀은 HasFormula 참에 FormulaTextAvailable 거짓을 보고하고 빈 수식 문자열을 지어내는 대신 캐시된 결과를 넘깁니다. 행 커서와 그 어댑터는 Delphi Win32와 Win64, 그리고 C++Builder 37.0 Win64 정적 패키지에서 1,298개 검사를 통과했습니다
지금 가진 로더와 풀 커서를 저울질한다면 묻는 질문은 어느 쪽이 더 빨리 파싱하느냐가 아니라, 어느 쪽이 실제로 필요한 종료 조건을 쓰게 해 주느냐입니다. 전체 컴포넌트 세부, 지원 IDE 버전, 라이선싱은 HotXLS Delphi spreadsheet component page에 있습니다