Я новичок в StackOverflow и прочитал много вопросов по XSLT, но изо всех сил пытаюсь найти ответ на проблему, с которой столкнулся.
В настоящее время я работаю с двумя различными выходными форматами OCR на основе XML (ABBYY и HOCR) и использую XSLT, чтобы преобразовать их в формат HTML, который Стандартные инструменты для электронных книг могут использовать для вывода рабочего EPUB. Оба формата OCR определяют страницы, абзацы и строки внутри абзаца, поэтому несложно собрать строки обратно и вывести блоки абзацев HTML.
Сейчас я сталкиваюсь с проблемой при попытке снова соединить абзацы, которые разбиты на страницы.
Крайне упрощенный пример исходного документа выглядит так:
<document xmlns = "http://www.abbyy.com/FineReader_xml/FineReader10-schema-v1.xml">
<page>
<block>
<par>This is a line.</par>
<par>This is a line</par>
<par>that is split.</par>
</block>
</page>
<page>
<block>
<par>line split</par>
</block>
</page>
<page>
<block>
<par>across pages.</par>
</block>
</page>
</document>
и идеальный результат
<p>This is a line.</p>
<p>This is a line that is split.</p>
<p>line split across pages</p>
Я начал с этого:
<xsl:stylesheet version = "1.0" xmlns:xsl = "http://www.w3.org/1999/XSL/Transform" xmlns:x = "http://www.abbyy.com/FineReader_xml/FineReader10-schema-v1.xml" exclude-result-prefixes = "x">
<xsl:output omit-xml-declaration = "yes"/>
<xsl:template match = "/">
<xsl:apply-templates select = "x:document/x:page/x:block/x:par"/>
</xsl:template>
<xsl:template match = "x:par">
<p><xsl:value-of select = "."/></p><xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
который выполняет базовое преобразование. Субшаблон par, насколько я понимаю, и в результате попыток не имеет сведений о предыдущих или последующих узлах par.
Я пробовал вернуться на несколько уровней назад и использовать цикл for-each, определять конец абзаца по знаку препинания (также упрощенно для этого примера) и использовать following-sibling, но с этим все еще возникают проблемы, а вы этого не делаете. кажется, что можно пропустить узел, и, что более важно, par на следующей page не кажется родственным par на предыдущей странице, что имеет смысл - это другая ветвь.
<xsl:stylesheet version = "1.0" xmlns:xsl = "http://www.w3.org/1999/XSL/Transform" xmlns:x = "http://www.abbyy.com/FineReader_xml/FineReader10-schema-v1.xml" exclude-result-prefixes = "x">
<xsl:output omit-xml-declaration = "yes"/>
<xsl:template match = "/">
<xsl:apply-templates select = "x:document"/>
</xsl:template>
<xsl:template match = "x:document">
<xsl:for-each select = "x:page/x:block/x:par">
<xsl:choose>
<xsl:when test = "substring(./text(), string-length(./text()))!='.'">
<p><xsl:apply-templates select = "."/><xsl:text> </xsl:text>
<xsl:apply-templates select = "following-sibling::*[1]"/></p><xsl:text>
</xsl:text>
</xsl:when>
<xsl:otherwise>
<p><xsl:apply-templates select = "."/></p><xsl:text>
</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:template>
<xsl:template match = "x:par">
<xsl:value-of select = "."/>
</xsl:template>
</xsl:stylesheet>
Я также пытался использовать условие xsl:if, чтобы выбрать, когда печатать теги <p>, но получаю ошибки от xsltproc о несовпадающих тегах.
Возможно ли то, что я пытаюсь сделать, с помощью XSLT? Я также не обязательно предан XLST 1.0 — xsltproc — это просто легкодоступный процессор. Разумеется, порядок тоже важен.





