6262
Как получить таблицу данных из xlsx-файла у приложения Excel из MS Office 2007 используя структуру данных формата Open XML?

 

Содержание:

 

Для чего написана эта статья.

Если вы имеете установленную у Вас серию продуктов из MS Office 2007, Вы наверное обратили внимание на тип файла "Книга Excel (*.xlsx)", имеющегося в диалоге "Сохранить [как/Другие форматы]", расширение которого, как и структура содержащихся в нём данных, отличается от прежнего xls-формата (совместимого с Excel 97-2003). Этот типа принадлежит к т.н. типам файлов Office 2007, известных как Office XML Open Format. См. также статьи Алексея Федорова в разделе: Внешние ссылки по теме. Ниже приведена таблица форматов, поддерживаемых Excel из MS Office 2007 с краткими пояснениями к ним (позаимствована из документации):

Формат Расширение Описание
Книга Excel XLSX Стандартный формат файлов Office Excel 2007 на основе XML. Не сохраняет код VBA-макросов, а также листы макросов Microsoft Excel 4.0 (XLM).
Лист Excel (код) XLSM Формат файлов Office Excel 2007 на основе XML, поддерживающий сохранение макросов. Сохраняет код VBA-макросов, а также листы макросов Excel 4.0 (XLM).
Двоичная книга Excel XLSB Формат двоичных файлов Office Excel 2007 (BIFF12).
Шаблон XLTX Стандартный формат файлов шаблонов Office Excel 2007Excel. Не сохраняет код VBA-макросов, а также листы макросов Microsoft Excel 4.0 (XLM).
Шаблон (код) XLTXM Формат файлов шаблонов Office Excel 2007Excel, поддерживающий сохранение макросов. Сохраняет код VBA-макросов, а также листы макросов Microsoft Excel 4.0 (XLM).
Книга Microsoft Excel 97-2003 XLS Формат двоичных файлов Excel 97 - Excel 2003 (BIFF8).
Шаблон Excel 97- Excel 2003 XLT Формат двоичных файлов Excel 97 - Excel 2003 (BIFF8) для хранения шаблонов Excel.
Книга Microsoft Excel 5.0/95 XLS Формат двоичных файлов Excel 5.0/95 (BIFF5).
XML-таблица 2003 XML Формат файлов XML-таблиц 2003 (XMLSS).
Данные XML Data XML Формат данных XML.
Надстройка Microsoft Excel XLAM Формат файлов надстроек Office Excel 2007, обеспечивающих дополнительные возможности программ, создаваемых для исполнения дополнительного кода, на основе XML, и поддерживающих макросы. Поддерживает использование проектов VBA и листов макросов Excel 4.0 (XLM).

На самом деле, любой файл xlsx-формата представляет собой zip-файл, содержащий в себе несколько подкаталогов, в которых имеются ряд файлов с данными в xml(eXtensible Markup Language)-формате. В этом нетрудно убедиться, если заменить расширение xlsx-файла на zip и попробовать разархивировать полученный после переименования zip-файл с помощью какой-нибудь утилиты, способной это сделать, например используя WinRAR.exe, а в старших версиях OS Windows это можно также осуществить и с помощью обычного "Проводника" OS Windows.

Цель данной заметки заключается в том, чтобы на конкретном примере показать способ извлечений таблицы данных из xlsx-файла, используя XSLT(eXtensible Stylesheet Language Transformations)-преобразования над xml-данными, хранящимися в xlsx-файлах. Нами будет проделано две вещи:

 

Получение xlsx-файла по данным из xml+xsd.

Чтобы самим не придумывать данные для Excel-таблицы, воспользуемся тем, что можно загрузить из интернета по ссылке: Сопоставление XML-элементов и отмена их сопоставленияexternal.gif. Продублирую прямо сюда текст файлов, позаимствованных из этого источника. Итак, там взяты два файла: Расходы.xml - пример данных, на основе которых, нами будет построена Excel-таблица и Расходы.xsd - схема для него.

В файле Расходы.xml имеем следующее:

<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<
Root>
  <
EmployeeInfo>
    <
Name>Jane Winston</Name>
    <
Date>2001-01-01</Date>
    <
Code>0001</Code>
  </
EmployeeInfo>
  <
ExpenseItem>
    <
Date>2001-01-01</Date>
    <
Description>Airfare</Description>
    <
Amount>500.34</Amount>
  </
ExpenseItem>
  <
ExpenseItem>
    <
Date>2001-01-01</Date>
    <
Description>Hotel</Description>
    <
Amount>200</Amount>
  </
ExpenseItem>
  <
ExpenseItem>
    <
Date>2001-01-01</Date>
    <
Description>Taxi     Fare</Description>
    <
Amount>100.00</Amount>
  </
ExpenseItem>
  <
ExpenseItem>
    <
Date>2001-01-01</Date>
    <
Description>Long     Distance Phone Charges</Description>
    <
Amount>57.89</Amount>
  </
ExpenseItem>
  <
ExpenseItem>
    <
Date>2001-01-01</Date>
    <
Description>Food</Description>
    <
Amount>82.19</Amount>
  </
ExpenseItem>
  <
ExpenseItem>
    <
Date>2001-01-02</Date>
    <
Description>Food</Description>
    <
Amount>17.89</Amount>
  </
ExpenseItem>
  <
ExpenseItem>
    <
Date>2001-01-02</Date>
    <
Description>Personal Items</Description>
    <
Amount>32.54</Amount>
  </
ExpenseItem>
  <
ExpenseItem>
    <
Date>2001-01-03</Date>
    <
Description>Taxi Fare</Description>
    <
Amount>75.00</Amount>
  </
ExpenseItem>
  <
ExpenseItem>
    <
Date>2001-01-03</Date>
    <
Description>Food</Description>
    <
Amount>36.45</Amount>
  </
ExpenseItem>
  <
ExpenseItem>
    <
Date>2001-01-03</Date>
    <
Description>New Suit</Description>
    <
Amount>750.00</Amount>
  </
ExpenseItem>
</
Root>

а в файле Расходы.xsd:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<
xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <
xsd:element name="Root">
    <
xsd:complexType>
      <
xsd:sequence>
        <
xsd:element minOccurs="0" maxOccurs="1" name="EmployeeInfo">
          <
xsd:complexType>
            <
xsd:all>
              <
xsd:element minOccurs="0" maxOccurs="1" name="Name" type="xsd:anyType" />
              <
xsd:element minOccurs="0" maxOccurs="1" name="Date" type="xsd:anyType" />
              <
xsd:element minOccurs="0" maxOccurs="1" name="Code" type="xsd:anyType" />
            </
xsd:all>
          </
xsd:complexType>
        </
xsd:element>
        <
xsd:element minOccurs="0" maxOccurs="unbounded" name="ExpenseItem">
          <
