Un xlsx válido no tiene por qué contener xl/worksheets/sheet1.xml. HotXLS, el componente de hoja de cálculo Excel nativo para Delphi y C++Builder, localiza cada parte a través del grafo de relaciones OPC en lugar de adivinar nombres, porque ISO/IEC 29500-2 solo garantiza que las partes sean alcanzables desde _rels/.rels, nunca que se ubiquen en rutas convencionales
Por qué mi analizador falla en un xlsx válido
Porque los nombres de parte que memorizaste son una convención de un solo productor, no un requisito del formato. Cada ruta que alguna vez codificaste, xl/workbook.xml, xl/sharedStrings.xml, xl/styles.xml, xl/worksheets/sheetN.xml, es lo que el escritor de Excel de escritorio da la casualidad de que emite. Un paquete conforme puede poner el libro en office/book.xml y la primera hoja de trabajo en xl/custom/data-sheet.xml y seguir siendo SpreadsheetML legal, siempre que las relaciones apunten ahí. Esta es la razón más común por la que un lector casero reporta "no se puede encontrar sheet1.xml" en un archivo que Excel, LibreOffice, y Numbers abren todos sin quejarse
Los productores que hacen esto no son exóticos. Los generadores de reportes del lado del servidor reutilizan un paquete plantilla y conservan su diseño original. Las canalizaciones de exportación que fusionan dos libros renumeran las hojas y dejan huecos, así que un libro de cinco hojas tiene sheet1, sheet2, sheet4, sheet7, y sheet9. Las herramientas que eliminan una hoja no siempre renumeran a las sobrevivientes. En cada uno de esos casos, la suposición basada en índice xl/worksheets/sheet + IntToStr(i + 1) + .xml lee en silencio la hoja equivocada o no lee nada, lo cual es peor que una excepción porque el libro se carga y los números están mal. El paquete mínimo de abajo ejercita todo el problema, y es la forma contra la que HotXLS ejecuta sus pruebas de regresión
<!-- _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>
Qué garantiza realmente ISO/IEC 29500-2
Garantiza alcanzabilidad, no ubicación. ISO/IEC 29500-2 es la parte de Open Packaging Conventions del estándar, y su cláusula de relaciones define exactamente un punto de entrada fijo: la parte de relación del paquete en _rels/.rels. Desde ahí sigues la relación cuyo Type es http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument para llegar a la parte del libro, y cada otra parte se descubre leyendo la propia parte de relación de esa parte y siguiendo aristas tipadas hacia afuera
Dos reglas más del mismo estándar hacen el trabajo real. La cláusula de nomenclatura de partes fija dónde vive una parte de relación: para una parte en <folder>/<name>, sus relaciones están en <folder>/_rels/<name>.rels, y para una parte en la raíz del paquete la carpeta es simplemente _rels/. La cláusula de marcado de relaciones establece que Target es una referencia URI resuelta contra la URI de la parte de origen, en el sentido ordinario de RFC 3986, a menos que TargetMode="External" la marque como apuntando fuera del paquete. La resolución relativa al origen es el paso que todo el mundo se salta, y es por eso que el mismo literal ../notes/review.xml significa una cosa dentro de xl/custom/_rels/data-sheet.xml.rels y algo completamente distinto dentro de un archivo rels una carpeta más abajo. Un último detalle se encuentra entre el modelo lógico y los bytes en disco: los nombres de parte en el modelo lógico son absolutos y comienzan con una barra diagonal, pero la cláusula de mapeo físico a ZIP elimina esa barra cuando convierte un nombre de parte en un nombre de elemento ZIP, así que un resolutor que lo olvida busca /xl/sharedStrings.xml en el archivo y no encuentra nada
Dentro de XlsxResolveRelationshipTarget
HotXLS concentra toda la regla de resolución en una sola función, XlsxResolveRelationshipTarget, declarada en lxHandleX.pas como function XlsxResolveRelationshipTarget(const OwnerPartName, Target: WideString): WideString. Recibe el nombre de elemento ZIP de la parte de origen y el atributo Target crudo, y devuelve un nombre de elemento ZIP sin barra diagonal inicial, listo para entregarse directamente al archivo. Pasar un OwnerPartName vacío resuelve contra la raíz del paquete, que es exactamente lo que necesita la parte de relación del paquete. El orden de las operaciones importa más que los pasos individuales: las barras invertidas se normalizan primero a barras diagonales, porque algunos productores escriben separadores de Windows en Target; cualquier fragmento introducido por # se corta antes del manejo de ruta, así que ../charts/chart1.xml#Sheet1 resuelve hacia un nombre de parte en lugar de hacia una entrada de archivo inexistente; solo entonces la función separa lo absoluto de lo relativo
// 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;
El bucle de segmentos es un recorrido de pila simple: los segmentos vacíos y . se descartan, .. saca un nivel, y un .. que escaparía de la raíz del paquete se absorbe en lugar de producir un índice negativo o un nombre que comience con ../. La asignación StrictDelimiter := True no es cosmética. Sin ella, un TStringList de Delphi trata los espacios como delimitadores y respeta los caracteres de comilla, lo que estropea cualquier nombre de parte que contenga un espacio, y los nombres de parte con espacios son legales
Siguiendo el grafo: libro, hoja de trabajo, dibujo
HotXLS recorre tres niveles de partes de relación en la ruta de TXLSXWorkbook.Open. El nivel de paquete lo maneja XlsxFindOfficeDocumentPart, que lee _rels/.rels y devuelve el destino officeDocument. El nivel de libro lee la parte de relación del libro y construye dos mapas a la vez: un mapa de identificadores para búsquedas de r:id y un mapa de tipos para partes únicas. Los niveles de hoja de trabajo y dibujo repiten el patrón con ParseWorksheetRelsXml y ParseDrawingRelsXml, cada uno pasando su propio nombre de parte como base de resolución para que un dibujo que referencia ../media/image3.png aterrice en el blob correcto
// 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';
Las hojas específicamente deben pasar por el mapa de identificadores, no por el mapa de tipos. Los elementos <sheet> en la parte del libro llevan atributos r:id, y ese identificador es lo único que vincula un nombre de hoja con una parte. HotXLS recolecta esos identificadores durante ParseWorkbookXml y resuelve cada uno contra el mapa de relaciones del libro, recayendo en el nombre numerado convencional solo cuando el identificador está ausente o no se puede resolver
// 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;
Todo lo que sigue depende de ese mismo mecanismo. Las cadenas compartidas, los estilos, el tema, el proyecto VBA bajo el tipo con espacio de nombres de Microsoft http://schemas.microsoft.com/office/2006/relationships/vbaProject, los enlaces externos, la parte de persona a nivel de libro, los comentarios heredados, los comentarios con hilos, el dibujo VML que lleva la geometría del globo de comentario, los dibujos, las imágenes, los gráficos, las tablas, y las PivotTables, todos alcanzan sus bytes a través de destinos resueltos. La parte de tema en particular tiene que localizarse correctamente o un ciclo de ida y vuelta sobrescribe en silencio la paleta de marca de un cliente con el tema estándar de Office, uno de los modos de fallo cubiertos en las notas sobre el ciclo de ida y vuelta sin pérdidas de XLSX para tema, extLst, y calcChain. La lectura de relaciones también es por qué la carga se organiza por etapas de la manera en que lo está: todo el acceso al archivo ocurre en un solo hilo antes de que se analice el XML de la hoja de trabajo, porque el estado de inflado de un archivo ZIP no es seguro para hilos, una restricción explicada en el análisis sobre el análisis paralelo de XLSX y el asignador de memoria
Por qué un rId duplicado rompe el enrutamiento basado en tipo
Porque una entrada malformada posterior puede sobrescribir a una anterior válida y secuestrar la búsqueda. Se supone que los identificadores de relación son únicos dentro de una parte de relación, pero los paquetes malformados los reutilizan, y una asignación ingenua Values[Id] := hace que gane la última escritura. Si un primer rId3 apunta a una hoja de trabajo real y un segundo rId3 apunta a un destino no compatible o vacío, la última escritura gana y pierde la hoja de trabajo. ParsePartRelationshipsXml, por lo tanto, aplica una regla de primera-gana con dos condiciones: el destino resuelto debe ser no vacío, y el identificador no debe estar ya presente. Ambas condiciones juntas son lo que lo hace seguro, porque la prueba de no vacío evita que una relación con un Target faltante reclame la ranura antes de que llegue una utilizable
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));
Nota la asimetría deliberada en ese fragmento. El mapa de identificadores es un mapa verdadero con una protección de primera-gana, mientras que la colección de tipos es una lista de solo anexar de pares type=target. Esa distinción es estructural: un libro tiene exactamente una relación de cadenas compartidas pero muchas relaciones de hoja de trabajo y de enlace externo, así que la búsqueda por tipo mediante Values[] devuelve la primera coincidencia para las partes únicas, y los tipos con múltiples valores como externalLink se enumeran recorriendo la lista
Dónde se detiene el seguimiento de relaciones
Los límites honestos importan más que una historia prolija. HotXLS recae en nombres convencionales siempre que falta una relación, así que un paquete con una parte de relación dañada o faltante igual se abre si da la casualidad de que sigue el diseño de Excel; ese respaldo es una característica de compatibilidad, no una segunda fuente de verdad, y puede enmascarar un error del productor durante las pruebas. Vale la pena conocer tres límites más. Los destinos marcados TargetMode="External" se almacenan textualmente en lugar de resolverse, lo cual es correcto para hipervínculos y para la relación externalLinkPath que lleva una URL de libro remoto, pero significa que el valor que obtienes de vuelta es lo que sea que el productor haya escrito. Las partes de gráfico descubiertas a través de una parte de relación de dibujo se emparejan con anclas de dibujo posicionalmente en lugar de por identificador, así que un orden de anclas inusual puede desalinear las vinculaciones de gráfico. Y el lector directo de streaming en lxDirectRead.pas mantiene su propia ruta más ligera de manejo indexada a xl/, así que el resolutor completo descrito aquí gobierna los puntos de entrada TXLSXWorkbook.Open y GetSheetNames, no la ruta de escaneo de baja asignación documentada en el artículo sobre el lector directo de streaming para Delphi
Si estás construyendo esto tú mismo, el resumen correcto más corto es: nunca construyas un nombre de parte, siempre resuelve uno. Lee _rels/.rels, sigue officeDocument, resuelve cada Target contra la parte que lo declaró, y enruta las hojas por r:id. Si prefieres tener eso ya probado contra partes renombradas, numeración de hojas no contigua, e identificadores de relación duplicados, el resolutor descrito aquí se incluye en el componente de hoja de cálculo HotXLS para Delphi, junto con la maquinaria de ida y vuelta que mantiene intactas las partes que no analiza