기술 문서

델파이 HotXLS 통합 문서 감사와 형식 변환

대량 스프레드시트 정규화 작업은 외투 하나를 걸친 세 가지 문제입니다. 형식이 뒤섞인 보관소가 있습니다. BIFF 시절 .xls, 요즘 .xlsx, 어떤 LibreOffice 실험에서 흩뿌려진 .ods 몇 개, 그리고 암호가 퇴사한 직원과 함께 걸어 나가는 바람에 아무도 열지 못하는 파일 한 줌입니다. 목표는 전부를 XLSX와 CSV로 바꾸는 것입니다. 대부분의 사람이 쓰는 그 작업의 판본은 파일마다 열어 새 확장자로 저장하는 루프이고, 어느 파일이 차트를 잃었고 매크로를 떨어뜨렸고 아예 열리지도 않았는지 누가 물을 때까지는 잘 굴러갑니다. 그 루프에는 답이 없습니다. 변환만으로는 아무 기록도 남지 않기 때문입니다. 작업대에는 답이 있습니다. 먼저 목록을 만들고, 다음에 변환하고, 셋째로 검증합니다. 그리고 그 셋이 서로 정보를 나눠 가져야 무엇이든 믿을 만해집니다

델파이나 C++Builder에서 그 작업대를 조립한다는 것은 HotXLS의 네 가지 능력을 엮는 일이며, 그중 어느 것도 파이프라인 어디에도 Excel 설치를 요구하지 않습니다. 네이티브 엔진이 둘 있습니다. .xls를 위한 BIFF8 파사드와 .xlsx·.ods를 위한 OOXML 파사드입니다. 파일 전체를 구문 분석하지 않고 메타데이터를 읽는 값싼 탐색 호출이 있습니다. 통합 문서가 실제로 무엇을 지녔는지 알려 주는 시트별 감사 계수기가 있습니다. 그리고 경로마다 문서화된 충실도 프로필이 딸린 변환 행렬이 있습니다. 일은 그 각각의 날카로운 모서리가 어디인지 아는 데 있습니다. 하나같이 모서리가 있고, 그 모서리야말로 말끔한 야간 배치를 월요일 아침 사고로 바꿔 놓는 바로 그것이기 때문입니다

델파이에서 감사를 먼저 하는 HotXLS 변환 작업대의 파이프라인 그림. xls와 xlsx와 ods가 뒤섞인 보관소의 목록을 만들고, 경로별로 변환한 다음, 목록 단계에서 기록한 이전 수치와 견주어 검증한다
작업대는 세 단계로 변환하며, 목록 단계에서 기록한 감사 계수가 검증이 견줄 이전 수치가 됩니다

적재하기 전에 살펴보십시오: 시트 이름과 암호화 탐지

200MB 통합 문서를 열어 놓고서야 암호화된 것임을 알아채면 파일마다 몇 분씩 낭비하고, 큰 보관소 전체로 곱하면 며칠을 낭비합니다. 양쪽 파사드 모두 GetSheetNames를 드러내는데, 통합 문서를 채우지 않고 시트 메타데이터만 읽습니다. BIFF 구현은 스트림 앞머리의 BoundSheet 레코드만 훑고, OOXML 구현은 zip 안의 workbook.xml만 읽습니다. 그 곁에서 CanReadEncrypted는 복호화를 시도하지 않고 암호화 컨테이너를 탐지합니다:

var
  Probe: TXLSXWorkbook;
  Names: TStringList;
begin
  Names := TStringList.Create;
  Probe := TXLSXWorkbook.Create;
  try
    if Probe.CanReadEncrypted(FileName) then
    begin
      Writeln(FileName + ': encrypted container - route to manual handling');
      Exit;
    end;
    if Probe.GetSheetNames(FileName, Names) <= 0 then
      Writeln(FileName + ': unreadable - quarantine')
    else
      Writeln(Format('%s: %d sheet(s), first "%s"',
        [FileName, Names.Count, Names[0]]));
  finally
    Probe.Free;
    Names.Free;
  end;
end;

운영상의 세부 둘이 이 루프를 값싸게 만듭니다. GetSheetNames는 통합 문서 인스턴스를 초기화하지도 채우지도 않으므로, 탐색용 객체 하나로 다시 만들 필요 없이 수천 개 파일을 분류할 수 있습니다. 그리고 같은 호출의 XLS 파사드 판본은 .xlsx 패키지도 이해하므로, 파일 확장자를 믿을 수 없을 때 편리한 단일 탐색기가 됩니다. 그만큼 오래된 보관소에서 확장자를 믿을 수 있는 일은 드뭅니다. 적재 전 분류는 따로 다룰 값어치가 있으며, 가벼운 검사의 작동 원리는 시트 나열과 가벼운 통합 문서 검사를 다룬 글에 있습니다

