Условно объединить значения узлов XML через XSLT

Я новичок в 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>&#xa;</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>&#xa;</xsl:text>
                </xsl:when>
                <xsl:otherwise>
                    <p><xsl:apply-templates select = "."/></p><xsl:text>&#xa;</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 — это просто легкодоступный процессор. Разумеется, порядок тоже важен.

Оба ответа работают на упрощенном примере, но из-за дополнительных сложностей реального документа с заголовками и другими элементами, которые не заканчиваются знаками препинания, технику рекурсивного возврата в ответе @michael.hor257k было проще реализовать.

Simon 15.07.2024 07:31
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
1
58
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

С 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".

Simon 02.06.2024 07:18
Ответ принят как подходящий

Вот как это можно было сделать в 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>, который имеет желаемый результат.

Simon 03.06.2024 02:55

Я считаю, что более элегантным способом было бы избежать двойного тестирования узлов. Я добавил это в свой ответ.

michael.hor257k 03.06.2024 05:12

Другие вопросы по теме