У меня есть большой набор данных XML, который необходимо проанализировать и преобразовать в CSV. Одним из элементов XML является процедура, последовательность шагов. Последовательность шагов возникла на отформатированном экране, где много кода RTF допускало маркированные списки, различия шрифтов и так далее. При экспорте из базы данных в мой исходный XML эти отформатированные инструкции стали кодами RTF в XML, например так:
<SPECORMETHOD>{\rtf1\ansi\deff0\uc1\ansicpg1252\deftab720{\fonttbl{\f0\fnil\fcharset1 Arial;}{\f1\fnil\fcharset1 Garamond;}{\f2\fnil\fcharset0 Garamond;}{\f3\fnil\fcharset1 WingDings;}}{\colortbl\red0\green0\blue0;\red255\green0\blue0;\red0\green128\blue0;\red0\green0\blue255;\red255\green255\blue0;\red255\green0\blue255;\red128\green0\blue128;\red128\green0\blue0;\red0\green255\blue0;\red0\green255\blue255;\red0\green128\blue128;\red0\green0\blue128;\red255\green255\blue255;\red192\green192\blue192;\red128\green128\blue128;\red0\green0\blue0;}\wpprheadfoot1\paperw12240\paperh15840\margl720\margr720\margt720\margb720\headery720\footery720\endnhere\sectdefaultcl{\*\generator WPTools_5.17;}{\*\listtable{\list\listtemplateid1\listsimple{\listlevel\leveljc0\levelfollow0\levelstartat1\levelspace0\levelindent360\levelnfc0{\leveltext\'02\'00.;}{\levelnumbers\'01;}}\listid1}}{\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}}{\ls1\ilvl0{\listtext 1.\tab}\li400\fi-400\plain\f2\fs26 Procedure Step 1.\par{\listtext\fs26 2.\tab}\plain\f2\fs26 Procedure Step 2.\par{\listtext\fs26 3.\tab}\plain\f2\fs26 Procedure Step 3.\par{\listtext\fs26 4.\tab}\plain\f2\fs26 Procedure Step 4.\par{\listtext\fs26 5.\tab}\plain\f2\fs26 Procedure Step 5.\par{\listtext\fs26 6.\tab}\plain\f2\fs26 Procedure Step 6.\par\pard\plain\plain\f2\fs26\par\plain\f2\fs26 Entry dated 02-07-2023\par}}</SPECORMETHOD>
Если я сохраню этот контент в формате RTF, открою его в любой текстовой программе и сохраню как текст, я получу желаемые результаты:
1. Procedure Step 1.
2. Procedure Step 2.
3. Procedure Step 3.
4. Procedure Step 4.
5. Procedure Step 5.
6. Procedure Step 6.
Entry dated 02-07-2023
Однако я бы предпочел делать это динамически в потоке XSLT, поскольку в структуре XML есть десятки тысяч экземпляров процедур. Если я разделю их на файлы, мне придется повторно связать их обратно в правильное положение в XML с дополнительными шагами (что хорошо, если мне нужно, но кажется неэффективным).
Я пробовал:
Я использую XML 1.1, XSLT 2.0 через saxon-he-11.3 на Java 17.0.4.1, все через Eclipse IDE 2022-12 (4.26.0).
В конце концов, я ищу предложения о том, как лучше всего подойти к этому массовому преобразованию RTF в текст в элементе XML во время обработки XSLT.
Спасибо, Майкл
🤔 А знаете ли вы, что...
XML допускает создание пользовательских тегов и определение собственных правил разметки данных.
Я нашел Apache Tika в качестве конвертера RTF в XHTML (https://tika.apache.org/2.7.0/examples.html#Parsing_to_XHTML) и сумел интегрировать его как встроенную функцию расширения в Saxon 11 HE, которая принимает входную строку rtf и преобразует ее в XdmNode, поэтому в XSLT/XPath вы можете в дальнейшем обрабатывать ее как обычное входное дерево:
package org.example;
import net.sf.saxon.s9api.*;
import org.apache.tika.exception.TikaException;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.sax.ToXMLContentHandler;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.tika.parser.AutoDetectParser;
import org.xml.sax.XMLFilter;
import org.xml.sax.XMLReader;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamSource;
public class Main {
public static void main(String[] args) throws SaxonApiException {
Processor processor = new Processor(false);
processor.registerExtensionFunction(new ExtensionFunction() {
@Override
public QName getName() {
return new QName("http://example.com/mf/tika", "parse-rtf");
}
public SequenceType getResultType() {
return SequenceType.makeSequenceType(
ItemType.ANY_NODE, OccurrenceIndicator.ONE
);
}
@Override
public SequenceType[] getArgumentTypes() {
return new SequenceType[]{
SequenceType.makeSequenceType(
ItemType.STRING, OccurrenceIndicator.ONE)};
}
@Override
public XdmValue call(XdmValue[] xdmValues) throws SaxonApiException {
try {
return parseRtfToHTML(xdmValues[0].itemAt(0).getStringValue(), processor);
} catch (IOException | URISyntaxException e) {
throw new SaxonApiException(e);
} catch (SAXException e) {
throw new SaxonApiException(e);
} catch (TikaException e) {
throw new SaxonApiException(e);
}
}
});
XsltCompiler xsltCompiler = processor.newXsltCompiler();
Xslt30Transformer xslt30Transformer = xsltCompiler.compile(new StreamSource(new File("sheet1.xsl"))).load30();
XdmValue result = xslt30Transformer.applyTemplates(new StreamSource(new File("sample1.xml")));
System.out.println(result);
}
public static XdmNode parseRtfToHTML(String rtf, Processor processor) throws IOException, SAXException, TikaException, URISyntaxException {
DocumentBuilder docBuilder = processor.newDocumentBuilder();
docBuilder.setBaseURI(new URI("urn:from-string"));
ContentHandler handler = new ToXMLContentHandler();
AutoDetectParser parser = new AutoDetectParser();
Metadata metadata = new Metadata();
try (InputStream stream = new ByteArrayInputStream(rtf.getBytes("utf8"))) {
parser.parse(stream, handler, metadata);
return docBuilder.build(new StreamSource(new StringReader(handler.toString())));
} catch (SaxonApiException e) {
throw new RuntimeException(e);
}
}
}
POM-зависимости:
<dependencies>
<dependency>
<groupId>net.sf.saxon</groupId>
<artifactId>Saxon-HE</artifactId>
<version>11.4</version>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-parsers-standard-package</artifactId>
<version>2.7.0</version>
</dependency>
</dependencies>
С образцом, подобным тому, который указан в вашем вопросе, и таблицей стилей следующим образом
<?xml version = "1.0" encoding = "utf-8"?>
<xsl:stylesheet xmlns:xsl = "http://www.w3.org/1999/XSL/Transform"
version = "3.0"
xmlns:xs = "http://www.w3.org/2001/XMLSchema"
xmlns:tika = "http://example.com/mf/tika"
exclude-result-prefixes = "#all"
expand-text = "yes">
<xsl:template match = "SPECORMETHOD">
<rtf-as-xhtml>
<xsl:sequence select = "tika:parse-rtf(.)"/>
</rtf-as-xhtml>
</xsl:template>
<xsl:mode on-no-match = "shallow-copy"/>
<xsl:output indent = "yes"/>
<xsl:template match = "/" name = "xsl:initial-template">
<xsl:next-match/>
<xsl:comment>Run with {system-property('xsl:product-name')} {system-property('xsl:product-version')} {system-property('Q{http://saxon.sf.net/}platform')}</xsl:comment>
</xsl:template>
</xsl:stylesheet>
вывод, например.
<rtf-as-xhtml><html xmlns = "http://www.w3.org/1999/xhtml">
<head>
<meta name = "X-TIKA:Parsed-By" content = "org.apache.tika.parser.DefaultParser"/>
<meta name = "X-TIKA:Parsed-By" content = "org.apache.tika.parser.microsoft.rtf.RTFParser"/>
<meta name = "Content-Type" content = "application/rtf"/>
<title/>
</head>
<body><p>Procedure Step 1.</p>
<p>Procedure Step 2.</p>
<p>Procedure Step 3.</p>
<p>Procedure Step 4.</p>
<p>Procedure Step 5.</p>
<p>Procedure Step 6.</p>
<p/>
<p>Entry dated 02-07-2023</p>
<p/>
</body></html></rtf-as-xhtml>
<!--Run with SAXON HE 11.4 -->
Таким образом, в этой простой демонстрации я не предпринимал никаких усилий для дальнейшей обработки XHTML, возвращенного Tika из встроенной функции расширения, но, конечно, вы можете использовать полный набор XSLT 3.0/XPath 3.1 в Saxon 11, чтобы выбрать или преобразовать его дальше.