기술 문서

HotXLS Delphi Component: Delphi에서 sheet listing and lightweight workbook inspection

때로는 수신 루틴이 답해야 할 유일한 질문이 구조에 관한 것일 때가 있습니다. 이 워크북에 "Mapping"이라는 시트가 있는가, 또는 탭이 몇 개나 있는가와 같은 질문입니다. 이를 Open을 호출해서 답하는 것은 비용이 큰 방법입니다. 완전한 열기는 공유 문자열 테이블을 부풀리고, 모든 스타일 레코드를 디코딩하며, 각 워크시트의 셀을 순회합니다. 여러분이 목차만 원했다는 사실을 알 방법이 없기 때문입니다. 큰 파일에서는 몇 킬로바이트짜리 목록을 읽기 위해 수백 메가바이트의 할당과 몇 초의 CPU를 소비하는 셈입니다. losLab의 네이티브 Delphi 스프레드시트 라이브러리인 HotXLS는 이 목록을 스스로 제공합니다. GetSheetNames는 단 하나의 셀도 구체화하지 않고 워크북 순서대로 워크시트 이름을 돌려줍니다

카탈로그를 읽는 비용이 저렴한 이유

두 스프레드시트 형식 모두 목차를 앞부분에 두며, 이것이 목록 조회 호출을 빠르게 만드는 요인입니다. 특별한 기교가 필요한 것이 아닙니다. OOXML 패키지는 시트 카탈로그를 xl/workbook.xml에 보관하며, 이 파트는 워크북이 10행이든 천만 행이든 작게 유지됩니다. BIFF8 .xls는 BoundSheet 레코드를 워크북 전역 스트림의 시작 부분, 즉 어떤 셀 데이터보다도 앞에 저장합니다. 그러므로 목록 조회 호출이 피하는 작업은 완전한 열기에 비해 반올림 오차 수준이 아닙니다. 파일 대부분입니다. 카탈로그를 읽는 비용은 행 수와 무관하게 몇 킬로바이트로 동일한 반면, 완전한 열기는 데이터에 비례해 커지며, 수 메가바이트짜리 워크북에서는 접촉하는 바이트 수와 할당되는 메모리 양 모두에서 그 격차가 몇 자릿수에 이릅니다

Delphi에서 HotXLS GetSheetNames는 XLSX 또는 XLS 파일의 시트 카탈로그만 읽는 반면, 전체 열기는 모든 셀을 순회
카탈로그는 workbook.xml이나 BoundSheet 레코드에 있으므로, 나열은 몇 킬로바이트로 끝나는 반면 전체 열기는 데이터에 비례합니다

이 평평한 비용이야말로 이를 중심으로 설계할 가치가 있는 속성입니다. GetSheetNames 위에 구축된 수신 게이트는 200행짜리 파일과 200MB짜리 파일에서 똑같이 동작하므로, 배치 안에서 가장 느린 파일이 더 이상 처리할 가치가 있는지조차 결정하는 속도를 좌우하지 않습니다

.xls, .xlsx, 템플릿 형식을 아우르는 한 번의 호출

XLS 파사드에서 TXLSWorkbook.GetSheetNames는 .xls보다 더 많은 것을 읽습니다. zip 기반의 .xlsx, .xlsm, .xltx, .xltm도 받아들이며, 아카이브에서 workbook.xml만 뽑아냅니다. 진짜 .xls 입력에 대해서는 BoundSheet 레코드를 스캔하다가 전역 하위 스트림의 첫 번째 EOF 레코드에서 멈추므로, 큰 바이너리 파일이라도 그 시작 부분의 몇 킬로바이트만 비용을 치릅니다. XLSX 파사드는 처음 보기보다 오래 실행되는 서비스 코드에 더 중요한 보장을 지니고 있습니다. TXLSXWorkbook.GetSheetNames는 워크북 인스턴스를 초기화하지도 채우지도 않으므로, 이미 열린 문서를 담고 있는 인스턴스도 손에 든 것을 어지럽히지 않고 다른 파일들을 조사할 수 있습니다. GetODSSheetNames는 같은 접근 방식을 OpenDocument 패키지에 적용하며, 이 호출 하나하나 모두 스트림 오버로드를 지니고 있어 디스크에 닿은 적 없는 업로드도 조사할 수 있습니다

