Не так давно по долгу службы понадобилось редактировать файлы MS Office (в первую очередь MS Word и MS Excel) средствами PL/SQL, то есть языка, с .NET не связанного практически никак. В связи с этим возникла проблема, что в руководстве от Microsoft про редактирование этих файлов, что называется, «руками» не сказано практически ничего, а единственный вменяемый сайт-справочник по этому делу, видимо, не обновлялся года этак с 2010. Понимание необходимости свести воедино все, что я по крупицам собрал со Stackoverflow и собственных экспериментов, пришло почти сразу.

Сразу хочется сделать несколько замечаний.

Первое. В основном говорить буду про то, с чем сталкивался лично. Претензий на стопроцентное знание «изнанки» формата у меня нет.

Второе. Как многие, вероятно, знают, файлы MS Office 2007 и выше представляют собой архив, который можно открыть с помощью любого архиватора (WinRAR, 7zip и так далее).

Третье. «Под капотом» у этих файлов — в основном XML-разметка, гордо именуемая OOXML или просто OpenXML. Поэтому, в принципе, для понимания принципов редактирования файлов «руками» достаточно будет Блокнота (или, что удобнее, Notepad++).

Итак, начнем с формата MS Excel как наиболее употребимого для генерации всевозможных отчетов, выгрузок из БД и иже с ними.

Когда имеешь дело с форматом .XLSX, на каждый вопрос «почему оно реализовано… так?» следует отвечать: «того требовала оптимизация». Когда Microsoft расширили допустимый размер листа до неприличного с точки зрения наблюдателя (около миллиона строк и 16 тысяч с хвостиком столбцов), они понимали, что найдутся маньяки, которые все это забьют данными под завязку. А через это, если подходить к хранению данных «в лоб», то даже все возрастающих мощностей ПК не хватит ни при каких условиях. Следовательно, надо сделать так, чтобы объем хранимых данных был меньше, чем объем данных в книге. Как это сделать? Забегая вперед: например, исключить дубликаты строк.

Первый интересующий нас файл — %file%/xl/workbook.xml. Основное его назначение — манифест, то есть перечень листов, из которых наша книга Excel, собственно, и состоит.
Выглядит этот перечень, к примеру, так:

<sheets>
	<sheet name="Лист1" sheetId="1" r:id="rId1"/>
	<sheet name="Лист2" sheetId="2" r:id="rId2"/>
	<sheet name="Лист3" sheetId="3" r:id="rId3"/>
	<sheet name="Лист4" sheetId="4" r:id="rId4"/>
</sheets>

Это значит, что в книге 4 листа, и их имена указаны в атрибутах name. Каждому тегу должен соответствовать файл в папке %file%/xl/worksheets. Excel сам знает, как должны называться эти файлы и при попытке их переименовать, сочтет всю книгу поврежденной.

Еще в папке %file%/xl нас интересует файл styles.xml.

Как нетрудно догадаться, здесь хранится информация об оформлении ячеек. Причем в угоду оптимизации, хранится она в достаточно интересном виде. Файл состоит из следующих секций:

1. Шрифты:

<fonts count="2" x14ac:knownFonts="1">
	<font>
		<sz val="11"/>
		<color theme="1"/>
		<name val="Calibri"/>
		<family val="2"/>
		<scheme val="minor"/>
	</font>
	<font>
		<b/>
		<sz val="11"/>
		<color theme="1"/>
		<name val="Calibri"/>
		<family val="2"/>
		<charset val="204"/>
		<scheme val="minor"/>
	</font>
</fonts>

Как можно понять, здесь перечислены только уникальные стили оформления текста, использованные в книге. Каждый тег — один стиль. Вложенные теги — особенности стиля, такие как полужирное написание (тег <b/>), кегль (<sz/>) и другие.

2. Заливка ячеек:

<fills count="2">
	<fill>
		<patternFill patternType="none"/>
	</fill>
	<fill>
		<patternFill patternType="gray125"/>
	</fill>
</fills>

Как видно, первый вариант — без заливки вообще, а второй — сплошная заливка библиотечного цвета «gray125».

3. Границы:

<borders count="2">
	<border>
		<left/>
		<right/>
		<top/>
		<bottom/>
		<diagonal/>
	</border>
	<border>
		<left style="thin">
			<color indexed="64"/>
		</left>
		<right style="thin">
			<color indexed="64"/>
		</right>
		<top style="thin">
			<color indexed="64"/>
		</top>
		<bottom style="thin">
			<color indexed="64"/>
		</bottom>
		<diagonal/>
	</border>
</borders>