델파이 HotXLS 통합 문서 배치의 분류 흐름도. CanReadEncrypted가 암호화된 컨테이너를 수동 처리로 돌리고, GetSheetNames가 읽을 수 없는 파일을 격리하며, 통과한 파일은 변환 경로를 정하는 감사 단계로 들어간다
CanReadEncrypted와 GetSheetNames로 살펴보면 적재 전에 모든 파일이 분류되므로, 암호화되었거나 읽을 수 없는 통합 문서는 변환 루프에 결코 닿지 않습니다

통합 문서가 정말로 담고 있는 것 세기

파일이 분류를 통과하고 나면 감사 단계가 그 변환 경로를 정합니다. XLSX 파사드는 충실도 판단에 걸리는 모든 기능 계열의 계수기를 드러냅니다. 병합된 셀, 차트, 이미지, 조건부 서식, 데이터 유효성 검사, 표, 하이퍼링크, 메모에, 매크로와 보호와 원본 형식에 대한 통합 문서 수준 플래그가 더해집니다. 파일의 변환 경로는 이 중 어느 것이 0이 아닌 값으로 돌아오는지에 거의 전적으로 달려 있습니다

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  I: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open(FileName) <> 1 then Exit;
    for I := 0 to Book.Sheets.Count - 1 do
    begin
      Sheet := Book.Sheets[I];
      Writeln(Format('%s: cells=%d merges=%d charts=%d cf=%d dv=%d protected=%s',
        [Sheet.Name, Sheet.Cells.Count, Sheet.MergedCells.Count,
         Sheet.Charts.Count, Sheet.ConditionalFormats.Count,
         Sheet.DataValidations.Count, BoolToStr(Sheet.IsProtected, True)]));
    end;
    if Book.HasVbaProject then
      Writeln('  contains VBA project - macro policy applies');
    if Book.ExternalLinks.Count > 0 then
      Writeln(Format('  %d external link(s)', [Book.ExternalLinks.Count]));
  finally
    Book.Free;
  end;
end;

Cells.Count는 단서 하나를 마음에 두고 읽으십시오. 셀 저장소는 희소하므로, 이 숫자는 사용된 범위의 직사각형 넓이가 아니라 실제로 만들어진 셀을 셉니다. A1에 값 하나, ZZ9999에 또 하나가 있는 시트는 그 사이에 놓인 백만 남짓이 아니라 두 개를 보고합니다. BIFF 쪽의 같은 스캔은 UsedRange 경계를 ForEachCell과 함께 쓰는데, 거의 모두가 처음에 걸려 넘어지는 하나 어긋남을 안고 있습니다. UsedRange.FirstRow와 그 형제들은 0 기반인데 Cells.Item[Row, Col]은 1 기반입니다. 경계마다 하나 더하기를 잊은 순회는 엉뚱한 직사각형을 감사하고서도 그렇다고 말해 주지 않습니다

지렛대 둘이 대용량 예전 파일에 대한 감사 전용 단계의 비용을 줄여 줍니다. .xls를 열기 전에 _DisableGraphics를 true로 두면 OfficeArt 그리기 계층 구문 분석을 통째로 건너뛰므로, 도형이 빽빽한 통합 문서에서 실질적인 시간을 아낍니다. 다만 이것은 엄격히 읽기 전용 최적화입니다. 그렇게 연 인스턴스에서 저장하면 구문 분석하지 않은 그림이 떨어져 나가므로, 이 플래그는 파일을 결코 되쓰지 않을 경로에만 속합니다. 감사에 개수가 아니라 셀별 내용이 필요할 때는 ForEachCell 콜백이 채워진 셀을 곧장 훑으면서, 인덱싱된 셀 속성이 읽을 때마다 치르는 접근당 Variant 부담을 비켜 갑니다. 그 부담은 수백만 셀에 걸쳐 빠르게 쌓입니다

들쭉날쭉한 반환 코드를 일찍 정규화하십시오

HotXLS의 I/O 호출은 예외가 아니라 정수 결과로 오류를 보고하고, 그 관례는 API 전반에 걸쳐 한결같지 않습니다. 대부분의 열기와 저장 호출은 성공에 1, 실패에 -1을 돌려줍니다. GetSheetNames는 시트 개수를 돌려주거나, 목록을 비운 채 -1을 돌려줍니다. XLSX의 SaveAsHTML은 그 무늬를 또 한 번 깨고 성공에 0, 시트 인덱스 범위 초과에 -1을 돌려줍니다. 어디서나 = 1을 검사하는 작업대는 다른 방식으로 성공을 알리는 호출을 조용히 잘못 분류하고, <> -1을 검사하는 작업대는 다른 코드로 실패하는 호출을 삼켜 버립니다