xsd:complexType>
            <
xsd:sequence>
              <
xsd:element name="Date" type="xsd:date" />
              <
xsd:element name="Description" type="xsd:string" />
              <
xsd:element name="Amount" type="xsd:decimal" />
            </
xsd:sequence>
          </
xsd:complexType>
        </
xsd:element>
      </
xsd:sequence>
    </
xsd:complexType>
  </
xsd:element>
</
xsd:schema>

Далее, создадим на основе данных этих двух файлов таблицу в Excel из MS Office 2007. Ниже шаг за шагом и т.с. в картинках показано, как это можно сделать. Но прежде всего, если у Вас на "Панели быстрого доступа" отсутствует вкладка "Разработчик", то проделайте действия, изображённые на Рис.1:

xlsx1.jpg
Рис.1

т.е. отметьте пункт "Показывать вкладку "Разработчик" на ленте" в диалоге "Параметры Excel" и подтвердите изменения, нажав кнопку "Ok" в правой нижней части окна этого диалога.

Следующим действием загрузим файл-схему Расходы.xsd в Excel, для чего выполните пункты, отмеченные красными кружками на Рис.2:

xlsx2.jpg
Рис.2

Если всё предшествующее не вызвало никаких ошибок, то в результате Вы должны получить то, что изображено на Рис.3:

xlsx3.jpg
Рис.3

т.е. в правой панели "Источник XML" у Вас должна появиться древовидная структура полей, описанных в фале-схеме Расходы.xsd.

Следующим действием, мы должны сопоставить данные в области листа книги с полями загруженной xsd-схемы. Для чего следует мышкой выделить корневой элемент в правой панели "Источник XML" (Root в нашем случае) и не отпуская мышки, перетащить его в ячейку A1 на Лист 1. После чего отпустить кнопку мыши. Проделав это, мы получим ситуацию, подобную изображённой на Рис.4:

xlsx4.jpg
Рис.4

Наконец, попробуем выполнить загрузку/импорт собственно самих данных из файла Расходы.xml, для чего выделив область данных нашей таблицы на Лист 1 (область ячеек: A2:F2), воспользуемся кнопкой "Импорт" на закладке "Разработчик" так, как показано на Рис.5:

xlsx5.jpg
Рис.5

Если никаких ошибок при загрузке xml-данных из файла Расходы.xml не возникло, то вы должны получить ситуацию, изображённую на Рис.6:

xlsx6.jpg
Рис.6

Наконец-то мы достигли конечной цели данного этапа, и можем сохранить полученное в виде файла Книга1.xlsx. Только перед сохранением желательно удалить пустые лишние листы: Лист 2, Лист 3.

Здесь резонно возникает вопрос: а возможна ли загрузка/импорт табличных xml-данных в MS Excel 2007 в случае отсутствия файла xsd-схемы? И ответ здесь: да возможна. В этом случае, Excel 2007 сам попробует создать схему/карту Ваших xml-данных, опираясь на анализ значений самих исходных данных. Что же, попробуем открыть файл Расходы.xml без предварительной загрузки его файла-схемы Расходы.xsd, а в возникшем при этом вопросе в диалоге "Открытие XML", подтвердить пункт "XML-таблица". На Рис.7 ниже изображена ситуация, которую я получил при таком эксперименте:

xlsx7.jpg
Рис.7

Обратите внимание, что в столбцах B и C тип данных будет соответственно: "Дата" и "Общий" в отличии от "Текст", которые мы имели выше, т.е. при использовании файла-схемы Расходы.xsd перед загрузкой xml-данных.

Получив после сохранения файл Книга1.xlsx, сделайте его копию как файл Книга1.zip, и разархивируйте последний, включая структуру подкаталогов, с помощью используемого Вами средства работы с zip-архивами в каталог Книга1. Если скажем, у Вас имеется установленный WinRAR, то разархивировать файл Книга.xlsx Вы сможете создав и выполнив командный файл getContentsUseWinRAR.cmd вот с таким содержанием:

@echo off
mode con: cp select=1251 > _tmp.txt
"C:\Program Files\WinRAR\WinRAR.exe" X -ad -ibck -o+ Книга1.xlsx
del _tmp.txt

Описание структуры полученных после разархивации подкаталогов/файлов можно найти в ссылках, приведённых мной в разделе Внешние ссылки по теме, для дальнейшего нас будут интересовать только полученные после распаковки xml-файлы, содержащие как собственно данные, так и описания их типов. К ним относятся:

xlsx11.jpg

Скопируйте перечисленные выше файлы: sharedStrings.xml, xmlMaps.xml, table1.xml и sheet1.xml в отдельный рабочий каталог. Работой только с этими файлами в отдельном рабочем каталоге мы собственно и займёмся в дальнейшем.

 

Извлечение xml-таблицы данных из xlsx-файла.

Прежде всего, попытка выполнить экспорт данных, содержащихся в полученном нами файле Книга1.xlsx, завершается неудачей, изображённой на рисунке ниже:

xlsx8.jpg
Рис.8

