본문으로 건너뛰기

.xlsb의 정의된 이름 — 변환 단계 제거

문서 목록으로

정의된 이름을 테이블 경계로 쓰는 레이아웃이 .xlsb 워크북을 미리 변환하지 않고 직접 읽게 하는 설계입니다.

바뀌는 것은 이름을 어디에서 읽는가 하나입니다. 셀을 읽는 경로도, 임포터가 만드는 RawSheet 격자도, 그 아래 전부도 그대로입니다. 그래서 이 문서에서 분량이 가장 큰 절은 설계가 아니라 4절 — 무엇으로 같다고 판정하였는가입니다.

워크북 읽기가 「나중」으로 미뤄 둔 항목입니다. 그 문서는 선행 조건 2개를 명시하였습니다 — BrtName 파서와, 변환본과 셀 개수가 다른 워크북 7개의 원인 규명. 이 문서의 3~4절이 앞의 것을, 2절이 뒤의 것을 처리합니다.

부수적으로 비용도 낮아집니다. 같은 61 MiB 워크북에서 원본 .xlsb 직독이 0.9초 · 52 MB, 변환본 .xlsx가 4.1초 · 126 MB입니다(실측, 같은 문서의 9절). 변환 단계를 없애는 것이 읽기 자체도 빠르게 합니다.


1. 변환 단계가 존재하는 이유

스트리밍 셀 리더는 워크북의 정의된 이름을 보고하지 않습니다. 그래서 WorkbookPackage가 패키지에서 직접 읽습니다. 읽는 대상이 XML 파트 하나로 고정되어 있습니다.

var workbook = Part(zip, "xl/workbook.xml");
if (workbook is null)
return new WorkbookPackage(definedNames, skippedNames, notes);

.xlsb도 zip이지만 안에 든 파트가 XML이 아닙니다. 실측입니다.

xl/workbook.bin xl/styles.bin xl/sharedStrings.bin xl/calcChain.bin
xl/workbook.xml : 없음

그래서 이름이 0개로 반환됩니다. 예외도 경고도 발생하지 않습니다. 이름 기반 레이아웃은 어떤 이름도 덮지 않는 시트를 읽지 않으므로(XlsxImporter.cs:219), 결과는 테이블 0개로 성공하는 실행입니다. 실패보다 발견이 늦습니다.

지금은 사전 변환 스크립트가 그 자리를 대신합니다. 그 대가가 넷입니다.

대가내용
변환기의 의존성Excel COM을 씁니다. Excel이 설치된 Windows에서만 수행할 수 있고, 리눅스·맥 검증 경로에는 이 단계가 존재하지 않습니다
파생물 103 MB.xlsx.xlsb의 약 2배이고, 파생물이므로 저장소에 등재하지 않습니다. 게이트도 없습니다
recipe가 사본을 지시소스 항목의 Path가 원본 워크북이 아니라 변환 산출물을 가리킵니다
경로 중복소스 경로는 하위 디렉터리까지 순회하므로, 변환 폴더를 별도로 기재하면 같은 워크북을 두 번 읽습니다. 이 주의사항이 recipe 주석에 상주합니다

2. 셀 값의 동등성 — 이미 성립

변환을 없애려면 두 가지가 성립해야 합니다 — 셀 값이 같을 것, 그리고 이름을 읽을 수 있을 것. 앞의 것은 이미 성립합니다.

같은 워크북 12개를 .xlsb와 그 변환 산출물 .xlsx로 각각 열어 SheetGridReader가 렌더링한 셀을 대조하였습니다. 실측 · 셀 8,802,157개.

항목결과
차이131개 (0.0015%)
차이의 종류전부 1종 — .xlsb 쪽만 비어 있음
숫자·날짜 렌더링, 수식 오류, 공유 문자열, 행 정렬전부 일치
정의된 이름의 사각형 안에 든 차이0개

131개의 원인은 셀이 아니라 입니다. 셀 리더가 일부 행의 필드 수를 실제보다 작게 보고하고, 그 뒤의 셀이 렌더링에서 누락됩니다.

어떤 워크북의 한 시트
3563행 xlsb: fields= 3 xlsx: fields=19 O열에 값 있음
3564행 xlsb: fields=15 xlsx: fields=15 일치
3569행 xlsb: fields= 2 xlsx: fields=19 O열에 값 있음

셀 레코드는 .xlsb 안에 정상적으로 존재하고(BrtCellIsst, 공유 문자열 테이블도 정상), 리더가 행을 짧게 종료합니다. 바로 인접한 3564행은 양쪽이 일치하므로 전면적인 결함이 아닙니다.

이 결함은 이번 작업의 선행 조건이 아닙니다. 이름 기반 레이아웃이 읽는 범위는 이름 사각형 안쪽이고 거기에는 차이가 없습니다. 다만 시트를 통째로 읽는 레이아웃에 .xlsb가 입력되는 조합이 생기면 그때 다시 판단해야 합니다 — 7절.

3. 읽어야 하는 것

