SpreadsheetML에서 통화 형식을 지닌 열은 정확히 하나의 요소면 됩니다. <col min="2" max="2" style="3"/>입니다. ECMA-376 Part 1은 셀 스타일을 세 곳에만 붙입니다. 열(§18.3.1.13), 행(§18.3.1.73), 셀(§18.3.1.4)입니다. 형식을 B2:D500000으로 좁히면 SpreadsheetML에는 그 모양을 위한 단일 요소가 없습니다. 열 스타일은 500000행을 넘어 흐르고, 행 스타일은 모든 열을 덮으므로, 작성기는 결국 스타일 전용 셀로 내보내게 됩니다. 1,499,997개입니다
인메모리 스프레드시트 모델은 한 단계 앞서서 같은 벽에 부딪힙니다. 서식이 각 셀 객체에 프로퍼티를 설정하는 것이라면 열 헤더 한 번의 클릭이 작은 정수 하나를 나르기 위해 백만 개의 객체를 할당하고, 숫자 형식 하나만 원했던 Delphi 프로세스가 디스크에서는 몇 킬로바이트인 통합 문서에서 주소 공간을 다 써버립니다. 진짜 설계 질문은 편집하는 동안 서식이 어디에 살고, 언제 셀별 데이터로 변하느냐입니다
HotXLS가 셀을 만들지 않고 범위에 서식 입히는 방법
HotXLS는 범위 서식을 인터벌로 유지합니다. TXLSXRange의 모든 스타일 세터 — SetNumberFormat, SetNumberFormatIndex, SetFontIndex, SetFillIndex, SetBorderIndex, SetAlignmentIndex — 는 워크시트의 StyleOverlays 목록에 레코드 하나를 추가합니다. 네 개의 좌표, 호출이 소유한 스타일 파트, 그 파트들의 풀 인덱스입니다. 사각형 안에 이미 존재하는 셀은 새 파트를 즉시 받아 들이고, 빈 좌표는 빈 채로 남아 아무것도 할당되지 않습니다
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Edge: TXLSXBorder;
BorderIdx, Row: Integer;
Stats: TXLSXCellStorageStatistics;
begin
Workbook := TXLSXWorkbook.Create;
try
Sheet := Workbook.Sheets.Add('Ledger');
Sheet.WriteStringRowValues(1, 2, ['Net', 'Tax', 'Gross']);
for Row := 2 to 1001 do
Sheet.WriteFloatRowValues(Row, 2, [Row * 1.5, Row * 0.3, Row * 1.8]);
// 전체 1,048,576행에 걸친 B..D열: 호출당 인터벌 하나
Sheet.RCRange[1, 2, XlsxMaxRow, 4].SetNumberFormat('#,##0.00 "EUR"');
// 테두리를 한 번 만들고 인덱스로 적용
Edge := TXLSXBorder.Create;
try
Edge.SetAll(xlsxBorderThin);
BorderIdx := Workbook.Borders.LookupOrAdd(Edge); // 1 기반
finally
Edge.Free;
end;
Sheet.RCRange[1, 2, XlsxMaxRow, 4].SetBorderIndex(BorderIdx);
Stats := Sheet.Cells.GetStorageStatistics;
// 3,003 예상: 헤더 셋 + 값 3,000개
WriteLn('Cells: ', Stats.CellCount);
if Workbook.SaveAs('ledger.xlsx') <> 1 then
raise Exception.Create('Save failed');
finally
Workbook.Free;
end;
end;
두 서식 호출은 3,145,728개 좌표를 커버하고 그 개수에 아무것도 더하지 않았습니다. 인터벌이 의도적으로 1행에서 시작합니다. 열 전체 인터벌은 하나의 <col> 요소로 저장되고, 어차피 1행의 텍스트 헤더는 숫자 형식의 영향을 받지 않습니다. RCRange, Range['A1:C5'], EntireColumn, EntireRow가 돌려주는 범위는 워크시트가 소유하므로, 예제는 그것들을 해제하지 않습니다
여러 인터벌이 서로 싸우지 않게 하는 것은 TXLSXFormatParts입니다. 멤버는 xfpFont, xfpFill, xfpBorder, xfpNumberFormat, xfpAlignment, xfpProtection, xfpCellControl이며, 각 TXLSXRange 세터는 정확히 하나를 요구합니다. 셀이 해석될 때 그것을 덮는 인터벌들은 추가된 순서대로 적용되고 각자 자기 파트만 기록하며, 셀 자신의 명시적 서식이 그 전부를 이깁니다. 숫자 형식을 설정하는 열 인터벌과 채우기를 설정하는 행 인터벌이 모두 셀 B5에 온전히 닿습니다
하나의 레코드가 여러 파트를 나르게 하려면 Sheet.StyleOverlays.Add(Row1, Col1, Row2, Col2, Parts, State)가 풀 인덱스가 채워진 TXLSXCellStyleState를 받습니다. Add는 좌표를 1,048,576 × 16,384 그리드로 클램프하고, 뒤집힌 모서리를 교환하며, Parts가 비어 있으면 아무것도 기록하지 않은 채 nil을 돌려줍니다. 구조 편집도 레코드를 걸맞게 유지합니다. 서식 블록 안의 InsertRows는 인터벌을 삽입된 수만큼 넓히고, 위에 삽입하면 레코드 전체를 아래로 밀어냅니다
SetBorderIndex는 셀을 안 만드는데 SetBorders는 왜 만드나?
TXLSXRange.SetBorders(Kind, Style, Color)는 답이 위치에 달려 있으므로 각 셀을 개별적으로 봐야 합니다. xlsxEdgeOutline에서는 맨 윗행이 위 모서리를, 맨 왼쪽 열이 왼쪽 모서리를 얻고 내부는 아무것도 얻지 않습니다. xlsxEdgeInside에서는 대략 그 반대입니다. 이 호출은 새 모서리를 셀이 이미 가진 테두리에 병합하므로, Outline 다음 Inside가 완전한 그리드를 만듭니다. 그 셀별 합성은 Cells[R, C]를 거치는데, 이 접근자는 없을 때 셀을 만듭니다. 10 × 40 리포트 블록에서는 이것이 정확히 옳습니다. EntireColumn(3)에 xlsxEdgeAll이면 1,048,576개의 새 셀 객체입니다
그래서 분담은 단순합니다. 화면 한 화면보다 긴 데이터라면 첫 예제가 하듯 테두리를 한 번 만들어 인덱스로 적용하십시오. TXLSXBorder.SetAll, 그다음 1 기반 인덱스를 얻는 Workbook.Borders.LookupOrAdd, 그다음 SetBorderIndex입니다. SetBorders는 외곽선과 내부선이 다른 블록, 위의 10 × 40 리포트 블록 같은 곳에 남겨 두십시오
작업을 되돌리지 않고 서식 다시 읽기
같은 함정이 읽기만 하는 코드도 잡습니다. Cells[R, C]는 없으면 만드는 접근자입니다. B500000의 숫자 형식을 묻면 올바른 답과 함께 B500000에 새로운, 서식 입은, 빈 셀을 얻는데, 이 셀은 이제 저장되고 기록됩니다. 그 루프를 열 하나에 돌려 서식을 감사하면 인터벌이 피하려 했던 백만 개의 객체를, 얼핏 무해해 보이는 읽기를 한 번에 하나씩 하며 다시 만들게 됩니다
TXLSXWorksheet.GetCellVisibleFormat은 셀을 추가하지 않고 좌표의 유효 스타일을 해석합니다. 워크시트 기본값, 행과 열 인터벌, 사각형 인터벌, 테이블 스타일, 셀의 명시적 서식을 병합합니다. out ABuiltinNumberFormat 매개변수를 지닌 오버로드는 14(짧은 날짜) 같은 내장 Excel 형식을 별도로 보고하는데, NumberFormatIndex는 통합 문서의 사용자 지정 NumberFormats 풀을 가리킵니다. Cells.HasCell(R, C)는 물리적 셀이 존재하는지를 답합니다
var
Sheet: TXLSXWorksheet;
Before, After: TXLSXCellStorageStatistics;
State: TXLSXCellStyleState;
Row, Builtin, Unformatted, Physical: Integer;
begin
Sheet := Workbook.Sheets[1]; // Sheets는 1 기반
Before := Sheet.Cells.GetStorageStatistics;
Unformatted := 0;
Physical := 0;
for Row := 1 to 200000 do
begin
// 유효 스타일, 시트에 셀을 추가하지 않고 해석
State := Sheet.GetCellVisibleFormat(Row, 2, Builtin);
if (State.NumberFormatIndex = 0) and (Builtin = 0) then
Inc(Unformatted);
if Sheet.Cells.HasCell(Row, 2) then
Inc(Physical);
// Sheet.Cells[Row, 2].NumberFormatIndex도 같은 질문에 답하지만,
// 방문하는 행마다 새 셀 객체 하나를 남겨둔다
end;
After := Sheet.Cells.GetStorageStatistics;
// 두 개수는 루프 전후로 동일해야 한다
WriteLn(Format('cells %d -> %d, styled blanks %d -> %d',
[Before.CellCount, After.CellCount,
Before.StyledBlankCellCount, After.StyledBlankCellCount]));
end;
이런 검사를 실제 워크로드에 대해 한 번은 돌려 보십시오. StyledBlankCellCount는 스타일은 있지만 값, 수식, 리치 텍스트가 없는 셀을 셉니다. 읽기 전용으로 알려진 패스 동안 그 수가 늘어난다면 어떤 헬퍼가 서식을 담기 위해서만 셀을 만들고 있는 것입니다. 보통은 셀에 서식이 필요한지 판단하려고 Cells[R, C]를 호출하는 경우입니다
256행 블록이 여러분 코드에 의미하는 것
물리적 셀은 256행 블록으로 삽니다. 블록 안에서 채워진 행마다 셀을 열 기준 정렬된 목록으로 유지하고, 셀 없는 행은 그저 빈 슬롯이며, 셀이 전혀 없는 256행 구간은 블록 자체가 없습니다. 따라서 3, 7, 512행에 데이터가 있는 희소 시트는 두 개의 블록을 지닙니다. 저장은 같은 구조를 행 우선 순서로 훑는데, 이것이 SpreadsheetML이 <row> 요소와 그 <c> 자식에게 요구하는 순서입니다
여러분이 쥐고 있는 TXLSXCell 참조는 주변에서 셀이 추가되거나 제거되어도 같은 객체입니다. 블록이 재편성하는 것은 자기 장부이지 여러분의 핸들이 아닙니다. WriteFloatRowValues와 WriteStringRowValues 같은 대량 작성기는 새 시트에서 더 빠른 길을 택해 행 우선 값을 압축 저장소에 덧붙입니다. Cells[R, C]를 통한 첫 셀별 접근이 그 저장소를 한 번에 셀 객체로 변환합니다. GetStorageStatistics는 RowBlockCount와 PopulatedRowCount 옆에서 PackedStorageActive와 PackedCellCount로 어느 상태인지 보여 줍니다
스타일 풀은 저장 시점에 다듬어집니다. styles.xml이 기록되기 전에 HotXLS는 폰트, 채우기, 테두리, 숫자 형식, 정렬, 보호 풀을 여전히 사용 중인 항목만 담도록 재구축하므로, 사용자가 한 시간 동안 서식을 바꾼 통합 문서가 버려진 채우기를 전부 실어 나르지 않습니다. 절대 FormatIndex에 고정된 셀은 자기 슬롯을 그대로 유지합니다
행 단위 스트리밍 저장과 그것을 보여 주는 숫자들
TXLSXWorkbook.StreamingWrite는 기본값이 True입니다. 켜져 있으면 SaveAs는 각 워크시트 <row>를 생성해 그 ZIP 엔트리의 deflate 스트림으로 곧장 기록하므로, 임의 순간 메모리에 있는 가장 큰 워크시트 XML 조각은 단일 행입니다. 꺼져 있으면 모든 워크시트의 XML이 먼저 완전한 문자열 하나로 만들어진 뒤 압축되고, 피크 메모리는 가장 큰 시트를 따라갑니다. 사각형 인터벌이 변하는 스타일 전용 셀들, 도입부의 B2:D500000 사례도 그 행별 루프 안에서 생성되므로 파일에는 더해지지만 하나의 XML 블록으로 메모리에 앉는 일은 없습니다
공유 문자열과 피벗 캐시 레코드도 같은 패턴을 따릅니다. 각 <si>나 <r> 항목이 제각각 생성되고 기록되므로, 어떤 워크시트보다도 커지기 쉬운 공유 문자열 테이블이 넓은 분석 통합 문서에서 하나의 XML 블록으로 존재해야 할 이유가 없습니다
이 모든 것을 믿음으로 받아들일 필요는 없습니다. 저장마다 통합 문서가 네 개의 읽기 전용 프로퍼티를 노출합니다. LastSaveUsedStreaming, LastSavePeakRowXmlChars(가장 긴 단일 행, 문자 수), LastSaveBufferedWorksheetXmlChars(메모리에 든 워크시트 XML 총량, 스트리밍이면 0), LastSavePeakAuxiliaryXmlChars(가장 긴 공유 문자열 또는 피벗 항목)입니다. LastSaveUsedStreaming은 .xlsx, .xltx, .xltm 출력과 바이너리 .xlsb 작성기에서 True이고, OpenDocument 출력은 자체 작성기를 거치므로 False를 보고합니다
procedure ReportSave(Workbook: TXLSXWorkbook; const FileName: string;
Streaming: Boolean);
begin
Workbook.StreamingWrite := Streaming;
if Workbook.SaveAs(FileName) <> 1 then
raise Exception.CreateFmt('Save failed: %s', [FileName]);
WriteLn(Format('streaming=%s peakRow=%d buffered=%d peakAux=%d',
[BoolToStr(Workbook.LastSaveUsedStreaming, True),
Workbook.LastSavePeakRowXmlChars,
Workbook.LastSaveBufferedWorksheetXmlChars,
Workbook.LastSavePeakAuxiliaryXmlChars]));
end;
// 같은 통합 문서, 두 모드. 프로파일러로 프로세스 메모리를 지켜보십시오
ReportSave(Workbook, 'ledger-stream.xlsx', True);
ReportSave(Workbook, 'ledger-buffered.xlsx', False);
여러분의 데이터에서 버퍼링 줄은 시트 전체 XML에 버금가는 buffered 수치를 보여 주고, 스트리밍 줄은 0과 여러분의 가장 넓은 행보다 크지 않은 peakRow를 보여 줍니다. StreamingWrite는 켜두십시오. 플래그가 결정하는 것은 압축 전에 워크시트 XML을 온전히 조립할지 여부뿐이고, LastSaveBufferedWorksheetXmlChars가 그 조립의 크기를 문자 수로 보고합니다
남은 메모리가 쓰이는 곳
셀과 스타일이 압축되고 나면 큰 통합 문서의 다음 최대 소비자는 공유 문자열 테이블과 파일이 나르는 위성 파트들, 즉 피벗 캐시, 드로잉, 객체 모델이 모델링하지 않는 파트의 보존된 XML입니다. 이들에게는 각자의 레버가 있고, 모두를 커버하는 단일 설정은 없습니다
병목이 저장이 아니라 열기라면 선택적 로딩부터 시작하십시오. 메타데이터 전용 및 선택적 워크시트 로딩 산책문이 손대지 않을 시트의 비용을 치르지 않고 통합 문서를 읽는 방법을 다룹니다. 매우 큰 파일에서 읽기 경로 처리량은 병렬 XLSX 파싱과 메모리 할당자 노트를, 객체 모델이 전혀 필요 없는 출력 전용 작업은 서버 배치 작업을 위한 스트리밍 쓰기가 여기서 어떤 튜닝보다 더 잘 맞습니다
HotXLS는 Excel이나 OLE 자동화 없이 네이티브 Delphi와 C++Builder 코드에서 XLS와 XLSX를 읽고 쓰므로, 위의 모든 숫자는 여러분 자신의 프로세스에서 나오며 그 자리에서 확인할 수 있습니다. 지원 형식과 RAD Studio 버전은 HotXLS Delphi 스프레드시트 컴포넌트 페이지에 정리되어 있습니다