Я не нашёл разгадки этой очередной MS-загадки... :-( Однако, если бы даже выполнение экспорта таблицы данных из xlsx-файла средствами MS Excel 2007 не вызывало бы проблем (например, можно попробовать сохранить содержание листа в формате: "XML-таблица 2003" и о работе с данными в рамках этого формата, можно посмотреть здесь: Как получить dbf-таблицы из xls-файла при наличии групп в данных?), изложенное ниже в этом разделе, может иметь самостоятельный интерес, т.к. позволяет выполнить экспорт xml-таблицы данных даже в тех случаях, когда у клиента отсутствует установленный у него на компьютере Excel из MS Office 2007.

Что же, попробуем разобраться во внутреннем представлении xml-данных, а точнее файлов: sharedStrings.xml, table1.xml и sheet1.xml из предыдущего раздела, с тем чтобы попытаться всё же выполнить экспорт таблицы данных.

В полученном нами Книга.zip-файле, начальный фрагмент файла sharedStrings.xml имеет вид:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<
sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="46" uniqueCount="16">
  <
si>
    <
t>Name</t>
  </
si>
  <
si>
    <
t>Date</t>
  </
si>
  <
si>
    <
t>Code</t>
  </
si>
  <
si>
    <
t>Description</t>
  </
si>
  <
si>
    <
t>Amount</t>
  </
si>
  <
si>
    <
t>Date2</t>
  </
si>
  <
si>
    <
t>Jane Winston</t>
  </
si>
  <
si>
    <
t>2001-01-01</t>
  </
si>
  <
si>
    <
t>0001</t>
  </
si>
  <
si>
    <
t>Airfare</t>
  </
si>
  <
si>
    <
t>Hotel</t>
  </
si>
  <
si>
    <
t>Taxi Fare</t>
  </
si>
  <
si>
   <
t>Long Distance Phone Charges</t>
  </
si>
  ...
</
sst>

и как не трудно догадаться по названию файла, элементы <t>...</t> которого, у каждого элемента <si>...</si> в качестве текста имеют строки из нашей исходной xml-таблицы данных. Точнее, это заголовки столбцов нашей таблицы, и далее строки-значения данных исходной таблицы xml-данных. Причём, если непосредственно в данных одна и та же строка может встретиться несколько раз (как например, строка 'Jane Winston' или '2001-01-01'), то в файле sharedStrings.xml такое значение представлено единственным si-элементом.

Забегая слегка вперёд, XPATH-выражение фильтра для выбора элемента <t>...</t>, содержащего текст у n-го элемента <si>...</si>, могло бы выглядеть так: select="/sst/si[$n]/t", где переменная n - имела бы в качестве значения число, означающее порядковый номер выбираемого si-элемента, начиная с 1.

В представленном ниже файле table1.xml, нас будет интересовать только информация о типах данных столбцов таблицы:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<
table xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" id="1" name="1" displayName="1"
    
ref="A1:F11" tableType="xml" totalsRowShown="0" connectionId="1">
  <
autoFilter ref="A1:F11"/>
  <
tableColumns count="6">
    <
tableColumn id="1" uniqueName="Name" name="Name">
      <
xmlColumnPr mapId="1" xpath="/Root/EmployeeInfo/Name" xmlDataType="anyType"/>
    </
tableColumn>
    <
tableColumn id="2" uniqueName="Date" name="Date">
      <
xmlColumnPr mapId="1" xpath="/Root/EmployeeInfo/Date" xmlDataType="anyType"/>
    </
tableColumn>
    <
tableColumn id="3" uniqueName="Code" name="Code">
      <
xmlColumnPr mapId="1" xpath="/Root/EmployeeInfo/Code" xmlDataType="anyType"/>
    </
tableColumn>
    <
tableColumn id="4" uniqueName="Date" name="Date2">
      <
xmlColumnPr mapId="1" xpath="/Root/ExpenseItem/Date" xmlDataType="date"/>
    </
tableColumn>
    <
tableColumn id="5" uniqueName="Description" name="Description">
      <
xmlColumnPr mapId="1" xpath="/Root/ExpenseItem/Description" xmlDataType="string"/>
    </
tableColumn>
    <
tableColumn id="6" uniqueName="Amount" name="Amount">
      <
xmlColumnPr mapId="1" xpath="/Root/ExpenseItem/Amount" xmlDataType="decimal"/>
    </
tableColumn>
  </
tableColumns>
  <
tableStyleInfo name="TableStyleMedium9" showFirstColumn="0" showLastColumn="0" showRowStripes="1"
    
showColumnStripes="0"/>
</
table>

т.е. значение атрибута xmlDataType у соответствующего элемента tableColumn, в то время как выбор требуемого столбца можно сделать, опираясь на значение атрибута id, содержащего в качестве значений порядковый номер столбца, начиная с 1. Соответствующее XPATH-выражение для выбора типа данных n-го столбца, могло бы выглядеть так: select="/table/tableColumns/tableColumn[@id=$n]/xmlColumnPr/@xmlDataType"

Наконец, рассмотрим содержание файла sheet1.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<
worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
    
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
  <
sheetPr codeName="1"/>
  <
dimension ref="A1:F11"/>
  <
sheetViews>
    <
sheetView tabSelected="1" topLeftCell="C1" zoomScaleNormal="100" workbookViewId="0">
      <
selection activeCell="I3" sqref="I3"/>
    </
sheetView>
  </
sheetViews>
  <
sheetFormatPr defaultRowHeight="15"/>
  <
cols>
    <
col min="1" max="1" width="12.85546875" bestFit="1" customWidth="1"/>
    <
col min="2" max="2" width="10.42578125" bestFit="1" customWidth="1"/>
    <
col min="3" max="3" width="7.7109375" bestFit="1" customWidth="1"/>
    <
col min="4" max="4" width="12" bestFit="1" customWidth="1"/>
    <
col min="5" max="5" width="27.28515625" bestFit="1" customWidth="1"/>
    <
col min="6" max="6" width="10.42578125" bestFit="1" customWidth="1"/>
  </
cols>
  <
sheetData>
    <
row r="1" spans="1:6">
      <
c r="A1" t="s">
        <
v>0</v>
      </
c>
      <
c r="B1" t="s">
        <
v>1</v>
      </
c>
      <
c r="C1" t="s">
        <
v>2</v>
      </
c>
      <
c r="D1" t="s">
        <
v>5</v>
      </
c>
      <
c r="E1" t="s">
        <
v>3</v>
      </
c>
      <
c r="F1" t="s">
        <
v>4</v>
      </
c>
    </
row>
    <
row r="2" spans="1:6">
      <
c r="A2" s="1" t="s">
        <
v>6</v>
      </
c>
      <
c r="B2" s="1" t="s">
        <
v>7</v>
      </
c>
      <
c r="C2" s="1" t="s">
        <
v>8</v>
      </
c>
      <
c r="D2" s="2">
        <
v>36892</v>
      </
c>
      <
c r="E2" s="1" t="s">
        <
v>9</v>
      </
c>
      <
c r="F2">
        <
v>500.34</v>
      </
c>
    </
row>
    ...
  </
sheetData>
  <
pageMargins left="0.7" right="0.7" top="0.75" bottom="0.75" header="0.3" footer="0.3"/>
  <
pageSetup paperSize="9" orientation="portrait" r:id="rId1"/>
  <
tableParts count="1">
   <
tablePart r:id="rId2"/>
  </
tableParts>
</
worksheet>

На верхнем уровне файл имеет коневой элемент <worksheet>...</worksheet>, который в свою очередь, содержит элементы: sheetPr, dimension, sheetViews, sheetFormatPr, cols, sheetData, pageMargins, pageSetup, tableParts. Собственно сами данные (в т.ч. и данные о данных) содержатся в виде списка дочерних элементов <row>...</row> у элемента <sheetData>...</sheetData>. Ради сокращения объёма элементы <row>...</row> с значениями своих атрибутов r="3" и до конца (т.е. до r="11" в нашем случае) удалены из приведённого выше текста, а оставлены только с r="1" и r="2", что вполне достаточно, чтобы разобраться в том, как они "устроены".

Разглядывая содержание элементов row (т.е. строк листа) видим, что каждый из них состоит из набора элементов <c>...</c>, (т.е. колонок [или столбцов]), каждый из которых помимо атрибутов: r [, s, t], содержит элемент <v>...</v>, текст которого собственно и представляет из себя значение содержания соответствующей колонки. А приглядевшись внимательно к значениям элементов <v>...</v>, можно заметить странность, которая заключается в том, что там, где мы ожидали бы увидеть текст в виде строки, имеем просто число. Обратите внимание, что атрибут t у соответствующего элемента <c>...</c> при этом, имеет значение "s". Однако, вспомнив о существовании выше рассмотренного нами файла sharedStrings.xml, легко обнаруживается, что число, используемое в качестве значения у элементов <v>...</v>, есть ни что иное, как порядковый номер si-элемента в файле sharedStrings.xml, правда с нумерацией от значения 0, t-элемент которого, собственно и содержит искомую нами текстовую строку, как значение колонки/столбца у соответствующей строки из файла sheet1.xml. Этот факт легко обнаруживается например, у элемента <row>...</row> при значении его атрибута r="1", т.е. когда дочерние c-элементы должны содержать заголовки столбцов нашей таблицы... Вот такая нормализация (исключение дублирования значений) для строк имеет место быть в MS Excel 2007, однако! :-)

