Geçerli bir xlsx'in xl/worksheets/sheet1.xml içermesi zorunlu değildir. Delphi ve C++Builder için yerel Excel elektronik tablo bileşeni olan HotXLS, isimleri tahmin etmek yerine her parçayı OPC ilişki grafiği üzerinden bulur, çünkü ISO/IEC 29500-2 yalnızca parçaların _rels/.rels'ten erişilebilir olduğunu garanti eder, geleneksel yollarda oturduklarını hiçbir zaman değil
Ayrıştırıcım neden geçerli bir xlsx'te başarısız oluyor?
Çünkü ezberlediğiniz parça isimleri biçimin bir gereksinimi değil, bir üreticinin kuralıdır. Şimdiye kadar sabit kodladığınız her yol — xl/workbook.xml, xl/sharedStrings.xml, xl/styles.xml, xl/worksheets/sheetN.xml — masaüstü Excel yazıcısının tesadüfen yayınladığı şeydir. Uyumlu bir paket, çalışma kitabını office/book.xml'e ve ilk çalışma sayfasını xl/custom/data-sheet.xml'e koyabilir ve ilişkiler oraya işaret ettiği sürece hâlâ yasal SpreadsheetML olabilir. Bu, ev yapımı bir okuyucunun, Excel'in, LibreOffice'in ve Numbers'ın hiçbir şikayet olmadan açtığı bir dosyada "sheet1.xml bulunamadı" bildirmesinin en yaygın tek nedenidir
Bunu yapan üreticiler egzotik değildir. Sunucu tarafı rapor üreteçleri bir şablon paketini yeniden kullanır ve orijinal düzenini korur. İki çalışma kitabını birleştiren dışa aktarma pipeline'ları sayfaları yeniden numaralandırır ve boşluklar bırakır, bu yüzden beş-sayfalı bir çalışma kitabında sheet1, sheet2, sheet4, sheet7 ve sheet9 bulunur. Bir sayfayı çıkaran araçlar hayatta kalanları her zaman yeniden numaralandırmaz. Bu durumların her birinde, indeks-tabanlı tahmin xl/worksheets/sheet + IntToStr(i + 1) + .xml, sessizce yanlış sayfayı okur ya da hiçbir şey okumaz; ki bu bir exception'dan daha kötüdür, çünkü çalışma kitabı yüklenir ve sayılar yanlıştır. Aşağıdaki minimal paket tüm sorunu sergiler ve HotXLS'in regresyon testi yaptığı şekildir
<!-- _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>
ISO/IEC 29500-2 gerçekte neyi garanti eder?
Erişilebilirliği garanti eder, konumu değil. ISO/IEC 29500-2, standardın Open Packaging Conventions bölümüdür ve ilişkiler maddesi tam olarak bir sabit giriş noktası tanımlar: _rels/.rels'teki paket ilişki parçası. Oradan, çalışma kitabı parçasına ulaşmak için Type'ı http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument olan ilişkiyi takip edersiniz ve her diğer parça, o parçanın kendi ilişki parçasını okuyarak ve tipli kenarları dışarıya doğru takip ederek keşfedilir
Aynı standarttan iki kural daha gerçek işi yapar. Parça-adlandırma maddesi, bir ilişki parçasının nerede yaşadığını sabitler: <folder>/<name>'deki bir parça için ilişkileri <folder>/_rels/<name>.rels'tedir ve paket kökündeki bir parça için klasör basitçe _rels/'dir. İlişki işaretleme maddesi, Target'ın, TargetMode="External" onu paketin dışına işaret ettiğini işaretlemedikçe, olağan RFC 3986 anlamında kaynak parçanın URI'sine karşı çözülen bir URI referansı olduğunu belirtir. Kaynağa-göreli çözümleme, herkesin atladığı adımdır ve aynı literal ../notes/review.xml'in xl/custom/_rels/data-sheet.xml.rels içinde bir şey ve bir klasör daha derindeki bir rels dosyasının içinde tamamen başka bir şey anlamına gelmesinin nedeni budur. Mantıksal model ile diskteki baytlar arasında son bir kırışıklık daha vardır: mantıksal modeldeki parça isimleri mutlaktır ve bir eğik çizgiyle başlar, ama ZIP fiziksel eşleme maddesi, bir parça ismini bir ZIP öğe ismine dönüştürürken o eğik çizgiyi kaldırır, bu yüzden bunu unutan bir çözücü arşivde /xl/sharedStrings.xml'i arar ve hiçbir şey bulamaz
XlsxResolveRelationshipTarget'ın içinde
HotXLS, tüm çözümleme kuralını lxHandleX.pas'ta function XlsxResolveRelationshipTarget(const OwnerPartName, Target: WideString): WideString olarak bildirilen tek bir fonksiyonda, XlsxResolveRelationshipTarget'ta yoğunlaştırır. Kaynak parçanın ZIP öğe ismini ve ham Target özniteliğini alır ve arşive doğrudan verilmeye hazır, başında eğik çizgi olmayan bir ZIP öğe ismi döndürür. Boş bir OwnerPartName geçirmek paket köküne karşı çözülür, ki bu tam olarak paket ilişki parçasının ihtiyaç duyduğu şeydir. İşlemlerin sırası, tek tek adımlardan daha önemlidir: ters eğik çizgiler önce ileri eğik çizgilere normalleştirilir, çünkü bazı üreticiler Target'a Windows ayırıcıları yazar; # tarafından tanıtılan herhangi bir parça, yol işlemesinden önce kesilir, bu yüzden ../charts/chart1.xml#Sheet1 var olmayan bir arşiv girdisine değil bir parça ismine çözülür; ancak o zaman fonksiyon mutlak olanı göreli olandan ayırır
// 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 döngüsü düz bir yığın gezintisidir: boş segmentler ve . düşürülür, .. bir seviye çıkarır ve paket kökünden kaçacak bir .., negatif bir indeks ya da ../ ile başlayan bir isim üretmek yerine soğurulur. StrictDelimiter := True ataması kozmetik değildir. Onsuz bir Delphi TStringList, boşlukları ayırıcı olarak ele alır ve tırnak karakterlerini onurlandırır, ki bu boşluk içeren herhangi bir parça ismini bozar ve boşluklu parça isimleri yasaldır
Grafiği takip etmek: çalışma kitabı, çalışma sayfası, çizim
HotXLS, TXLSXWorkbook.Open yolunda ilişki parçalarının üç katmanını gezer. Paket katmanı, _rels/.rels'i okuyan ve officeDocument hedefini döndüren XlsxFindOfficeDocumentPart tarafından işlenir. Çalışma kitabı katmanı, çalışma kitabı ilişki parçasını okur ve iki haritayı aynı anda oluşturur: r:id aramaları için bir tanımlayıcı haritası ve tekil parçalar için bir tür haritası. Çalışma sayfası ve çizim katmanları, deseni ParseWorksheetRelsXml ve ParseDrawingRelsXml ile tekrarlar; her biri, ../media/image3.png'ye referans veren bir çizimin doğru blob'a inmesi için kendi parça ismini çözümleme temeli olarak geçirir
// 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';
Sayfalar özellikle tür haritası yerine tanımlayıcı haritasından geçmelidir. Çalışma kitabı parçasındaki <sheet> elemanları r:id öznitelikleri taşır ve o tanımlayıcı, bir sayfa ismini bir parçaya bağlayan tek şeydir. HotXLS, o tanımlayıcıları ParseWorkbookXml sırasında toplar ve her birini çalışma kitabı ilişki haritasına karşı çözer, tanımlayıcı yoksa ya da çözülemezse geleneksel numaralı isme geri döner
// 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;
Aşağı akıştaki her şey aynı mekanizmaya biner. Paylaşılan dizeler, stiller, tema, Microsoft-isim-uzaylı tür http://schemas.microsoft.com/office/2006/relationships/vbaProject altındaki VBA projesi, harici bağlantılar, çalışma-kitabı-kapsamlı kişi parçası, eski yorumlar, threaded yorumlar, yorum balonu geometrisini taşıyan VML çizimi, çizimler, görüntüler, grafikler, tablolar ve PivotTable'lar hepsi çözülmüş hedefler üzerinden baytlarına ulaşır. Tema parçası özellikle doğru bulunmalıdır yoksa bir round-trip, müşteri marka paletini stok Office temasıyla sessizce üzerine yazar; bu, tema, extLst ve calcChain'in kayıpsız XLSX round-trip'i üzerine notlarda ele alınan hata modlarından biridir. İlişki okuma aynı zamanda yüklemenin neden bu şekilde aşamalandığının da nedenidir: tüm arşiv erişimi çalışma sayfası XML'i ayrıştırılmadan önce tek bir iş parçacığında gerçekleşir, çünkü bir ZIP arşivinin şişirme durumu iş-parçacığı-güvenli değildir; bu kısıt, paralel XLSX ayrıştırma ve bellek tahsis edicisi üzerine yazıda açıklanmıştır
Tekrar eden bir rId neden tür-tabanlı yönlendirmeyi bozar?
Çünkü daha sonraki bozuk bir girdi, önceki geçerli bir girdinin üzerine yazabilir ve aramayı ele geçirebilir. İlişki tanımlayıcıları bir ilişki parçası içinde benzersiz olmalıdır, ama bozuk paketler onları yeniden kullanır ve saf bir Values[Id] := ataması son-yazan-kazanır'dır. rId3 önce gerçek bir çalışma sayfasına işaret ediyorsa ve ikinci bir rId3 desteklenmeyen ya da boş bir hedefe işaret ediyorsa, son-yazan-kazanır çalışma sayfasını kaybeder. ParsePartRelationshipsXml bu yüzden iki koşullu bir ilk-kazanır kuralı uygular: çözülen hedef boş olmamalıdır ve tanımlayıcı zaten mevcut olmamalıdır. İki koşul birlikte onu güvenli kılan şeydir, çünkü boş-olmama testi, eksik bir Target'a sahip bir ilişkinin, kullanılabilir biri gelmeden önce yuvayı talep etmesini durdurur
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));
O parçacıktaki kasıtlı asimetriye dikkat edin. Tanımlayıcı haritası, bir ilk-kazanır korumasına sahip gerçek bir haritadır, tür koleksiyonu ise type=target çiftlerinden oluşan yalnızca-ekleme bir listedir. Bu ayrım yük taşır: bir çalışma kitabının tam olarak bir paylaşılan-dizeler ilişkisi vardır ama birçok çalışma-sayfası ve harici-bağlantı ilişkisi vardır, bu yüzden Values[] üzerinden tür araması tekiller için ilk eşleşmeyi döndürür ve externalLink gibi çok-değerli türler, listeyi gezerek numaralandırılır
İlişki takibinin durduğu yer
Dürüst sınırlar, temiz bir hikayeden daha önemlidir. HotXLS, bir ilişki bulunmadığında geleneksel isimlere geri döner, bu yüzden hasarlı ya da eksik bir ilişki parçasına sahip bir paket, tesadüfen Excel düzenini takip ediyorsa hâlâ açılır; o geri dönüş ikinci bir doğruluk kaynağı değil bir uyumluluk özelliğidir ve test sırasında bir üretici hatasını maskeleyebilir. Bilmeye değer üç sınır daha var. TargetMode="External" ile işaretlenmiş hedefler çözülmek yerine olduğu gibi saklanır; ki bu köprüler ve uzak bir çalışma kitabı URL'si taşıyan externalLinkPath ilişkisi için doğrudur, ama geri aldığınız değerin üreticinin yazdığı her neyse o olduğu anlamına gelir. Bir çizim ilişki parçası aracılığıyla keşfedilen grafik parçaları, tanımlayıcıya göre değil konumsal olarak çizim çapalarıyla eşleştirilir, bu yüzden alışılmadık bir çapa sıralaması grafik bağlarını yanlış hizalayabilir. Ve lxDirectRead.pas'taki streaming direct reader, xl/'e anahtarlanmış kendi daha hafif yol işlemesini tutar, bu yüzden burada anlatılan tam çözücü, TXLSXWorkbook.Open ve GetSheetNames giriş noktalarını yönetir, Delphi için streaming direct reader üzerine yazıda belgelenen düşük-tahsisli tarama yolunu değil
Bunu kendiniz inşa ediyorsanız, en kısa doğru özet şudur: asla bir parça ismi inşa etmeyin, her zaman birini çözün. _rels/.rels'i okuyun, officeDocument'i takip edin, her Target'ı onu bildiren parçaya karşı çözün ve sayfaları r:id'ye göre yönlendirin. Bunun yeniden adlandırılmış parçalara, sürekli olmayan sayfa numaralandırmasına ve tekrar eden ilişki tanımlayıcılarına karşı zaten test edilmiş olmasını tercih ederseniz, burada anlatılan çözücü, ayrıştırmadığı parçaları bozulmadan tutan round-trip mekanizmasıyla birlikte HotXLS Delphi elektronik tablo bileşeninde gönderilir