С XSLT 2 или 3 вы можете использовать, например.
<xsl:stylesheet xmlns:xsl = "http://www.w3.org/1999/XSL/Transform"
version = "3.0"
xmlns:xs = "http://www.w3.org/2001/XMLSchema"
xpath-default-namespace = "http://www.abbyy.com/FineReader_xml/FineReader10-schema-v1.xml"
exclude-result-prefixes = "#all">
<xsl:template match = "document">
<xsl:for-each-group select = "//par" group-ending-with = "par[ends-with(., '.')]">
<p>
<xsl:apply-templates select = "current-group()/node()"/>
</p>
</xsl:for-each-group>
</xsl:template>
<xsl:output indent = "yes"/>
</xsl:stylesheet>
и получить, например.
<p>This is a line.</p>
<p>This is a linethat is split.</p>
<p>line splitacross pages.</p>
Вы можете легко запустить XSLT 3 с помощью SaxonJ для Java (11 и 12 — поддерживаемые выпуски), SaxonC для Python и C/C++ и PHP, Saxon.NET для .NET framework, SaxonJS/xslt3 для JavaScript или Node.js .
group-ending-with = "par[ends-with(., '.')]" пытается обработать ваш упрощенный вариант использования, но его можно заменить, например, на group-ending-with = "par[matches(., '[.!?]$')]", чтобы обрабатывать больше случаев.
Я опробовал это и попытался немного изменить его, чтобы добавить пробелы к текстовым фрагментам, чтобы получить желаемый результат. например ``` <xsl:template match = "node()"> <xsl:value-of select = "."/> <xsl:if test = "position()!=last()"><xsl:text > </xsl:text></xsl:if> </xsl:template> ``` Хотя это работает, я не совсем уверен, какой элемент node() соответствует в этом контексте. Этот подшаблон не применяется при настройке match = "par".
Вот как это можно было сделать в XSLT 1.0:
<xsl:stylesheet version = "1.0"
xmlns:xsl = "http://www.w3.org/1999/XSL/Transform" xmlns:x = "http://www.abbyy.com/FineReader_xml/FineReader10-schema-v1.xml"
exclude-result-prefixes = "x">
<xsl:output omit-xml-declaration = "yes"/>
<xsl:template match = "/x:document">
<xsl:for-each select = "x:page/x:block/x:par[substring(., string-length(.)) = '.']">
<p>
<xsl:apply-templates select = "."/>
</p>
</xsl:for-each>
</xsl:template>
<xsl:template match = "x:par">
<xsl:apply-templates select = "preceding::x:par[1][not(substring(., string-length(.)) = '.')]"/>
<xsl:value-of select = "."/>
</xsl:template>
</xsl:stylesheet>
Обратите внимание, что результатом является фрагмент XML, а не правильно сформированный XML-документ (без единого корневого элемента).
О другом (и, возможно, более эффективном) подходе см.: https://stackoverflow.com/a/67035437/3016153
Чтобы вставить пробел после каждого сегмента, кроме последнего, вы можете сделать:
<xsl:stylesheet version = "1.0"
xmlns:xsl = "http://www.w3.org/1999/XSL/Transform"
xmlns:x = "http://www.abbyy.com/FineReader_xml/FineReader10-schema-v1.xml"
exclude-result-prefixes = "x">
<xsl:output omit-xml-declaration = "yes"/>
<xsl:template match = "/x:document">
<xsl:for-each select = "x:page/x:block/x:par[substring(., string-length(.)) = '.']">
<p>
<xsl:apply-templates select = ".">
<xsl:with-param name = "last" select = "true()"/>
</xsl:apply-templates>
</p>
</xsl:for-each>
</xsl:template>
<xsl:template match = "x:par">
<xsl:param name = "last"/>
<xsl:apply-templates select = "preceding::x:par[1][not(substring(., string-length(.)) = '.')]"/>
<xsl:value-of select = "."/>
<xsl:if test = "not($last)">
<xsl:text> </xsl:text>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Да, я доволен тем, что это фрагмент — следующий шаг в цепочке инструментов с этим вполне согласен. Я немного повозился с этим — между фрагментами текста должны быть вставлены пробелы, и я придумал измененный подшаблон x:par: <xsl:template match = "x:par"> <xsl:apply-templates select = "preceding::x:par[1][not(substring(., string-length(.)) = '.')]"/> <xsl:value-of select = "."/><xsl:if test = "not(substring(., string-length(.)) = '.')"> <xsl:text> </xsl:text> </xsl:if></xsl:template>, который имеет желаемый результат.
Я считаю, что более элегантным способом было бы избежать двойного тестирования узлов. Я добавил это в свой ответ.
Оба ответа работают на упрощенном примере, но из-за дополнительных сложностей реального документа с заголовками и другими элементами, которые не заканчиваются знаками препинания, технику рекурсивного возврата в ответе @michael.hor257k было проще реализовать.