Ну хорошо, а как же теперь всем этим воспользоваться, чтобы "собирать" разбросанные таким образом значения в один xml-файл, представляющий собой результат экспорта таблицы данных из xlsx-файла? На мой взгляд, проще всего воспользоваться технологиями XSLT, чтобы осуществить это "меньшей кровью". Ниже показывается как это можно сделать.

В качестве первого шага, попробуем написать XSLT-преобразование, в котором оставаясь в рамках структуры элемента sheetData из файла sheet1.xml в результате преобразований, заменим все ссылки на строки из файла sharedStrings.xml (т.е. элементы <v>...</v> с числовыми значениями, у которых родительский c-элемент будет иметь атрибут t="s"). XSLT-код такого преобразования мог бы быть следующим:

<?xml version="1.0" encoding="utf-8"?>
<!--
File: extractXlsxData.xslt -->
<
xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
    
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
    
exclude-result-prefixes="xsl r msxsl">

  <
xsl:output version="1.0" method="xml"
    
standalone="yes" encoding="utf-8"/>

  <
xsl:param name="prmPathToSharedStringsFile">
    <
xsl:text></xsl:text>
  </
xsl:param>
  <
xsl:variable name="varDocSharedStrings">
    <
xsl:copy-of select="document(concat($prmPathToSharedStringsFile, 'sharedStrings.xml'))" />
  </
xsl:variable>

  <
xsl:template match="/">
    <
sheetData>
      <
xsl:apply-templates select="/./*[local-name()='worksheet']/*[local-name()='sheetData']/*" />
    </
sheetData>
  </
xsl:template>

  <
xsl:template match="@*|node()">
    <
xsl:variable name="varLocalName">
      <
xsl:value-of select="local-name()" />
    </
xsl:variable>
    <
xsl:choose>
      <
xsl:when test="string-length($varLocalName)>0 and $varLocalName != 'v'">
        <
xsl:element name="{$varLocalName}">
          <
xsl:for-each select="@*">
            <
xsl:attribute name="{local-name()}">
              <
xsl:value-of select="." />
            </
xsl:attribute>
          </
xsl:for-each>
          <
xsl:apply-templates select="*" />
        </
xsl:element>
      </
xsl:when>
      <
xsl:when test="$varLocalName='v'">
        <
xsl:choose>
          <
xsl:when test="parent::*/@t='s'">
            <
xsl:variable name="varNumSs">
              <
xsl:value-of select="." />
            </
xsl:variable>
            <
v>
              <
xsl:value-of select="msxsl:node-set($varDocSharedStrings)/./*[local-name()='sst']/*[local-name()='si'][number($varNumSs) + 1]/*[local-name()='t']" />
            </
v>
          </
xsl:when>
          <
xsl:otherwise>
            <
v>
              <
xsl:value-of select="." />
            </
v>
          </
xsl:otherwise>
        </
xsl:choose>
      </
xsl:when>
      <
xsl:otherwise>
        <
xsl:apply-templates select="@*|node()" />
      </
xsl:otherwise>
    </
xsl:choose>
  </
xsl:template>

</
xsl:stylesheet>