API 전체와 부딪히고도 살아남는 규칙은 보이는 것보다 좁습니다. 개수를 돌려주는 호출에서는 <= 0을 실패로 다루고, 실제로 쓰는 저장 루틴마다 문서화된 성공 값을 확인하고, 그 둘을 작은 결과 검사 함수 하나 뒤에 두어 관례가 정확히 한자리에서만 살게 하십시오. 배치 파이프라인은 진기한 파서 버그보다 검사하지 않은 반환 코드가 느리게 쌓이는 것 때문에 훨씬 자주 무너지며, 이것을 틀린 대가는 사만 개 파일 뒤에, 어느 변환이 실제로 이뤄졌는지 아무도 기억하지 못할 때 치릅니다

변환 행렬과 각 길이 데이터를 잃는 자리

두 파사드가 변환 일을 나눠 가집니다. TXLSXWorkbook은 XLSX, ODS, CSV를 열고 XLSX, ODS, CSV, HTML, RTF, AES로 암호화된 XLSX를 저장합니다. TXLSWorkbook은 BIFF를 열고 저장하며 HTML, RTF, CSV로 내보냅니다. 쓸모 있는 점은 각 경로에 정확성에 대한 막연한 약속이 아니라 문서화된 충실도 프로필이 딸려 있다는 것이어서, 어느 경로가 어느 파일에 안전한지 미리 정할 수 있습니다

CSV 내보내기는 BOM이 붙은 UTF-8, CRLF 줄 끝, RFC 4180 따옴표 규칙으로 씁니다. 하지 않는 일은 수식 평가입니다. =SUM(...)을 지닌 셀은 글자 그대로의 수식 텍스트로 내보내지므로, 먼저 값을 계산하지 않으면 수식으로 된 시트가 문자열로 된 시트가 됩니다. HTML 내보내기는 표 하나를 만드는데, 병합된 셀 자리에는 colspan과 rowspan이 서고 기본 스타일은 인라인으로 들어갑니다. RTF 내보내기의 한계는 더 날카롭습니다. 병합된 셀을 열에 걸쳐 펼치지 못하므로, 병합의 이어지는 셀은 비어 나옵니다. ODS 가져오기는 라이브러리 자체 문서에 따르면 일부러 가볍게 만들어졌습니다. 스칼라 값과 캐시된 수식 결과는 넘어오고, 스타일과 살아 있는 ODF 수식 식과 그림은 넘어오지 않습니다. 보관소에 OASIS ODF 1.3을 따르는 진짜 OpenDocument 파일이 들어 있는 순간 그것이 중요해집니다. 시각적으로 충실한 변환에 가까운 무엇이든 이 가져오기 경로가 지고 가도록 만들어진 것보다 더 많은 것을 요구하며, 배치가 그런 파일을 조용히 납작하게 만들기 전에 그것들이 존재한다고 알려 주는 것이 감사 단계입니다

SaveXLSWorkbookAsXLSX는 데이터 다리이지 배치 다리가 아닙니다

BIFF 파사드는 OOXML을 곧장 쓰지 못하므로, .xls에서 .xlsx로 건너가는 일은 lxXlsxExport 유닛의 SaveXLSWorkbookAsXLSX 함수를 거쳐 갑니다. 그 다리의 충실도는 분명히 말해 둘 값어치가 있습니다. 이름이 실제보다 더 많은 것을 시사하기 때문입니다. 이 함수는 값, 수식, 숫자 형식, 채우기 색, 핵심 글꼴 속성, 열 너비, 눈금선 같은 보기 설정을 복사합니다. 테두리, 병합된 범위, 메모, 차트, 조건부 서식은 복사하지 않습니다. 하류 시스템이 결과를 구문 분석하고 아무도 서식을 들여다보지 않는 데이터 등급 정규화에는 그것으로 정확히 충분하고, 누구에게든 필요한 것은 아무것도 잃지 않습니다. 사람이 읽으라고 만든 서식 있는 이사회 보고서에는 충분하지 않으며, 바로 여기서 감사 계수기가 제 자리를 벌어들입니다. 감사가 차트와 조건부 서식을 지녔다고 표시한 파일은, 그 둘을 말없이 떨어뜨릴 다리가 아니라 수동 대기열로 가야 합니다

델파이 HotXLS SaveXLSWorkbookAsXLSX의 다리 충실도 그림. 값, 수식, 숫자 형식, 채우기 색, 핵심 글꼴 속성, 열 너비, 보기 설정은 BIFF xls에서 XLSX로 건너가고, 테두리와 병합된 범위와 메모와 차트와 조건부 서식은 떨어진다
SaveXLSWorkbookAsXLSX는 파서가 필요로 하는 데이터를 BIFF에서 OOXML로 건너 나르고, 차트와 병합이 떨어질 파일을 표시해 주는 것은 감사 계수기입니다
var
  Legacy: IXLSWorkbook;        // 인터페이스 참조: Free 하지 마십시오
  Modern: TXLSXWorkbook;
