使用StAX从父元素解析文本和元素

问题描述 投票:0回答:1

DL/DR;

我正在使用

StAX
流式解析 XML 文件以提取一些数据。

我遇到的问题是,当我遇到一个同时包含文本和另一个

element
element
时,当我尝试使用
xmlEventReader.getElementText()
提取文本时,它会抛出异常,因为该方法期望父级
element
仅包含文本。

<div>Hello <i>World</i>!</div>

事实上,

div
标签直接包含文本和
i
标签,导致文本提取失败。

我希望能够从上面的示例 XML 中提取

Hello World!

整个故事

我正在编写一个小型 Java 应用程序来导出当前存储在 Evernote 中的食谱,然后将它们导入到另一个应用程序中。

我只是厌倦了 Evernote 不断涌现的注册 PRO 的行为。

我正在使用

StAX
来流式解析包含我的笔记的 XML 文件,每个笔记都包含一个食谱。

我可以从 Evernote 导出所有笔记,现在我需要解析这些笔记以提取我的食谱数据。成分和说明以

CDATA element
内的 HTML 标记存储在笔记正文中。

我基本上解析了所有 XML/HTML 元素,一旦到达

li
标签,我就设置我处于
list-item
内的状态,并将其中的任何文本连接在一起,从而删除任何文本作为格式插入的 HTML 标记。

它工作得很好,但是当有一个

element
包含
text
和另一个
element
时,我遇到了一个小问题。

当我到达该父元素并调用

xmlEventReader.getElementText()
时,它会抛出异常,因为该方法期望
element
仅包含文本。

示例 XML

我使这个示例非常简单,仅包含一些说明,删除了成分逻辑。

<en-note>
    <ol>
        <li>
            <div>Add all ingredients to a </div>
            <div>ziplock freezer bag</div>
        </li>
        <li>
            <div>Freezer until needed (<i>maximum 2 months</i>).</div>
        </li>
    </ol>
</en-note>

这是解析笔记正文的代码。

我简化了这段代码,删除了与成分相关的任何逻辑,因为它与问题无关。

上述 XML 将被加载到代码示例顶部的

recipeContent
变量中。

由于

listItemValueSb.append(xmlEventReader.getElementText());
标签,代码在解析第二个方向时在
i
处中断。

import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;

import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

...

private static void parseRecipeContent(
        final String recipeContent,
        final RecipeContentHandler recipeContentHandler
)
        throws XMLStreamException
{
    final XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
    XMLEventReader xmlEventReader = null;

    try(
            final StringReader stringReader = new StringReader(recipeContent);
    )
    {
        xmlEventReader = xmlInputFactory.createXMLEventReader(stringReader);
        XMLEvent currentEvent;
        StartElement tmpStartElement;
        EndElement tmpEndElement;
        boolean directionsMode = false;
        boolean withinLiTag = false;
        StringBuilder listItemValueSb = null;
        final List<String> directions = new ArrayList<>();
        
        while (xmlEventReader.hasNext())
        {
            currentEvent = xmlEventReader.nextEvent();
            
            // If the current event is an END-EVENT, then potentially end this recipe.
            if (currentEvent.isEndElement())
            {
                tmpEndElement = currentEvent.asEndElement();
                
                switch (tmpEndElement.getName().getLocalPart().toLowerCase())
                {
                    case "en-note":
                        // Inform the calling code of the newly found recipe.
                        recipeContentHandler.handleRecipeContent(
                                ingredients,
                                directions
                        );
                        
                        break;
                    case "li":
                        withinLiTag = false;
                        
                        if (
                                (listItemValueSb != null)
                                    &&
                                (listItemValueSb.length() > 0)
                        )
                        {
                            directions.add(listItemValueSb.toString());
                        }
                        
                        break;
                    default:
                        break;
                }
                
                continue;
            }
            
            // If the current event is a START-EVENT, then extract the relevant data.
            if (!currentEvent.isStartElement())
            {
                continue;
            }
            
            tmpStartElement = currentEvent.asStartElement();
            
            switch (tmpStartElement.getName().getLocalPart().toLowerCase())
            {
                case "en-note":
                    withinLiTag = false;
                    directions.clear();
                    
                    break;
                case "li":
                    withinLiTag = true;
                    listItemValueSb = new StringBuilder();
                    
                    break;
                default:
                    final XMLEvent nextXMLEvent = xmlEventReader.peek();
                    
                    if (
                            (nextXMLEvent == null)
                                ||
                            !nextXMLEvent.isCharacters()
                    )
                    {
                        break;
                    }
                    
                    if (withinLiTag)
                    {
                        listItemValueSb.append(xmlEventReader.getElementText());
                    }
                    
                    break;
            }
        }
    }
    finally
    {
        if (xmlEventReader != null)
        {
            xmlEventReader.close();
        }
    }
}
java stax
1个回答
0
投票

您的要求似乎是

li
内的所有文本都很重要,无论格式如何。使用
peek
事件并不能很好地处理这种情况。相反,如果您在当前“继续”的分支上获取所有文本内容,并在此处进行更改,则会更容易:

// If the current event is a START-EVENT, then extract the relevant data.
if (!currentEvent.isStartElement())  {
    // This ensures all character content inside <li> is recorded
    if (withinLiTag && currentEvent.isCharacters()) {
        listItemValueSb.append(currentEvent.asCharacters().getData());
    }
    continue;
}

上面这行收集了

li
的内部文本,并使所有窥视流的“默认”代码变得不必要,只需注释掉即可:

switch (tmpStartElement.getName().getLocalPart().toLowerCase()) {
    ...
    default:
        // comment out the default handling:
        // final XMLEvent nextXMLEvent = xmlEventReader.peek();
        // ...
© www.soinside.com 2019 - 2024. All rights reserved.