var
  Book: TXLSXWorkbook;
  Names: TStringList;
  I: Integer;
begin
  Names := TStringList.Create;
  Book := TXLSXWorkbook.Create;
  try
    if Book.GetSheetNames('upload-7f3a.xlsx', Names) <= 0 then
      raise Exception.Create('unreadable workbook package');
    if Names.IndexOf('Mapping') < 0 then
      raise Exception.Create('required Mapping sheet is missing');
    for I := 0 to Names.Count - 1 do
      Writeln(Format('sheet %d: %s', [I, Names[I]]));
  finally
    Book.Free;
    Names.Free;
  end;
end;

같은 호출은 훌륭한 데스크톱 가져오기 대화상자도 만들어 줍니다. 시트를 나열하고 사용자가 하나를 선택하게 한 다음, 선택이 이루어진 뒤에야 완전한 열기 비용을 치르십시오. 50개 시트짜리 워크북에서는 그 차이가 눈에 보입니다. 즉시 나타나는 선택기와, 전체 파일이 뒤에서 로드되는 동안 멈춰 있는 선택기의 차이입니다

매크로가 사용 가능한 .xlsm 파일과 템플릿 형식들은 평범한 .xlsx와 정확히 똑같이 목록화됩니다. 패키지 안에 vbaProject.bin이 함께 실려 있든 아니든 카탈로그는 같은 workbook.xml에 있기 때문입니다. 따라서 수신 파이프라인은 매크로 페이로드를 전혀 건드리지 않고, 그것을 실행할 만한 어떤 일도 하지 않은 채, 매크로 워크북의 시트를 라우팅을 위해 열거할 수 있으며, 매크로 정책 판단은 실제로 파일을 여는 단계에 맡길 수 있습니다

스스로를 속이지 않고 반환값 읽기

반환 관례는 HotXLS 전반에 걸쳐 균일하지 않습니다. 어떤 호출은 성공 시 1을 반환하고, 다른 호출은 개수를 반환하므로, 목록 조회 함수에 대해서만은 0 이하의 값은 무엇이든 실패로 취급하고 문자열 목록을 비운다는 검사만이 유효합니다. 빈 목록을 "시트가 없는 워크북"으로 읽고 싶은 유혹을 뿌리치십시오. ECMA-376과 BIFF8 명세 모두 유효한 워크북에는 최소 하나의 시트가 있어야 한다고 요구하므로, 이름이 0개라는 것은 항상 읽기가 실패했다는 뜻이지 파일이 정당하게 비어 있다는 뜻이 결코 아닙니다

실패한 목록 조회 자체도 지킬 가치가 있는 신호입니다. 이 호출이 실패하는 .xlsx 파일은 몇 가지 특정한 경우 중 하나입니다. 잘려 나갔거나, 사실은 진짜 OOXML 패키지가 전혀 아니거나(다른 시스템에서 나온, 잘못 라벨링된 CSV 내보내기가 여기서 끊임없이 나타납니다), 또는 암호화된 컨테이너입니다. 이들을 구분하는 것은 다음 검사의 몫입니다. 거부된 파일의 첫 몇 바이트를 실패와 함께 로깅하면 보통 지원 스레드 하나가 메시지 하나로 끝납니다

라우팅 전에 암호화된 컨테이너 감지하기

암호화된 .xlsx는 zip이 아닙니다. EncryptionInfoEncryptedPackage 스트림을 감싼 OLE 복합 파일이므로, GetSheetNames는 그 안을 볼 수 없고 다른 읽을 수 없는 파일과 마찬가지로 실패를 반환합니다. CanReadEncrypted는 그 컨테이너 형태를 검사하며, 이를 통해 수신 단계는 워커 깊숙한 어딘가에서 나온 일반적인 읽기 오류를 그냥 삼키는 대신 암호화된 파일을 의도적으로 라우팅할 수 있습니다:

HotXLS CanReadEncrypted와 GetSheetNames로 업로드를 needs-password, unreadable, normal으로 라우팅하는 Delphi 인테이크 트리아지 흐름
CanReadEncrypted가 먼저 실행됩니다. 암호화된 OOXML 파일은 나열 호출이 안을 들여다볼 수 없는 OLE 컨테이너이기 때문입니다
type
  TIntakeRoute = (irNormal, irNeedsPassword, irUnreadable);