.xlsb의 파트는 이진 레코드 스트림입니다. 레코드 하나는 [번호][길이][내용]이고 번호와 길이는 가변 길이 정수입니다. 각 XML 원소에 대응하는 레코드가 있고, 이름 앞에 붙는 Brt는 Binary Record Type의 약자입니다. 형식은 Microsoft가 MS-XLSB로 공개한 규격입니다.

xl/workbook.xmlxl/workbook.bin번호
<definedName>BrtName39
<sheet>BrtBundleSh156
(XML에는 대응이 없음)BrtExternSheet — XTI 테이블362
(XML에는 대응이 없음)BrtSupSelf — 자기 자신인 보조 통합문서357

BrtName의 내용은 다음과 같습니다.

flags(4) chKey(1) itab(4) name(길이4 + UTF-16) rgce(길이4 + 수식 토큰)
│ └ 참조가 여기에 들어 있습니다
└ 0xFFFFFFFF 이면 워크북 스코프. 그 밖은 시트 스코프이고
XML의 `localSheetId` 와 같은 뜻입니다

핵심은 rgce입니다. XML에서 참조는 'Ocean Zone'!$A$1:$IP$100 같은 문자열이라 현재 TryParseArea가 따옴표·$·:를 직접 분해합니다. 이진 쪽은 이미 파싱된 토큰이므로 사각형이 정수로 나옵니다.

ptg(1) ixti(2) rowFirst(4) rowLast(4) colFirst(2) colLast(2) = 15바이트
0x3B PtgArea3d

하나의 사각형인지 여부가 토큰의 종류로 판정됩니다. 지금 문자열의 형태로 걸러내는 것들 — union, 열 전체(A:A), 다른 통합문서로의 참조 — 이 전부 다른 토큰이거나 다른 길이입니다. 문자열 파싱보다 정밀합니다.

시트 이름을 얻는 경로만 한 단계 더 있습니다. ixti는 시트 번호가 아니라 XTI 테이블의 인덱스입니다. 그 테이블도 같은 workbook.bin 안에 있고, 항목 하나가 12바이트입니다.

BrtExternSheet = cXti(4) + rgXti[cXti]
rgXti 항목 = iSupBook(4) itabFirst(4) itabLast(4)

iSupBookBrtSupSelf의 자리이고 itabFirst == itabLast일 때만 이 워크북의 시트 하나를 지시합니다. 그 값이 BrtBundleSh 등장 순서의 인덱스이고, 거기에서 시트 이름이 나옵니다.

이 한 단계를 생략할 수 없는 이유가 실측에 있습니다. 시트 140개짜리 워크북에서 ixti=174가 관측되었고, 그 워크북의 XTI 테이블은 190개 항목을 가집니다. ixti를 시트 번호로 취급하면 범위를 벗어나거나 다른 시트를 지시합니다.

4. 판정 — 253개 완전 일치

3절의 해석을 구현하여, 같은 워크북 12개에서 .xlsb가 산출한 (이름, 시트, 사각형).xlsx가 산출한 것 을 대조하였습니다. 이것이 이 설계의 성립 근거입니다.

워크북해석된 이름사각형이 아니어서 제외불일치
12개 합계253개46개0개

한쪽에만 있는 이름 0개, 사각형이 다른 이름 0개입니다. 제외된 46개는 union·열 전체·삭제된 대상이고, 양쪽이 같은 46개를 같은 이유로 제외합니다.

판정 항목기준
이름의 집합양쪽이 완전히 같을 것
시트 이름이름마다 같을 것
사각형이름마다 네 좌표가 모두 같을 것
제외된 이름같은 이름이 같은 사유로 제외될 것

5. 설계

WorkbookPackage.Read가 확장자로 두 갈래로 분기합니다. 호출하는 쪽은 바뀌지 않습니다DefinedNamesSkippedNames의 형태가 같기 때문입니다.

확장자이름을 읽는 곳
.xlsx · .xlsmxl/workbook.xml — 지금 그대로
.xlsbxl/workbook.bin — 이번에 추가

SkippedName의 사유는 지금의 두 가지를 그대로 씁니다. 판정 근거만 달라집니다.

사유XML에서의 판정이진에서의 판정
NotARange참조가 비었거나 #REF! 를 포함rgce가 비었거나 참조 토큰이 아님
NotOneRectangleunion·열 전체·외부 통합문서를 문자열 형태로 판별PtgArea3d 단독이 아니거나, XTI가 다른 통합문서 또는 여러 시트를 지시

코어에 새 개념이 생기지 않습니다. 레이아웃도, 어트리뷰트도, recipe 스키마도 관여하지 않습니다 — 「이 확장자의 워크북에서 정의된 이름을 읽는다」는 임포터 내부의 일입니다.

6. 게이트

픽스처 한 쌍을 등재합니다 — 같은 워크북의 .xlsb.xlsx. 테스트는 양쪽에서 읽은 정의된 이름을 4절의 판정 항목으로 대조합니다.

픽스처를 새로 만드는 이유는 두 가지입니다. 샘플 워크북은 변환 산출물이 등재되어 있지 않아 대조 상대가 없고, 크기가 게이트에 부적합합니다. 픽스처는 시트 2~3개에 다음을 포함하는 최소 크기로 만듭니다.