begin
  if SameText(ExtractFileExt(FileName), '.xls') then
  begin
    Legacy := TXLSWorkbook.Create;
    if Legacy.Open(FileName) <= 0 then Exit;
    if SaveXLSWorkbookAsXLSX(Legacy,
         ChangeFileExt(FileName, '.xlsx')) <= 0 then
      Writeln('bridge failed: ' + FileName);
  end
  else
  begin
    Modern := TXLSXWorkbook.Create;
    try
      Modern.StreamingWrite := True;     // 시트 XML을 zip으로 흘려보냅니다
      if Modern.Open(FileName) = 1 then
        Modern.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
    finally
      Modern.Free;
    end;
  end;
end;

위의 루프는 OOXML 쪽의 처리량 지렛대도 보여 줍니다. StreamingWrite를 true로 두면 워크시트 XML을 메모리에 거대한 문자열 하나로 쌓아 두는 대신 출력 패키지로 곧장 흘려보내는데, 파일이 수십만 행에 이르렀을 때 편안한 실행과 메모리 부족 충돌을 가르는 차이입니다. 그 모드의 규모 산정과 메모리 거동은 서버 배치 작업을 위한 스트리밍 쓰기 글에서 따로 다룹니다. 모든 코어를 쓰고 싶은 배치에는 속성이 하나 더 중요합니다. 어느 파사드도 스레드 안전하지 않지만 어느 쪽도 전역 상태를 공유하지 않으므로, 병렬 변환에서 지원되는 패턴은 작업자 스레드마다 통합 문서 인스턴스를 하나씩 두고 그 사이에 잠금을 두지 않는 것입니다

암호 걸린 파일과 그것을 어떻게 할지

보관소의 잠긴 파일은 형식에 따라 깔끔하게 갈리고, 그 갈림이 어디로 갈지를 정합니다. 예전 .xls 암호화는 RC4든, CryptoAPI 위의 RC4든, 오래된 XOR 난독화든 읽을 수 있습니다. 암호를 Open에 건네면 파일은 다른 어떤 파일과도 같이 변환됩니다. 암호화된 .xlsx 패키지는 이야기가 다릅니다. HotXLS는 CanReadEncrypted로 그것을 탐지하지만 복호화하지는 못하므로, 정직한 움직임은 사람이 Excel에서 하나씩 열고 다시 저장한 뒤 파이프라인에 되돌려 놓는 대기열로 보내는 것뿐입니다. 그 비대칭은 미리 설계에 반영할 값어치가 있습니다. 암호화된 XLSX 파일이야말로 누군가 실제로 아끼는 기록일 가능성이 가장 높기 때문입니다

검증으로 고리 닫기

세 번째 단계가 건너뛰어지는 단계이고, 그것을 건너뛰는 것이 대량 변환을 책임 문제로 바꿉니다. HotXLS의 어느 저장 경로도 수식을 평가하지 않습니다. Excel은 파일을 열 때 다시 계산하므로 XLSX에서 XLSX로 가는 변환은 옳게 남지만, CSV 대상은 파이프라인이 먼저 셀에 Calculate를 돌려 결과를 되쓰지 않는 한 수식 텍스트를 글자 그대로 받습니다. 그것을 미리 아는 것이 숫자로 가득한 CSV와, 하류 가져오기가 목이 막힐 때까지 아무도 알아채지 못하는 =SUM(...) 문자열로 가득한 CSV의 차이입니다

검증 자체는 빼놓을 핑계가 없을 만큼 값쌉니다. 변환된 파일을 같은 라이브러리로 다시 열어 감사 계수기를 다시 돌리고, 목록 단계가 이미 기록해 둔 변환 전 수치와 견주십시오. 줄어든 시트 수, 원본에는 셋이었는데 0이 된 차트 수, 벼랑처럼 떨어진 셀 수. 하나하나가 한 번 더 여는 값으로 잡아낸 조용한 손실입니다. 그 위에 Excel이나 LibreOffice에서 표본을 눈으로 뽑아 확인하면, 그 조합이 출하 전에 변환 손상의 압도적 다수를 잡아냅니다. 목록 단계가 검증 단계에 먹이를 주는 이유가 바로 이것입니다. 이전 수치가 없으면 이후 수치는 아무것도 증명하지 못합니다

감사를 먼저 하는 작업대는 위험한 대량 변환을 측정 가능한 절차로 바꾸고, 깨끗이 통과하지 못하는 파일에는 격리 차선을 내줍니다. 여기서 보인 탐색과 계수와 변환 호출은 모두 HotXLS Delphi Component의 일부이며, Excel 자동화 없이 프로세스 안에서 네이티브로 돕니다