Műszaki cikk

XLSX OPC kapcsolat-feloldás Delphi elemzőkben

Egy érvényes xlsx-nek nem kell tartalmaznia xl/worksheets/sheet1.xml-t. A HotXLS, a natív Excel táblázatkomponens Delphihez és C++Builderhez, az OPC kapcsolati grafon keresztül keres meg minden részt nevek kitalálása helyett, mert az ISO/IEC 29500-2 csak azt garantálja, hogy a részek elérhetők a _rels/.rels-ből, sosem azt, hogy szokásos útvonalakon ülnek

Miért bukik el az elemzőm egy érvényes xlsx-en?

Mert a betanult részneveid egy előállító konvenciója, nem a formátum követelménye. Minden útvonal, amit valaha hardkódoltál, xl/workbook.xml, xl/sharedStrings.xml, xl/styles.xml, xl/worksheets/sheetN.xml, az, amit az asztali Excel-író véletlenül kibocsát. Egy megfelelő csomag elhelyezheti a munkafüzetet a office/book.xml-en, és az első munkalapot a xl/custom/data-sheet.xml-en, és még mindig legális SpreadsheetML, amíg a kapcsolatok oda mutatnak. Ez a leggyakoribb oka annak, hogy egy házon belül épített olvasó azt jelenti, "sheet1.xml nem található" egy olyan fájlon, amelyet az Excel, a LibreOffice és a Numbers panasz nélkül megnyit

Az ezt tevő előállítók nem egzotikusak. A szerveroldali jelentésgenerátorok újrahasznosítanak egy sablon-csomagot, és megtartják annak eredeti elrendezését. Az export pipeline-ok, amelyek két munkafüzetet egyesítenek, átszámozzák a munkalapokat, és réseket hagynak, így egy ötlapos munkafüzetnek van sheet1, sheet2, sheet4, sheet7 és sheet9-je. Az eszközök, amelyek eltávolítanak egy munkalapot, nem mindig számozzák át a túlélőket. Minden ilyen esetben az index-alapú találgatás xl/worksheets/sheet + IntToStr(i + 1) + .xml csendben rossz munkalapot olvas, vagy semmit sem olvas, ami rosszabb, mint egy kivétel, mert a munkafüzet betöltődik, és a számok rosszak. Az alábbi minimális csomag gyakorolja be az egész problémát, és ez az alak, amellyel a HotXLS regressziósan tesztel

<!-- _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>

Mit garantál valójában az ISO/IEC 29500-2?

Elérhetőséget garantál, nem elhelyezkedést. Az ISO/IEC 29500-2 a szabvány Open Packaging Conventions része, és a kapcsolati fejezete pontosan egy rögzített belépési pontot definiál: a csomag-kapcsolati részt a _rels/.rels-nél. Onnan követed azt a kapcsolatot, amelynek Type-ja http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument, hogy elérd a munkafüzet-részt, és minden más részt annak saját kapcsolati részének olvasásával fedezel fel, tipizált éleket kifelé követve

Két további szabály ugyanattól a szabványtól végzi a valódi munkát. A rész-elnevezési fejezet rögzíti, hol él egy kapcsolati rész: egy <folder>/<name> résznél a kapcsolatai a <folder>/_rels/<name>.rels-nél vannak, és egy csomag-gyökérnél lévő résznél a mappa egyszerűen _rels/. A kapcsolati jelölésfejezet kimondja, hogy a Target egy URI hivatkozás, amely a forrás rész URI-jához van feloldva, a szokásos RFC 3986 értelemben, hacsak a TargetMode="External" nem jelöli meg csomagon kívülre mutatóként. A forrás-relatív feloldás az a lépés, amit mindenki kihagy, és ez az oka, hogy ugyanaz a szó szerinti ../notes/review.xml egyet jelent az xl/custom/_rels/data-sheet.xml.rels-en belül, és teljesen mást egy mappával mélyebben lévő rels fájlon belül. Egy utolsó ránc a logikai modell és a lemezen lévő bájtok között: a rész-nevek a logikai modellben abszolútak, és perjellel kezdődnek, de a ZIP fizikai leképezés fejezet eltávolítja azt a perjelet, amikor egy rész-nevet ZIP-elem-névvé alakít, így egy feloldó, amely ezt elfelejti, a /xl/sharedStrings.xml-t keresi az archívumban, és nem talál semmit