Здесь, кажущиеся утяжелёнными условные XPATH-выражения типа: /./*[local-name()='worksheet']/*[local-name()='sheetData']/* вместо прямого выбора типа: /./worksheet/sheetData/*, на самом деле необходимы, т.к. обрабатываемые xml-данные, имеют namespace-ы по умолчанию (default namespace), а в таких случаях прямое использование названий элементов в XPATH-выражениях "не работает", в таких случаях приходится использовать относительные пути и условные выборки.

Для выполнения XSLT-преобразований, можно воспользоваться бесплатной утилитой командной строки от Microsoft msxsl.exe (см. "Command Line Transformation Utility (msxsl.exe)" в разделе: Внешние ссылки по теме) и применяя приведённое выше XSLT-преобразование extractXlsxData.xslt над файлами sharedStrings.xml и sheet1.xml (например, поместив в командный файл extractToXml.cmd строку: msxsl.exe sheet1.xml extractXlsxData.xslt -o tr.xml) получаем на выходе tr.xml-файл со следующим содержанием:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<
sheetData>
  <
row r="1" spans="1:6">
    <
c r="A1" t="s">
      <
v>Name</v>
    </
c>
    <
c r="B1" t="s">
      <
v>Date</v>
    </
c>
    <
c r="C1" t="s">
      <
v>Code</v>
    </
c>
    <
c r="D1" t="s">
      <
v>Date2</v>
    </
c>
    <
c r="E1" t="s">
      <
v>Description</v>
    </
c>
    <
c r="F1" t="s">
      <
v>Amount</v>
    </
c>
  </
row>
  <
row r="2" spans="1:6">
    <
c r="A2" s="1" t="s">
      <
v>Jane Winston</v>
    </
c>
    <
c r="B2" s="1" t="s">
      <
v>2001-01-01</v>
    </
c>
    <
c r="C2" s="1" t="s">
      <
v>0001</v>
    </
c>
    <
c r="D2" s="2">
      <
v>36892</v>
    </
c>
    <
c r="E2" s="1" t="s">
      <
v>Airfare</v>
    </
c>
    <
c r="F2">
      <
v>500.34</v>
    </
c>
  </
row>
  ...
</
sheetData>

Здесь, как и в предыдущем случае, для уменьшения объёма документа оставлены только две строки (row-элементы с атрибутами r="1" и r="2"), а остальные данные упущены. Как видим, всё работает так, как и ожидалось! Однако, обратите внимание на значение даты во второй строке в колонке D (<c r="D2"...><v>36892</v></c>). Хм... дата в xml-представлении не в формате: YYYY-MM-DD, а в количестве дней с 01.01.1970. Да уж!... Придётся с этим чего-то делать... :-(

Для выполнения XSLT-преобразований можно пробовать использовать и другие средства... Например, в MS .NET Framework 2.0, 3.0, C#-код консольного приложения мог бы быть таким:

using System;
using System.Collections.Generic;
using System.Text;

using System.Xml;
using System.Xml.Xsl;
namespace testTranse
{
  class Program
  {
    
const bool IS_DOCFUNC = true;
    
const bool IS_SCRIPT = false;
    
const bool USE_DEBUG = false;
    
const string XSL_FILE = "extractXlsxData.xslt";
    
const string INP_FILE = "sheet1.xml";
    
const string OUT_FILE = "tr.xml";

    
static void Main(string[] args)
    {
      
bool bIsOk = false;
      
try
      
{
        
XsltSettings xsltSettings = new XsltSettings(IS_DOCFUNC, IS_SCRIPT);
        
XslCompiledTransform xslTrans = new XslCompiledTransform(USE_DEBUG);
        xslTrans.Load(XSL_FILE, xsltSettings,
new XmlUrlResolver());
        xslTrans.Transform(INP_FILE, OUT_FILE);
        bIsOk =
true;
      }
      
catch (Exception ex)
      {
        
Console.WriteLine("*** Error: " + ex.ToString());
      }
      
if (bIsOk)
      {
        
Console.WriteLine("Transformation successfully completed!");
      }
      
Console.WriteLine("Press any key to continue...");
      
Console.ReadKey();
    }
  }
}

Если будете экспериментировать именно с extractXlsxData.xslt, здесь константе IS_DOCFUNC следует присвоить именно true, т.к. в его коде выполняется загрузка файла sharedStrings.xml, используя XSLT-функцию document().

Аналогичный MS Visual FoxPro 9.0 код с использованием MSXML 4.0 (Microsoft XML Core Services), мог бы выглядеть так:

#DEFINE XML_FILE "sheet1.xml"
#
DEFINE XSL_FILE "extractXlsxData.xslt"
#
DEFINE OUT_FILE "tr.xml"

#
DEFINE XSL_PRM_NAME "prmPathToSharedStringsFile"
#
DEFINE XSL_PRM_VALUE ""

#
DEFINE XML_VERS "" && or ".4.0" && or ".3.0"
#DEFINE XMLPI '<?xml version="1.0" encoding="windows-1251" standalone="yes"?>'

#
DEFINE SW_NORMAL 1
#
DEFINE CRLF CHR(13) + CHR(10)

SET DEFAULT TO (LEFT(SYS(16), RAT("\", SYS(16))))

LOCAL lcXmlFile as String;
  ,lcXslFile
as String;
  ,lcHtmFile
as String;
  ,lbResult
as Boolean

lcXmlFile =
LOWER(FULLPATH(XML_FILE))
lcXslFile =
LOWER(FULLPATH(XSL_FILE))
lcOutFile =
LOWER(FULLPATH(OUT_FILE))
IF !FILE(lcXslFile)
  
ERROR 101, lcXslFile
  RETURN lbResult
ENDIF
IF
!FILE(lcXmlFile)
  ERROR 101, lcXmlFile
  RETURN lbResult
ENDIF

DECLARE INTEGER
ShellExecute IN shell32.dll ;
  
INTEGER hwnd, ;
  
STRING @lsOperation, ;
  
STRING @lsFile, ;
  
STRING @lsParameters, ;
  
STRING @lsDirectory, ;
  
INTEGER liShowCmd
DECLARE INTEGER GetDesktopWindow IN user32.dll

LOCAL
loErr as Exception;
  ,lcPROGIDmsxml
as String;
  ,lcPROGIDmsxsl
as String;
  ,lcPROGIDmstemp
as String;
  ,loMsXml
as Msxml2.DOMDocument;
  ,loMsXsl
as Msxml2.FreeThreadedDOMDocument;
  ,loMsPrc
as Msxml2.XSLTemplate;
  ,lobjXSLTProc
as Msxml2.IXSLProcessor;
  ,lbParseError
as Boolean

TRY
  
*/////////////////////////////////////////////////
  *// Try do XML + XSL => OUT transformation
  
lcPROGIDmsxml = "Msxml2.DOMDocument" + XML_VERS
  lcPROGIDmsxsl = "Msxml2.FreeThreadedDOMDocument" + XML_VERS
  lcPROGIDmstemp = "Msxml2.XSLTemplate" + XML_VERS
  loMsXml =
CREATEOBJECT(lcPROGIDmsxml)
  loMsXsl =
CREATEOBJECT(lcPROGIDmsxsl)
  loMsPrc =
CREATEOBJECT(lcPROGIDmstemp)
  
*
  *-- Load XML
  
WITH loMsXml
    .async = .F.
    .
load(lcXmlFile) && Load XML-file
    
IF .parseError.errorCode != 0
      lbParseError = .T.
      ShowXmlErr(.parseError)
    
ENDIF
  ENDWITH
  IF
!lbParseError
    
*
    *-- Load XSL
    
WITH loMsXsl
      .async = .F.
      .
load(lcXslFile) && Load XSL-file
      
IF .parseError.errorCode != 0
        lbParseError = .T.
        ShowXmlErr(.parseError)
      
ENDIF
    ENDWITH
  ENDIF
  IF
!lbParseError
    
*
    *-- XSLT transformation
    
loMsPrc.stylesheet = loMsXsl.documentElement
    lobjXSLTProc = loMsPrc.createProcessor()
    
IF VARTYPE(lobjXSLTProc) = "O"
      lobjXSLTProc.
input = loMsXml
      
IF !EMPTY(XSL_PRM_VALUE)
        lobjXSLTProc.addParameter(XSL_PRM_NAME, XSL_PRM_VALUE, "")
      
ENDIF
      IF
lobjXSLTProc.transform()
        
LOCAL lcOutPut as String
        
lcOutPut = lobjXSLTProc.output
        
IF FILE(lcOutFile)
          
ERASE (lcOutFile)
        
ENDIF
        LOCAL
lnPosXmlPI as Number
        
lnPosXmlPI = ATC("?>", lcOutPut)
        
IF lnPosXmlPI > 0
          lcOutPut = XMLPI +
SUBSTRC(lcOutPut, lnPosXmlPI + 2)
        
ENDIF
        LOCAL
lnBytes as Number
        
lnBytes = 0
        lnBytes =
STRTOFILE(lcOutPut, lcOutFile)
        
IF lnBytes > 0 AND FILE(lcOutFile)
          
LOCAL lnRetCode as Number
          
lnRetCode = 0
          lnRetCode = ShellExecute(GetDesktopWindow(), "open", lcOutFile, "", "", SW_NORMAL)
          
IF lnRetCode > 32
            lbResult = .T.
          
ELSE
            ERROR