function ClassifyUpload(const FileName: string; Names: TStrings): TIntakeRoute;
var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    // 암호화된 OOXML은 zip이 아니라 OLE 컨테이너입니다: 목록 조회
    // 호출은 그 안을 볼 수 없으므로 먼저 확인합니다.
    if Book.CanReadEncrypted(FileName) then
      Exit(irNeedsPassword);
    if SameText(ExtractFileExt(FileName), '.ods') then
    begin
      if Book.GetODSSheetNames(FileName, Names) <= 0 then
        Exit(irUnreadable);
    end
    else if Book.GetSheetNames(FileName, Names) <= 0 then
      Exit(irUnreadable);
    Result := irNormal;
  finally
    Book.Free;
  end;
end;

암호화는 HotXLS가 의도적으로 비대칭인 지점이므로, 라우팅도 이를 존중해야 합니다. 레거시 .xls 암호화(RC4, RC4 CryptoAPI, XOR)는 읽을 수 있습니다. TXLSWorkbook.Open(FileName, Password)가 저장된 비밀번호로 복호화하며, 이런 파일은 자동화된 경로에 그대로 둘 수 있습니다. 암호화된 OOXML 패키지는 반대입니다. HotXLS는 SaveAsEncrypted로 이를 쓸 수 있지만, 다시 읽을 수는 없습니다. OpenEncrypted는 암호화된 패키지를 건네받으면 EXlsxEncryptionNotImplemented를 발생시키며, 이것이 정직한 수신 설계가 암호화된 .xlsx는 Excel을 가진 사람에게 보내고 비밀번호를 지닌 .xls는 코드 안에 남겨두는 이유입니다

배치 작업에서는 각 조사가 파일 열기 한 번과 몇 킬로바이트의 읽기 정도의 비용만 들기 때문에, 이 분류기가 실제 처리를 시작하기 전에 전체 수신 디렉터리를 미리 훑는 방식으로 제자리를 찾습니다. 이를 앞으로 당겨오면 운영팀이 실제로 신경 쓰는 실패 양상이 바뀝니다. 새벽 세 시에 작업이 600개 중 412번째 파일에서 죽는 대신, 412개의 파일이 큐에 들어가고 5개는 각각 이유가 붙은 채 수신 단계에서 거부됩니다. 같은 라이브러리 호출인데도 운영 측면의 이야기는 훨씬 낫습니다

목록 조회 호출이 답할 수 없는 질문들

이름과 순서가 여러분이 얻는 것의 전부입니다. 목록 조회 호출은 가시성에 대해서는 아무 말도 하지 않으므로, 숨겨진 시트와 매우 숨겨진 시트도 다른 시트와 똑같이 목록에 나타납니다. 사용된 범위의 치수, 셀 개수, 문서 속성도 보고하지 않습니다. docProps/core.xml 파트도 작지만, 오늘날에는 속성만을 위한 조회가 없으므로 작성자와 제목 메타데이터는 여전히 완전한 Open의 비용을 치러야 합니다. 이를 받아들이는 깔끔한 방법은 저렴한 사실들이 모든 파일을 라우팅하게 하고, 값비싼 사실들은 라우팅에서 살아남은 파일들을 위해 아껴두는 것입니다. 라우팅을 거쳐 실제로 깊은 읽기로 진행하는 파일들의 경우, 큰 .xls의 읽기 전용 스캔은 OfficeArt 파싱을 건너뛰는 _DisableGraphics := True와 함께라면 눈에 띄게 빨라집니다. 다만 그 인스턴스에서는 절대 저장하지 마십시오. 건너뛴 도면 계층은 모델에서 사라진 상태이며, 저장하면 파일에서도 그것이 빠지게 됩니다

선별 검사를 통과한 파일은 보통 더 깊은 분석으로 향합니다. 워크북 감사 및 변환 워크벤치는 완전한 열기가 정당화된 이후 수집할 가치가 있는 시트별 카운터를 다루며, 대규모 워크북 성능 가이드는 그 완전한 열기를 빠르게 유지하는 방법을 다룹니다

HotXLS는 Delphi와 C++Builder를 위한 네이티브 Object Pascal 스프레드시트 라이브러리입니다. 여기서 소개한 조사 호출을 포함한 전체 API 표면은 HotXLS Delphi Component 제품 페이지에 문서화되어 있습니다