Az XlsxResolveRelationshipTarget belsejében

A HotXLS az egész feloldási szabályt egyetlen függvényben koncentrálja, a XlsxResolveRelationshipTarget-ben, amelyet a lxHandleX.pas-ban function XlsxResolveRelationshipTarget(const OwnerPartName, Target: WideString): WideString-ként deklaráltak. A forrás rész ZIP-elem-nevét és a nyers Target attribútumot veszi át, és egy ZIP-elem-nevet ad vissza vezető perjel nélkül, közvetlenül átadásra készen az archívumnak. Egy üres OwnerPartName átadása a csomag-gyökér ellen oldja fel, ami pontosan az, amire a csomag-kapcsolati résznek szüksége van. A műveletek sorrendje jobban számít, mint az egyes lépések. A visszaperjeleket normalizálják előre-perjelekre először, mert egyes előállítók Windows elhatárolókat írnak a Target-be; bármely fragment, amelyet a # vezet be, levágásra kerül az útvonal-kezelés előtt, így a ../charts/chart1.xml#Sheet1 egy részes névre oldódik, nem egy nem létező archívum-bejegyzésre; csak ezután választja szét a függvény az abszolútat a relatívtól

// 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;

A szegmens-ciklus egyszerű verem-bejárás: az üres szegmensek és a . kiesnek, a .. egy szintet felugrik (pop), és egy .., amely elszökne a csomaggyökérből, elnyelődik ahelyett, hogy negatív indexet vagy ../-vel kezdődő nevet produkálna. A StrictDelimiter := True hozzárendelés nem kozmetikai. Nélküle egy Delphi TStringList szóközöket kezel elhatárolóként, és tiszteletben tartja az idézőjel-karaktereket, ami tönkreteszi bármely szóközt tartalmazó rész-nevet, és a szóközt tartalmazó rész-nevek legálisak

A gráf követése: munkafüzet, munkalap, rajzolat

A HotXLS három réteg kapcsolati részt jár be a TXLSXWorkbook.Open útvonalon. A csomag réteget a XlsxFindOfficeDocumentPart kezeli, amely olvassa a _rels/.rels-t, és visszaadja az officeDocument célt. A munkafüzet réteg olvassa a munkafüzet kapcsolati részt, és egyszerre épít két térképet: egy azonosító-térképet az r:id keresésekhez, és egy típus-térképet a szinguláris részekhez. A munkalap- és rajzolat-rétegek megismétlik a mintát a ParseWorksheetRelsXml-lel és a ParseDrawingRelsXml-lel, mindegyik átadva a saját rész-nevét feloldási alapként, így egy rajzolat, amely a ../media/image3.png-re hivatkozik, a helyes blobon landol

// 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';

A munkalapoknak konkrétan az azonosító-térképen keresztül kell menniük, nem a típus-térképen. A munkafüzet rész <sheet> elemei r:id attribútumokat hordoznak, és ez az azonosító az egyetlen dolog, amely egy munkalap nevét egy részhez köti. A HotXLS ezeket az azonosítókat a ParseWorkbookXml során gyűjti, és mindegyiket a munkafüzet kapcsolati térképe ellen oldja fel, csak akkor esve vissza a hagyományos számozott névre, ha az azonosító hiányzik vagy feloldhatatlan

// 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;

Minden downstream ugyanerre a mechanizmusra lovagol. A megosztott sztringek, stílusok, téma, a VBA projekt a Microsoft-névtéres http://schemas.microsoft.com/office/2006/relationships/vbaProject típus alatt, külső hivatkozások, a munkafüzet-szintű person rész, örökölt megjegyzések, szálas megjegyzések, a megjegyzés-buborék geometriáját hordozó VML rajzolat, rajzolatok, képek, diagramok, táblák és PivotTable-ök mind a feloldott célpontokon keresztül érik el a bájtjaikat. A témarészt konkrétan helyesen kell megtalálni, különben egy round-trip csendben felülírja az ügyfél márkapalettáját a gyári Office témával, ami az egyik hibamódja, amelyet a a téma, extLst és calcChain veszteségmentes XLSX round-trip-jéről szóló jegyzetek tárgyalnak. A kapcsolat-olvasás az is, amiért a betöltés úgy van szakaszolva, ahogy: minden archívumhozzáférés egy szálon történik, mielőtt a munkalap XML elemzésre kerülne, mert egy ZIP archívum inflate-állapota nem szálbiztos, egy korlátozás, amelyet a a párhuzamos XLSX-elemzésről és a memóriaallokátorról szóló írás megmagyaráz