"ShellExecute: errorcode = " + LTRIM(STR(lnRetCode))
          
ENDIF
        ELSE
          ERROR
101, lcOutFile
        
ENDIF
      ELSE
        ERROR
"Error on XML + XSL => XML transformation"
      
ENDIF
    ELSE
      ERROR
"Can not createProcessor()"
    
ENDIF
  ENDIF
CATCH TO
loErr
  
MESSAGEBOX("Error: " + LTRIM(STR(loErr.ErrorNo)) + CRLF ;
    + "LineNo: " +
LTRIM(STR(loErr.LineNo)) + CRLF ;
    + "Message: " + loErr.
Message,16, _SCREEN.Caption)
FINALLY
  STORE NULL TO
;
    loMsXml, loMsXsl, loMsPrc, lobjXSLTProc
ENDTRY
CLEAR DLLS
RETURN
lbResult

*/////////////////////////////////////////////////
*// ShowXmlErr
FUNCTION ShowXmlErr
  
LPARAMETERS toXmlErr as Msxml2.IXMLDOMParseError
  
IF VARTYPE(toXmlErr) # 'O'
    
RETURN .F.
  
ENDIF
  LOCAL
lcMsg as String
  WITH
toXmlErr
    lcMsg = "Error loading '" +
ALLTRIM(.url) + "'"
    lcMsg = lcMsg + CRLF + "Code: " +
TRANSFORM(.errorCode, '@0')
    
IF !EMPTY(.reason)
      lcMsg = lcMsg + CRLF + "Reason: " +
ALLTRIM(.reason)
    
ENDIF
    IF
!EMPTY(.filepos)
      lcMsg = lcMsg + CRLF + "Filepos: " +
TRANSFORM(.filepos)
    
ENDIF
    IF
!EMPTY(.linepos)
      lcMsg = lcMsg + CRLF + "Linepos: " +
TRANSFORM(.linepos)
    
ENDIF
    IF
!EMPTY(.line)
      lcMsg = lcMsg + CRLF + "Line: " +
TRANSFORM(.line)
    
ENDIF
    IF
!EMPTY(.srcText)
      lcMsg = lcMsg + CRLF + "Text: " +
ALLTRIM(.srcText)
    
ENDIF
  ENDWITH
  MESSAGEBOX
(lcMsg, 16, _SCREEN.Caption)
ENDFUNC

Ну хорошо, удачно осуществив "подгруздку" строк-значений в таблицу xml-данных, давайте сделаем с этим результатом чего-нибудь действительно полезное. Например, вторым шагом попробуем написать код XSLT-преобразования такой, чтобы на выходе у нас получалось html-представление наших xml-данных.

В общих чертах, такое преобразование должно выполнять следующее: используя результат предыдущего шага на входе (файл tr.xml в нашем случае), а также файл table1.xml для извлечения информации о типах значений данных в столбцах, сформировать html-таблицу таким образом, чтобы данные из первой строки входных данных были бы заголовками столбцов формируемой таблицы, а во время создания строк выходной таблицы из строк исходных данных начиная со второй, нами должны быть учтены типы значений столбцов (из файла table1.xml) с тем, чтобы осуществить соответствующее форматирование значений, если требуется. Также неплохо было бы выровнять текст в каждой из колонок влево или вправо, в зависимости от типа данных.

В попытке учесть всё вышеперечисленное, мной написан код вот такого XSLT-преобразования:

<?xml version="1.0" encoding="utf-8"?>
<!--
File: getHtml.xslt-->
<
xsl:stylesheet version="1.0"
    xmlns="http://www.w3.org/TR/html4"
    
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
    
xmlns:user="urn:A9A4A767-327E-4c7d-970B-DC3FF20F6DB5:user-function"
    
exclude-result-prefixes="xsl msxsl user">

  <
xsl:output version="4.0" method="html" indent="no"
    
encoding="utf-8" doctype-public="-//W3C//DTD HTML 4.0 Transitional//EN" />

  <
msxsl:script language="JScript" implements-prefix="user">
    
function getDate(newdate)
    {
      var n, d, s, p, td, tm;
      n = parseInt(newdate.toString()) - 1;
      d = new Date(0, 0, n);
      //s = d.toLocaleDateString();
      p = "-";
      td = d.getDate().toString();
      if (td.length == 1)
      { td = "0" + td; }
      tm = (d.getMonth() + 1).toString();
      if (tm.length == 1)
      { tm = "0" + tm; }
      s = d.getFullYear().toString() + p
        + tm + p
        + td;
      return(s);
    }
  
</msxsl:script>

  <xsl:decimal-format name="russian" grouping-separator=" " decimal-separator="," />

  
<
xsl:param name="prmTableFile">
    <
xsl:text>table1.xml</xsl:text>
  </
xsl:param>

  <
xsl:variable name="varDocTable">
    <
xsl:copy-of select="document($prmTableFile)"/>
  </
xsl:variable>
  <
xsl:variable name="varTableName">
    <
xsl:value-of select="msxsl:node-set($varDocTable)/./*[local-name()='table']/@displayName" />
  </
xsl:variable>

  <
xsl:template match="/">
    <
html>
      <
head>
        <
meta name="vs_targetSchema" content="http://schemas.microsoft.com/intellisense/ie5" />
        <
meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <
style type="text/css">
          <
xsl:comment>
            
h4
            {font-family: Verdana, Arial, Helvetica, sans-serif;
              font-size: 8.5pt;}
            th
            {font-family: Verdana, Arial, Helvetica, sans-serif;
              font-size: 8.5pt;
              color: darkblue;
              background-color: WhiteSmoke;}
            td
            {font-family: Verdana, Arial, Helvetica, sans-serif;
              font-size: 8.5pt;}
            .even
            {font-family: Verdana, Arial, Helvetica, sans-serif;
              font-size: 8.5pt;
              background-color: Azure;}
          
</xsl:comment>
        </
style>
        <
title>
          <
xsl:value-of select="$varTableName" />
        </
title>
      </
head>
      <
body>
        <
h4>
          <
xsl:value-of select="$varTableName" />
        </
h4>
        <
table border="1" cellspacing="0" cellpadding="2">
          <
xsl:for-each select="/./*/row">
            <
tr>
              <
xsl:choose>
                <
xsl:when test="position()=1">
                  <
xsl:for-each select="c/v">
                    <
th>
                      <
xsl:value-of select="." />
                    </
th>
                  </
xsl:for-each>
                </
xsl:when>
                <
xsl:otherwise>
                  <
xsl:variable name="varClass">
                    <
xsl:if test="position() mod 2">
                      <
xsl:text>even</xsl:text>
                    </
xsl:if>
                  </
xsl:variable>
                  <
xsl:for-each select="c">
                    <
