En gyldig xlsx trenger ikke inneholde xl/worksheets/sheet1.xml. HotXLS, den native Excel-regnearkkomponenten for Delphi og C++Builder, lokaliserer hver del gjennom OPC-relasjonsgrafen fremfor å gjette navn, fordi ISO/IEC 29500-2 bare garanterer at deler er nåbare fra _rels/.rels, aldri at de sitter ved konvensjonelle stier
Hvorfor feiler parseren min på en gyldig xlsx?
Fordi delnavnene du har memorert er én produsents konvensjon, ikke et krav i formatet. Hver sti du noensinne har hardkodet, xl/workbook.xml, xl/sharedStrings.xml, xl/styles.xml, xl/worksheets/sheetN.xml, er det desktop-Excel-skriveren tilfeldigvis utsteder. En konform pakke kan plassere arbeidsboken ved office/book.xml og det første regnearket ved xl/custom/data-sheet.xml og fortsatt være lovlig SpreadsheetML, så lenge relasjonene peker dit. Dette er den vanligste enkeltårsaken til at en hjemmesnekret leser rapporterer «finner ikke sheet1.xml» på en fil som Excel, LibreOffice og Numbers alle åpner uten klage
Produsenter som gjør dette er ikke eksotiske. Server-side rapportgeneratorer gjenbruker en malpakke og beholder dens originale oppsett. Eksportpipeliner som slår sammen to arbeidsbøker renummererer ark og etterlater hull, så en femarks-arbeidsbok har sheet1, sheet2, sheet4, sheet7, og sheet9. Verktøy som fjerner et ark renummererer ikke alltid de gjenværende. I hvert av disse tilfellene leser det indeksbaserte gjettet xl/worksheets/sheet + IntToStr(i + 1) + .xml stille feil ark eller leser ingenting, som er verre enn et unntak fordi arbeidsboken laster og tallene er feil. Den minimale pakken nedenfor øver på hele problemet, og det er formen HotXLS regresjonstester mot
<!-- _rels/.rels -->
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1"
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
Target="office/book.xml"/>
</Relationships>
<!-- office/_rels/book.xml.rels -->
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId42"
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
Target="../xl/custom/data-sheet.xml"/>
</Relationships>
<!-- xl/custom/_rels/data-sheet.xml.rels -->
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="note7"
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
Target="../notes/review.xml"/>
</Relationships>
Hva garanterer ISO/IEC 29500-2 egentlig?
Den garanterer nåbarhet, ikke plassering. ISO/IEC 29500-2 er Open Packaging Conventions-delen av standarden, og relasjonsklausulen dens definerer nøyaktig ett fast inngangspunkt: pakke-relasjonsdelen ved _rels/.rels. Derfra følger du relasjonen hvis Type er http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument for å nå arbeidsbokdelen, og hver annen del oppdages ved å lese den delens egen relasjonsdel og følge typede kanter utover
To regler til fra samme standard gjør det egentlige arbeidet. Delnavngivningsklausulen fester hvor en relasjonsdel bor: for en del ved <folder>/<name> er relasjonene dens ved <folder>/_rels/<name>.rels, og for en del ved pakkeroten er mappen ganske enkelt _rels/. Relasjonsmarkup-klausulen fastslår at Target er en URI-referanse løst opp mot kildedelens URI, i den vanlige RFC 3986-forstanden, med mindre TargetMode="External" markerer den som pekende utenfor pakken. Kilde-relativ oppløsning er steget alle hopper over, og det er derfor samme literale ../notes/review.xml betyr én ting inne i xl/custom/_rels/data-sheet.xml.rels og noe helt annet inne i en rels-fil én mappe dypere. En siste finurlighet sitter mellom den logiske modellen og bytene på disk: delnavn i den logiske modellen er absolutte og begynner med en skråstrek forover, men ZIP-den-fysiske-mapping-klausulen stripper den skråstreken når den gjør et delnavn om til et ZIP-elementnavn, så en resolver som glemmer det slår opp /xl/sharedStrings.xml i arkivet og finner ingenting
Inne i XlsxResolveRelationshipTarget
HotXLS konsentrerer hele oppløsningsregelen i én funksjon, XlsxResolveRelationshipTarget, deklarert i lxHandleX.pas som function XlsxResolveRelationshipTarget(const OwnerPartName, Target: WideString): WideString. Den tar ZIP-elementnavnet til kildedelen og det rå Target-attributtet, og returnerer et ZIP-elementnavn uten ledende skråstrek, klart til å gis rett til arkivet. Å sende en tom OwnerPartName løses opp mot pakkeroten, som er akkurat det pakke-relasjonsdelen trenger. Rekkefølgen på operasjonene betyr mer enn de individuelle stegene: bakoverstreker normaliseres til skråstreker forover først, fordi noen produsenter skriver Windows-separatorer inn i Target; ethvert fragment introdusert av # kuttes før stihåndtering, så ../charts/chart1.xml#Sheet1 løses opp til et delnavn fremfor til en ikke-eksisterende arkivoppføring; først da deler funksjonen absolutt fra relativt
// Normalization core, as implemented in lxHandleX.pas.
combined := StringReplace(Target, '\', '/', [rfReplaceAll]);
p := Pos('#', combined);
if p > 0 then
combined := Copy(combined, 1, p - 1);
if (combined <> '') and (combined[1] = '/') then
Delete(combined, 1, 1) // package-absolute: strip the slash only
else
begin
p := LastDelimiter('/', String(OwnerPartName));
if p > 0 then
baseName := Copy(OwnerPartName, 1, p)
else
baseName := '';
combined := baseName + combined; // relative to the source part folder
end;
source.StrictDelimiter := True; // '/' only, no quote or space handling
source.Delimiter := '/';
source.DelimitedText := String(combined);
for i := 0 to source.Count - 1 do
begin
segment := WideString(source[i]);
if (segment = '') or (segment = '.') then
Continue; // empty and dot segments vanish
if segment = '..' then
begin
if parts.Count > 0 then
parts.Delete(parts.Count - 1); // pop, and never below the root
end
else
parts.Add(String(segment));
end;
Segment-løkken er en enkel stakk-gjennomgang: tomme segmenter og . droppes, .. popper ett nivå, og en .. som ville rømme pakkeroten absorberes fremfor å produsere en negativ indeks eller et navn som begynner med ../. StrictDelimiter := True-tilordningen er ikke kosmetisk. Uten den behandler en Delphi TStringList mellomrom som skilletegn og respekterer anførselstegn, som ødelegger ethvert delnavn som inneholder et mellomrom, og delnavn med mellomrom er lovlige
Å følge grafen: arbeidsbok, regneark, tegning
HotXLS går gjennom tre lag av relasjonsdeler på TXLSXWorkbook.Open-veien. Pakkelaget håndteres av XlsxFindOfficeDocumentPart, som leser _rels/.rels og returnerer officeDocument-målet. Arbeidsboklaget leser arbeidsbokens relasjonsdel og bygger to kart på én gang: et identifikatorkart for r:id-oppslag og et typekart for enslige deler. Regneark- og tegningslagene gjentar mønsteret med ParseWorksheetRelsXml og ParseDrawingRelsXml, hver sender sitt eget delnavn som oppløsningsbasen slik at en tegning som refererer ../media/image3.png lander på riktig blob
// Tier 1: the only fixed name in the whole format.
WorkbookPartName := XlsxFindOfficeDocumentPart(zip);
if WorkbookPartName = '' then
WorkbookPartName := 'xl/workbook.xml'; // legacy fallback
if not zip.Exists(WorkbookPartName) then
Exit;
// Tier 2: <folder>/_rels/<name>.rels for the workbook part itself.
relsName := XlsxRelationshipPartName(WorkbookPartName);
if zip.Exists(relsName) then
begin
relsStream := zip.OpenFile(relsName);
try
ParsePartRelationshipsXml(relsStream, WorkbookPartName,
WorkbookTargetById, WorkbookTargetsByType);
finally
relsStream.Free;
end;
end;
// Typed singletons resolve by relationship type URI.
PartName := WorkbookTargetsByType.Values[XlsxRtSharedStrings];
if PartName = '' then
PartName := 'xl/sharedStrings.xml';
Ark spesielt må gå gjennom identifikatorkartet, ikke typekartet. <sheet>-elementene i arbeidsbokdelen bærer r:id-attributter, og den identifikatoren er det eneste som binder et arknavn til en del. HotXLS samler inn de identifikatorene under ParseWorkbookXml og løser opp hver mot arbeidsbokens relasjonskart, med fallback til det konvensjonelle nummererte navnet bare når identifikatoren mangler eller ikke kan løses opp
// Tier 2b: r:id -> worksheet part, per sheet, in workbook order.
PartName := '';
if (i < SheetRelIds.Count) and (SheetRelIds[i] <> '') then
PartName := WorkbookTargetById.Values[SheetRelIds[i]];
if PartName = '' then
PartName := 'xl/worksheets/sheet' + IntToStr(i + 1) + '.xml';
SheetPartNames.Add(String(PartName));
// Tier 3: each worksheet resolves its own satellites against its own name.
relsName := XlsxRelationshipPartName(PartName);
if zip.Exists(relsName) then
begin
relsStream := zip.OpenFile(relsName);
try
ParseWorksheetRelsXml(relsStream, PartName,
FParRels[i], ParTableTargets[i], ParPartTargets[i]);
finally
relsStream.Free;
end;
end;
Alt nedstrøms rir på den samme mekanismen. Delte strenger, stiler, tema, VBA-prosjektet under den Microsoft-navnrommet typen http://schemas.microsoft.com/office/2006/relationships/vbaProject, eksterne lenker, den arbeidsbok-omfattede person-delen, eldre kommentarer, tråde-kommentarer, VML-tegningen som bærer kommentarballong-geometri, tegninger, bilder, diagrammer, tabeller, og pivottabeller når alle bytene sine gjennom oppløste mål. Tema-delen spesielt må lokaliseres korrekt, ellers overskriver en rundtur stille en kundes merkevarepalett med standard Office-temaet, en av feilmodusene dekket i notatene om tapsfri XLSX-rundtur av tema, extLst og calcChain. Relasjonslesing er også hvorfor lastingen er iscenesatt slik den er: all arkivtilgang skjer på én tråd før regneark-XML parses, fordi inflate-tilstanden til et ZIP-arkiv ikke er trådsikker, en begrensning forklart i gjennomgangen av parallell XLSX-parsing og minneallokatoren
Hvorfor ødelegger en duplikat rId typebasert ruting?
Fordi en senere feilformet oppføring kan overskrive en tidligere gyldig en og kapre oppslaget. Relasjonsidentifikatorer skal være unike innenfor en relasjonsdel, men feilformede pakker gjenbruker dem, og en naiv Values[Id] :=-tilordning er siste-skriving-vinner. Hvis rId3 først peker på et ekte regneark og en andre rId3 peker på et ikke-støttet eller tomt mål, taper siste-skriving-vinner regnearket. ParsePartRelationshipsXml anvender derfor en først-vinner-regel med to betingelser: det oppløste målet må være ikke-tomt, og identifikatoren må ikke allerede være til stede. Begge betingelser sammen er det som gjør det trygt, fordi ikke-tom-testen hindrer en relasjon med et manglende Target fra å kreve plassen før en brukbar en ankommer
if (TargetById <> nil) and (Id <> '') and (resolvedTarget <> '') and
(TargetById.IndexOfName(String(Id)) < 0) then
TargetById.Values[String(Id)] := String(resolvedTarget);
if (TargetsByType <> nil) and (relType <> '') and (resolvedTarget <> '') then
TargetsByType.Add(String(relType + '=' + resolvedTarget));
Legg merke til den bevisste asymmetrien i det utdraget. Identifikatorkartet er et ekte kart med en først-vinner-vakt, mens typesamlingen er en bare-legg-til-liste av type=target-par. Det skillet er bærende: en arbeidsbok har nøyaktig én delte-strenger-relasjon men mange regneark- og ekstern-lenke-relasjoner, så typeoppslag via Values[] returnerer det første treffet for enslige, og fler-verdi-typer som externalLink listes opp ved å gå gjennom listen
Hvor relasjonsfølging stopper
Ærlige grenser betyr mer enn en ren historie. HotXLS faller tilbake til konvensjonelle navn når en relasjon mangler, så en pakke med en skadet eller manglende relasjonsdel åpner fortsatt hvis den tilfeldigvis følger Excel-oppsettet; den fallbacken er en kompatibilitetsfunksjon, ikke en andre sannhetskilde, og den kan skjule en produsentfeil under testing. Tre grenser til er verdt å kjenne til. Mål merket TargetMode="External" lagres ordrett fremfor å løses opp, som er korrekt for hyperlenker og for externalLinkPath-relasjonen som bærer en ekstern arbeidsbok-URL, men det betyr at verdien du får tilbake er hva produsenten enn skrev. Diagramdeler oppdaget gjennom en tegning-relasjonsdel pares med tegning-forankringer posisjonelt fremfor etter identifikator, så en uvanlig forankringsrekkefølge kan feiljustere diagram-bindinger. Og den strømmende direkte leseren i lxDirectRead.pas holder sin egen lettere sti-håndtering nøklet til xl/, så den fulle resolveren beskrevet her styrer TXLSXWorkbook.Open- og GetSheetNames-inngangspunktene, ikke den lav-allokerings skanningsveien dokumentert i artikkelen om den strømmende direkte leseren for Delphi
Hvis du bygger dette selv, er det korteste korrekte sammendraget: konstruer aldri et delnavn, løs alltid opp ett. Les _rels/.rels, følg officeDocument, løs opp hvert Target mot delen som deklarerte det, og rut ark etter r:id. Hvis du heller vil ha det allerede testet mot omdøpte deler, ikke-sammenhengende arknummerering, og duplikate relasjonsidentifikatorer, leveres resolveren beskrevet her i HotXLS Delphi regnearkkomponent, sammen med rundtur-mekanismen som holder delene den ikke parser intakte