Miért töri el egy duplikált rId a típus-alapú irányítást?

Mert egy későbbi, hibás bejegyzés felülírhat egy korábbi érvényeset, és eltérítheti a keresést. A kapcsolati azonosítóknak egyedinek kellene lenniük egy kapcsolati részen belül, de a hibás csomagok újrahasznosítják őket, és egy naiv Values[Id] := hozzárendelés utolsó-győz. Ha az rId3 először egy valódi munkalapra mutat, és egy második rId3 egy nem-támogatott vagy üres célra mutat, az utolsó-győz elveszti a munkalapot. A ParsePartRelationshipsXml ezért egy első-győz szabályt alkalmaz két feltétellel: a feloldott célnak nem-üresnek kell lennie, és az azonosítónak nem szabad már jelen lennie. A két feltétel együtt teszi biztonságossá, mert a nem-üres teszt megakadályozza, hogy egy hiányzó Target-tel rendelkező kapcsolat elfoglalja a helyet, mielőtt egy használható megérkezne

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));

Figyeld meg a szándékos aszimmetriát ebben a részletben. Az azonosító-térkép egy valódi térkép egy első-győz őrrel, míg a típus-gyűjtemény egy csak-hozzáfűzős lista type=target párokból. Ez a megkülönböztetés fontos: egy munkafüzetnek pontosan egy megosztott-sztring kapcsolata van, de sok munkalap- és külső-hivatkozás kapcsolata, így a típus-keresés a Values[]-en keresztül az első egyezést adja vissza a szinguláris típusokhoz, és a többértékű típusokat, mint az externalLink, a lista bejárásával sorolják fel

Ahol a kapcsolatkövetés megáll

Az őszinte határok fontosabbak, mint egy tiszta sztori. A HotXLS visszaesik a hagyományos nevekre, valahányszor egy kapcsolat hiányzik, így egy sérült vagy hiányzó kapcsolati résszel rendelkező csomag még mindig megnyílik, ha véletlenül követi az Excel-elrendezést; ez a visszaesés kompatibilitási funkció, nem egy második igazságforrás, és elrejthet egy előállítói hibát tesztelés közben. Három további korlátot érdemes ismerni. A TargetMode="External"-lel jelölt célok szó szerint kerülnek tárolásra feloldás helyett, ami helyes a hiperhivatkozásoknál és az externalLinkPath kapcsolatnál, amely egy távoli munkafüzet URL-t hordoz, de azt jelenti, hogy a visszakapott érték az, amit az előállító írt. A rajzolati kapcsolati részen keresztül felfedezett diagramrészek pozicionálisan, nem azonosító alapján párosulnak a rajzolat-horgonyokkal, így egy szokatlan horgony-sorrend félreigazíthatja a diagram-kötéseket. És a streamelő közvetlen olvasó a lxDirectRead.pas-ban saját, könnyebb útvonal-kezelést tart xl/-hez kulcsolva, így az itt leírt teljes feloldó a TXLSXWorkbook.Open és GetSheetNames belépési pontokat irányítja, nem az alacsony-allokációs pásztázó útvonalat, amelyet a a streamelő közvetlen olvasóról Delphiben szóló cikk dokumentál

Ha ezt magad építed, a legrövidebb helyes összefoglaló: sosem konstruálj egy rész-nevet, mindig oldj fel egyet. Olvasd a _rels/.rels-t, kövesd az officeDocument-et, oldj fel minden Target-et a rész ellen, amely deklarálta, és irányítsd a munkalapokat r:id szerint. Ha inkább már tesztelten szeretnéd ezt, átnevezett részek, nem-folytonos munkalap-számozás és duplikált kapcsolati azonosítók ellen, az itt leírt feloldó a HotXLS Delphi táblázatkomponensben érkezik, a round-trip gépezet mellett, amely érintetlenül tartja azokat a részeket, amelyeket nem elemez