xsl:variable name="varNumCol">
                      <
xsl:value-of select="position()"/>
                    </
xsl:variable>
                    <
xsl:variable name="varType">
                      <
xsl:value-of select="msxsl:node-set($varDocTable)/./*[local-name()='table']/*[local-name()='tableColumns']/*[local-name()='tableColumn' and @id=$varNumCol]/*[local-name()='xmlColumnPr']/@xmlDataType"/>
                    </
xsl:variable>
                    <
xsl:call-template name="getValue">
                      <xsl:with-param name="prmClass" select="$varClass" />
                      
<
xsl:with-param name="prmType" select="$varType" />
                      <
xsl:with-param name="prmValue" select="v" />
                    </
xsl:call-template>
                  </
xsl:for-each>
                </
xsl:otherwise>
              </
xsl:choose>
            </
tr>
          </
xsl:for-each>
        </
table>
      </
body>
    </
html>
  </
xsl:template>

  <
xsl:template name="getValue">
    <xsl:param name="prmClass" />
    
<
xsl:param name="prmType" />
    <
xsl:param name="prmValue" />
    <td>
      <xsl:if test="string-length($prmClass)>0">
        <xsl:attribute name="class">
          <xsl:value-of select="$prmClass"/>
        </xsl:attribute>
      </xsl:if>
      <
xsl:attribute name="vAlign">
        <
xsl:text>top</xsl:text>
      </
xsl:attribute>
      <
xsl:if test="$prmType='decimal' or $prmType='int' or $prmType='date'">
        <
xsl:attribute name="align">
          <
xsl:text>right</xsl:text>
        </
xsl:attribute>
      </
xsl:if>
      <
xsl:choose>
        <
xsl:when test="$prmType='decimal'">
          <
xsl:value-of select="format-number($prmValue, '### ##0,00', 'russian')" />
        </
xsl:when>
        <
xsl:when test="$prmType='date'">
          <
xsl:variable name="varDate">
            <
xsl:choose>
              <
xsl:when test="contains($prmValue, '-')">
                <
xsl:value-of select="$prmValue" />
              </
xsl:when>
              <
xsl:otherwise>
                <
xsl:value-of select="user:getDate(string($prmValue))" />
              </
xsl:otherwise>
            </
xsl:choose>
          </
xsl:variable>
          <
xsl:value-of select="msxsl:format-date($varDate, 'dd.MM.yyyy')" />
        </
xsl:when>
        <
xsl:otherwise>
          <
xsl:value-of select="$prmValue" />
        </
xsl:otherwise>
      </
xsl:choose>
    </td>
  </
xsl:template>

</
xsl:stylesheet>

Применяя это преобразование к данным из файла tr.xml (т.е к результату, полученному на первом шаге) используя утилиту msxsl.exe (например, предварительно создав командный файл convertToHtml.cmd со строкой: msxsl.exe tr.xml getHtml.xslt -o tr.htm) или используя вышеприведённый C#-код, заменив в нём соответствующие константы на:

    ...
    const
bool IS_DOCFUNC = true;
    
const bool IS_SCRIPT = true;
    
const bool USE_DEBUG = false;
    
const string XSL_FILE = "getHtml.xslt";
    
const string INP_FILE = "tr.xml";
    
const string OUT_FILE = "tr.htm";
    ...

получаем в файле tr.htm вот такой результат:

xlsx9.jpg
Рис.9

что в общем-то и соответствует тому, к чему мы стремились...

Пожалуй, полезным будет привести сюда C#-код с использованием библиотеки C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\WindowsBase.dll, содержащей namespace System.IO.Packaging и позволяющей работать с частями zip-ованых файлов в MS .Net Framework 2.0, 3.0. Код консольного приложения с использованием этих возможностей мог бы быть таким:

using System;
using System.Collections.Generic;
using System.Text;

using System.IO;
using System.Xml;
using System.Xml.Xsl;
using System.IO.Packaging;