Как видно, одно наименование здесь состоит из пяти элементов, 4 основных границы и диагональная, то есть все то, что можно настроить через GUI самого Excel.

4. Стили ячеек:

<cellXfs count="4">
	<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
	<xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" applyFont="1" applyAlignment="1">
		<alignment wrapText="1"/>
	</xf>
	<xf numFmtId="0" fontId="0" fillId="0" borderId="1" xfId="0" applyBorder="1" applyAlignment="1">
		<alignment horizontal="center" vertical="center"/>
	</xf>
	<xf numFmtId="0" fontId="1" fillId="2" borderId="1" xfId="0" applyFont="1" applyFill="1" applyBorder="1" applyAlignment="1">
		<alignment horizontal="center" vertical="center"/>
	</xf>
</cellXfs>

А вот здесь надо объяснить подробнее. Когда в файле листа мы будем указывать стиль ячейки, мы будем ссылаться как раз на эту секцию. Каждый тег <xf />, представляющий собой один стиль, является собранием ссылок на предыдущие секции, то есть он сам по себе не содержит объявления шрифта, границ и заливки. Рассмотрим интересные атрибуты ближе:

  1. numFmtId — указание формата текста в ячейке (дата, валюта, число, текст, ...). Полный перечень этих типов есть здесь
  2. fontId, fillId, borderId — ссылка на шрифт/заливку/границы (см. секцию 1, 2 и 3 соответственно). Нумерация начинается с 0.
  3. applyFont, applyFill, applyBorder — указание на то, что при оформлении этой ячейки вообще используется кастомный шрифт/заливка/границы. По умолчанию 0, так что можно не указывать вообще, как видно на примере в элементе #0.
  4. applyAlignment — указание на то, что выравнивание текста в ячейке будет отличаться от стандартного. По умолчанию 0, но если указано «1», то в родительский тег <xf />
    встраивается тег <alignment />, как видно на примере начиная с #1.

Далее разговор пойдет про текстовые данные и собственно разметку листов.

Комментарии (10)


  1. DocJester
    25.01.2018 18:57
    +2

    Как оно устроено — это здорово. А как вы с этим потом работали средствами PL/SQL рассказ будет?


    1. Lachrimae Автор
      25.01.2018 19:16
      +2

      Разумеется. Но сначала, на мой взгляд, лучше объяснить, как оно устроено, чтобы не было недосказанности: почему оно работает, как оно работает и так далее.


    1. habradante
      25.01.2018 19:21
      +1

      Вероятно потом, автор, путем xslt трансформации (модуль xml2) и функций pl/sql выполнял преобразование данных.
      Данные -> xml -> xslt_process -> Готовый xlsx документ
      По крайней мере, так я делал в PostgreSQL


      1. Lachrimae Автор
        25.01.2018 19:23
        +1

        Нет.
        Если в двух словах, то я перегнал исходный файл в BLOB, оттуда выделил тексты интересующих меня файлов в CLOB, а уже там начал колдовать.
        Подробнее напишу, когда дойду до этого.


    1. intsurfer
      26.01.2018 09:11

      через updatexml

      кусок реального кода — update tablica set dat = UPDATEXML(XMLTYPE.createXML(dat),'/data/address/text()','новый_адрес').getClobVal()
      where id = ИД; --dat хранится в формате clob

      А имея представление о структуре, дальше можно на лету все менять, тем более, что Oracle XML позволяет работать и с атрибутами. В общем, за поднятую тему автору СПАСИБО!


  1. iit
    26.01.2018 06:43

    Вот бы еще про .docx такую статейку!


  1. Sirion
    26.01.2018 09:13

    Маловата статья, по-хорошему, объединить бы её со следующей частью. Но тема интересная. Сам занимался разбором .xlsx (не нашёл нормальной либы для js, пришлось писать свой велосипед), но как устроены стили внутри, не разбирался, информация из статьи была мне полезна.


  1. irvinatkins
    26.01.2018 14:36

    Есть проект под названием abap2xlsx
    в нем ексель можно разобрать на атомы и потом собрать обратно, правда на абапе, но посмотреть как оно устроено внутри можно.


  1. Fynsoft
    26.01.2018 14:59

    Уже давно используем пакет по генерации XLSX из Oracle
    technology.amis.nl/2011/02/19/create-an-excel-file-with-plsql
    а не так давно и запись данных в шаблон xlsx
    habrahabr.ru/sandbox/94161


    1. Lachrimae Автор
      26.01.2018 15:02

      Этот пакет влился в библиотеку Alexandria PL/SQL. Некоторые процедуры я из нее и беру. Но я сделал над ней свою надстройку, про которую расскажу подробнее в следующем посте.