픽스처가 담아야 하는 것무엇을 지키는가
워크북 스코프 이름 여러 개기본 경로
시트 스코프 이름 하나itab으로 제외되는 것
공백이 포함된 시트 이름의 이름따옴표 처리가 XML 쪽에만 있는 문제였음을 확인
union 또는 열 전체 이름 하나NotOneRectangle이 양쪽에서 같게 나오는 것
삭제된 대상(#REF!)의 이름 하나NotARange가 양쪽에서 같게 나오는 것

셀 값의 동등성은 이 게이트의 대상이 아닙니다. 2절에서 별도로 측정하였고, 이 작업이 그것을 변경하지 않기 때문입니다.

6′. 구현 — 설계에서 달라진 것 둘

구현은 BinaryDefinedNames.cs이고, 게이트는 BinaryDefinedNameTests.cs와 픽스처 쌍 test/fixtures/xlsx/binary-names/입니다. 픽스처를 만드는 데만 Excel이 필요하고 읽는 데는 필요 없으므로 CI에서 그대로 돕니다.

3절의 해석이 두 군데에서 실물과 달랐습니다. 둘 다 픽스처가 잡았습니다.

무엇설계실물
Ptg의 값 클래스토큰 하나에 코드 하나로 적었습니다 (PtgArea3d = 0x3B)비트 5~6이 값 클래스라 코드가 셋입니다 (0x3B·0x5B·0x7B). 마스킹으로 지울 수 없습니다 — 0x20 미만의 코드는 클래스가 벗겨진 같은 토큰이 아니라 다른 토큰입니다
삭제된 대상PtgAreaErr3d 토큰으로 나타난다고 봤습니다토큰은 그대로 PtgArea3d이고, XTI 항목 쪽이 itabFirst = itabLast = -1로 비워집니다. XML이 #REF!로 적는 것과 같은 상태입니다

뒤의 것이 특히 픽스처가 아니었으면 늦게 발견될 것이었습니다 — 잘못 읽어도 「사각형이 아님」으로 제외되므로 결과가 조용히 한 칸 어긋날 뿐 실행은 성공합니다. 6절이 #REF! 이름 하나를 픽스처에 넣으라고 한 것이 그것을 잡았습니다.

7. 이번에 하지 않는 것

항목이유
셀 메모(xl/comments*.bin)RawCell.Note를 읽는 소비자가 없습니다. 임포터 2개가 채워 넣기만 하고 src/Cooking/ 어디에서도 조회하지 않습니다. 메모를 실제로 사용하게 될 때 함께 처리합니다
2절의 짧은 행 결함이름 사각형 안쪽에 영향이 없고, 시트를 통째로 읽는 레이아웃에 .xlsb가 입력되는 조합이 현재 존재하지 않습니다. 그 조합이 생기면 그때 선행 조건이 됩니다
.xls(BIFF8)레코드 체계가 다른 별개의 형식입니다. 이 문서의 범위 밖입니다

.xlsb에서 메모를 읽지 않는다는 사실은 표시합니다. 지금처럼 조용히 0개가 되는 상태를 유지하지 않습니다 — 이진 워크북에 메모 파트가 있으면 임포터가 경고 한 줄을 냅니다.

8. 이 작업으로 제거되는 것

무엇
변환 산출물 폴더 — 103 MB의 파생 산출물
그 도구 폴더 — 디렉터리 전체. 안에 convert-xlsb.ps1 하나뿐이고, 그 존재 이유가 곧 변환 단계입니다
.gitignore의 해당 한 줄
recipe 3개의 Path가 원본 워크북을 지시하게 됩니다
recipe 주석의 경로 중복 주의사항

제거 시점은 구현과 같습니다. 변환 산출물은 저장소에 등재되어 있지 않으므로, 스크립트를 먼저 삭제하면 그 사이에는 해당 recipe들을 실행할 방법이 없습니다.

함께 정정해야 하는 서술이 남습니다. 전부 「NPOI가 .xlsb를 열지 못한다」를 근거로 적혀 있고, 그 근거는 두 번 낡았습니다워크북 읽기의 9절이 이미 XSSFBReader로 열린다고 정정하였고, 그 뒤 리더 자체가 교체되어 NPOI가 읽기 경로에 없습니다.

위치현재 서술
사전 변환 스크립트 머리말파일과 함께 제거됩니다
그 recipe의 주석.xlsb 원본을 제외하는 이유로 NPOI를 지시합니다
이전 대규모 코퍼스의 레이아웃 조사 문서변환을 선택한 경위. 그 문서는 코퍼스와 함께 저장소에서 빠졌습니다
워크북 읽기의 9절 「문서 정정」·「.xlsb 직독」이 작업으로 두 항목이 함께 종료됩니다

그리고 .xlsb를 읽는다는 서술이 실제와 일치하게 됩니다. 지금은 셀만 읽고 이름은 읽지 못하는 상태이며, 그 차이가 오류 없이 빈 결과로 나타납니다.