namespace xlsx2html
{
  
class Program
  {
    
const string INPUT_FILE = "Книга1.xlsx";
    
const string XSLT_SHEET_FILE = "extractXlsxData.xslt";
    
const string XSLT_HTML_FILE = "getHtml.xslt";
    
const string ERR_FILE_NOTFOUND = "Not found file: ";
    
const string ERR_PACKAGE_NOTOPEN = "Not open package for file: ";
    
const string ERR_PACKAGEPART_NOTFOUND = "Not found PackagePart: ";
    
const string ERR_XMLDOC_NOTLOAD = "Not load XmlDocument: ";
    
const string URI_SHARED_STRINGS = "/xl/sharedStrings.xml";
    
const string URI_FORMAT_SHEET = "/xl/worksheets/sheet{0}.xml";
    
const string URI_FORMAT_TABLE = "/xl/tables/table{0}.xml";
    
const int NUMBER_SHEET = 1;
    
const int NUMBER_TABLE = 1;

    
static void Main(string[] args)
    {
      
string XlsxFile = INPUT_FILE;
      
string HtmlFile = System.String.Empty;
      
int NumberSheet = NUMBER_SHEET;
      
int NumberTable = NUMBER_TABLE;

      
if (args.Length > 0)
      {
        XlsxFile = args[0];
      }
      
int nPosLE = XlsxFile.LastIndexOf('.');
      
if (nPosLE == (-1))
      {
        XlsxFile +=
".xlsx";
      }
      
if (args.Length > 1)
      {
        HtmlFile = args[2];
      }
      
else
      
{
        nPosLE = XlsxFile.LastIndexOf(
'.');
        HtmlFile = XlsxFile.Substring(0, nPosLE) +
".htm";
      }

      
bool bResult = false;
      
Package package = null;
      
StringBuilder sb = new StringBuilder();

      
try
      
{
        
if (!File.Exists(XlsxFile))
        {
        
  throw (new FileNotFoundException(ERR_FILE_NOTFOUND + XlsxFile));
        }
        package =
Package.Open(XlsxFile);
        
if (package == null)
        {
        
  throw (new NullReferenceException(ERR_PACKAGE_NOTOPEN + XlsxFile));
        }
        
if (args.Length > 2)
        {
          NumberSheet =
Convert.ToInt32(args[3]);
        }
        
if (args.Length > 3)
        {
          NumberTable =
Convert.ToInt32(args[4]);
        }

        
/////////////////////////////////
        // SharedStrings
        
Uri UriSharedStrings = new Uri(URI_SHARED_STRINGS, UriKind.Relative);
        
PackagePart ppSharedStrings = package.GetPart(UriSharedStrings);
        
if (ppSharedStrings == null)
        {
          
throw (new NullReferenceException(ERR_PACKAGEPART_NOTFOUND + UriSharedStrings.ToString()));
        }
        
XmlDocument xmlDocSharedStrings = new XmlDocument();
        xmlDocSharedStrings.Load(ppSharedStrings.GetStream());
        
if (xmlDocSharedStrings.DocumentElement == null)
        {
        
  throw (new NullReferenceException(ERR_XMLDOC_NOTLOAD + UriSharedStrings.ToString()));
        }

        
/////////////////////////////////
        // Sheet
        
sb.Length = 0;
        sb.AppendFormat(URI_FORMAT_SHEET, NumberSheet);
        
Uri UriSheet = new Uri(sb.ToString(), UriKind.Relative);
        
PackagePart ppSheet = package.GetPart(UriSheet);
        
if (ppSheet == null)
        {
        
  throw (new NullReferenceException(ERR_PACKAGEPART_NOTFOUND + UriSheet.ToString()));
        }
        
XmlDocument xmlDocSheet = new XmlDocument();
        xmlDocSheet.Load(ppSheet.GetStream());
        
if (xmlDocSheet.DocumentElement == null)
        {
        
  throw (new NullReferenceException(ERR_XMLDOC_NOTLOAD + UriSheet.ToString()));
        }

        
/////////////////////////////////
        // Table
        
sb.Length = 0;
        sb.AppendFormat(URI_FORMAT_TABLE, NumberTable);
        
Uri UriTable = new Uri(sb.ToString(), UriKind.Relative);
        
PackagePart ppTable = package.GetPart(UriTable);
        
if (ppTable == null)
        {
        
  throw (new NullReferenceException(ERR_PACKAGEPART_NOTFOUND + UriTable.ToString()));
        }
        
XmlDocument xmlDocTable = new XmlDocument();
        xmlDocTable.Load(ppTable.GetStream());
        
if (xmlDocTable.DocumentElement == null)
        {
        
  throw (new NullReferenceException(ERR_XMLDOC_NOTLOAD + UriTable.ToString()));
        }

        
/////////////////////////////////////////////////////////
        // Resolve SharedStrings in sheet
        
XsltArgumentList xsltArgListSheet = new XsltArgumentList();
        
XslCompiledTransform xslTransSheet = new XslCompiledTransform();
        xslTransSheet.Load(XSLT_SHEET_FILE);
        xsltArgListSheet.AddParam(
"prmDocSharedStrings", "", xmlDocSharedStrings.CreateNavigator());
        
string sOutXmlSheet = System.String.Empty;
        sb.Length = 0;
        
using (TextWriter twSheet = new StringWriter(sb))
        {
          xslTransSheet.Transform(xmlDocSheet.CreateNavigator(), xsltArgListSheet, twSheet);
          sOutXmlSheet = sb.ToString();
        }

        
/////////////////////////////////////////////////////////
        // Convert to html
        
XmlDocument xmlDocSheetOut = new XmlDocument();
        xmlDocSheetOut.LoadXml(sOutXmlSheet);
        
XsltArgumentList xsltArgListHtml = new XsltArgumentList();
        
XsltSettings xsltSettings = new XsltSettings(false, true);
        
XslCompiledTransform xslTransHtml = new XslCompiledTransform();
        xslTransHtml.Load(XSLT_HTML_FILE, xsltSettings,
new XmlUrlResolver());
        xsltArgListHtml.AddParam(
"prmDocTable", "", xmlDocTable.CreateNavigator());
        
string sOutHtml = System.String.Empty;
        sb.Length = 0;
        
using (TextWriter twHtml = new StringWriter(sb))
        {
          xslTransHtml.Transform(xmlDocSheetOut.CreateNavigator(), xsltArgListHtml, twHtml);
          sOutHtml = sb.ToString();
        }

        
/////////////////////////////////////////////////////////
        // Write result as html-file
        
using (TextWriter twOutHtml = new StreamWriter(HtmlFile))
        {
          twOutHtml.Write(sOutHtml);
        }

        package.Close();
        package =
null;

        bResult =
true;
      }
      
catch (Exception ex)
      {
      
  Console.WriteLine("*** Error: " + ex.ToString());
      }
      
if (bResult)
      {
        
Console.WriteLine("Convert to html successfully completed!");
      }
      
Console.WriteLine("Press any key to continue...");
      
Console.ReadKey();
      
if (package != null)
      {
        package.Close();
        package =
null;
      }
    }
  }
}

Чтобы воспользоваться этим кодом в MS VS.NET 2005, создайте консольное приложение на C# с названием xlsx2html, в Solution Explorer в папку References добавьте библиотеку C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\WindowsBase.dll, а весь код в Program.cs замените вышеприведённым. В используемых при этом xslt-файлах: extractXlsxData.xslt и getHtml.xslt изменён способ подгруздки "дополнительных файлов", передаваемых через параметры. Чтобы адаптировать их на новые условия использования, необходимо внести ряд изменений.

Так, в extractXlsxData.xslt фрагмент кода:

  ...
  <
xsl:param name="prmPathToSharedStringsFile">
    <
xsl:text></xsl:text>
  </
xsl:param>
  <
xsl:variable name="varDocSharedStrings">
    <
xsl:copy-of select="document(concat($prmPathToSharedStringsFile, 'sharedStrings.xml'))" />
  </
xsl:variable>
  ...

следует изменить на

  ...
  <
xsl:param name="prmDocSharedStrings" />
  ...

и далее в тексте extractXlsxData.xslt все вхождения varDocSharedStrings необходимо поменять на prmDocSharedStrings

Аналогично, в файле getHtml.xslt фрагмент кода:

  ...
  <
xsl:param name="prmTableFile">
    <
xsl:text>table1.xml</xsl:text>
  </
xsl:param>

  <
xsl:variable name="varDocTable">
    <
xsl:copy-of select="document($prmTableFile)"/>
  </
xsl:variable>
  ...

следует изменить на

  ...
  <
xsl:param name="prmDocTable" />
  ...

и далее в тексте getHtml.xslt все вхождения varDocTable следует изменить на prmDocTable

Здесь у VFP-программиста может возникнуть раздражение: мол чего C#-код приводить, если из-под VFP его нельзя выполнить... и будет неправ... :-) т.к. использовать библиотеки классов из .NET Framework из-под VFP всё-таки можно! Один из возможных вариантов загрузки и использования .NET Framework-сборок описан на http://www.west-wind.com/WebLog/posts/104449.aspx - "Hosting the .NET Runtime in Visual FoxPro", by Rick Strahlexternal.gif usa.gif. Там же можно загрузить весь код, обеспечивающий поддержку такого подхода. Смысл предложенного в следующем:

1) Имеется возможность через небольшую прослойку C++кода (Unmanaged API) в виде Win32API-dll (ClrHost.dll)

2) Rick также написал небольшой C#-код в виде класса в .NET Framework-сборке (wwDotNetBridge.dll), позволяющего: