如何从XMLReader中获取属性

13

我有一些HTML代码,我正在使用Html.fromHtml(...)将其转换为Spanned,其中包括一个自定义标签:

<customtag id="1234">

所以我已经实现了一个TagHandler来处理这个自定义标签,就像这样:

public void handleTag( boolean opening, String tag, Editable output, XMLReader xmlReader ) {

    if ( tag.equalsIgnoreCase( "customtag" ) ) {

        String id = xmlReader.getProperty( "id" ).toString();
    }
}
在这种情况下,我收到了一个SAX异常,因为我相信"id"字段实际上是一个属性,而不是一个属性。但是,XMLReader没有getAttribute()方法。所以我的问题是,如何使用这个XMLReader获取"id"字段的值?谢谢。

TagHandler在哪里?通常使用SAX2的方式是使用ContentHandler,不是吗? - Ray Toal
1
TagHandler 用于通过 Html.fromHtml(String, ImageGetter, TagHandler) 将 HTML 文本转换为可跨度文本时使用。它用于处理未知标签(即 TagSoup 不识别的标签)。 - Jason Robinson
我明白了。我只是使用TagSoup标记了这个问题,以便那些熟悉这个解析器的人可以找到这个问题。我知道在标准Java库中的常规SAX2解析器中,你只需要设置ContentHandlers而不是TagHandlers,并且startElement回调已经具有属性。 - Ray Toal
6
我曾经遇到过同样的问题,当我查看Android源代码时,发现属性是有意不传递的。因此,我用具有特定名称的其他标签替换了带有属性的标签。就像在你的情况下使用<customtag1234>一样。 - vortexwolf
有没有人使用过像 https://github.com/NightWhistler/HtmlSpanner 或 https://github.com/commonsguy/cwac-richedit 这样的替代工具? - vitaly
显示剩余3条评论
5个回答

11

使用TagHandler提供的XmlReader可以获取标记属性值,而无需使用反射,但该方法比反射更加复杂。诀窍是将XmlReader使用的ContentHandler替换为自定义对象。只能在调用handleTag()时替换ContentHandler。这会导致获取第一个标记的属性值时出现问题,可以通过在HTML开头添加自定义标记来解决。

import android.text.Editable;
import android.text.Html;
import android.text.Spanned;

import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

import java.util.ArrayDeque;

public class HtmlParser implements Html.TagHandler, ContentHandler
{
    public interface TagHandler
    {
        boolean handleTag(boolean opening, String tag, Editable output, Attributes attributes);
    }

    public static Spanned buildSpannedText(String html, TagHandler handler)
    {
        // add a tag at the start that is not handled by default,
        // allowing custom tag handler to replace xmlReader contentHandler
        return Html.fromHtml("<inject/>" + html, null, new HtmlParser(handler));
    }

    public static String getValue(Attributes attributes, String name)
    {
        for (int i = 0, n = attributes.getLength(); i < n; i++)
        {
            if (name.equals(attributes.getLocalName(i)))
                return attributes.getValue(i);
        }
        return null;
    }

    private final TagHandler handler;
    private ContentHandler wrapped;
    private Editable text;
    private ArrayDeque<Boolean> tagStatus = new ArrayDeque<>();

    private HtmlParser(TagHandler handler)
    {
        this.handler = handler;
    }

    @Override
    public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader)
    {
        if (wrapped == null)
        {
            // record result object
            text = output;

            // record current content handler
            wrapped = xmlReader.getContentHandler();

            // replace content handler with our own that forwards to calls to original when needed
            xmlReader.setContentHandler(this);

            // handle endElement() callback for <inject/> tag
            tagStatus.addLast(Boolean.FALSE);
        }
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes)
            throws SAXException
    {
        boolean isHandled = handler.handleTag(true, localName, text, attributes);
        tagStatus.addLast(isHandled);
        if (!isHandled)
            wrapped.startElement(uri, localName, qName, attributes);
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException
    {
        if (!tagStatus.removeLast())
            wrapped.endElement(uri, localName, qName);
        handler.handleTag(false, localName, text, null);
    }

    @Override
    public void setDocumentLocator(Locator locator)
    {
        wrapped.setDocumentLocator(locator);
    }

    @Override
    public void startDocument() throws SAXException
    {
        wrapped.startDocument();
    }

    @Override
    public void endDocument() throws SAXException
    {
        wrapped.endDocument();
    }

    @Override
    public void startPrefixMapping(String prefix, String uri) throws SAXException
    {
        wrapped.startPrefixMapping(prefix, uri);
    }

    @Override
    public void endPrefixMapping(String prefix) throws SAXException
    {
        wrapped.endPrefixMapping(prefix);
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException
    {
        wrapped.characters(ch, start, length);
    }

    @Override
    public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException
    {
        wrapped.ignorableWhitespace(ch, start, length);
    }

    @Override
    public void processingInstruction(String target, String data) throws SAXException
    {
        wrapped.processingInstruction(target, data);
    }

    @Override
    public void skippedEntity(String name) throws SAXException
    {
        wrapped.skippedEntity(name);
    }
}

使用这个类来读取属性很容易:

    HtmlParser.buildSpannedText("<x id=1 value=a>test<x id=2 value=b>", new HtmlParser.TagHandler()
    {
        @Override
        public boolean handleTag(boolean opening, String tag, Editable output, Attributes attributes)
        {
            if (opening && tag.equals("x"))
            {
                String id = HtmlParser.getValue(attributes, "id");
                String value = HtmlParser.getValue(attributes, "value");
            }
            return false;
        }
    });

这种方法的优点在于它允许禁用某些标签的处理,同时对其他标签使用默认处理方式,例如,您可以确保不创建ImageSpan对象:

    Spanned result = HtmlParser.buildSpannedText("<b><img src=nothing>test</b><img src=zilch>",
            new HtmlParser.TagHandler()
            {
                @Override
                public boolean handleTag(boolean opening, String tag, Editable output, Attributes attributes)
                {
                    // return true here to indicate that this tag was handled and
                    // should not be processed further
                    return tag.equals("img");
                }
            });

我刚试了一下,处理器部分真的很好用,但是内置解析器有些问题,例如 <tag attr="a & b">text</tag> 解析出来的结果是 attributes = {attr="a", _="_", b="b"},而使用 &amp; 的时候也会出现类似的问题。 - TWiStErRob
@TWiStErRob 对我来说 String testStr = "<tag attr=\"a & b\">text</tag>"; 被正确解析了。当我解析 String testStr = "<tag attr=a & b>text</tag>"; 时,我遇到了你所描述的问题,因为它并不是一个有效的 HTML。 - Juozas Kontvainis
嗯,你正在使用硬编码的Java,我从资源中读取值:<string name="..."><![CDATA[ ... <tag...> ... ]]></string>。也许在框架读取XML时有些东西丢失了。我通过在属性中使用Java标识符(例如枚举常量名称)而不是纯文本来解决了这个问题,因此它们只有一个“单词”长。 - TWiStErRob

9

根据rekire的答案,我制作了这个稍微更加健壮的解决方案,可以处理任何标签。

private TagHandler tagHandler = new TagHandler() {
    final HashMap<String, String> attributes = new HashMap<String, String>();

    private void processAttributes(final XMLReader xmlReader) {
        try {
            Field elementField = xmlReader.getClass().getDeclaredField("theNewElement");
            elementField.setAccessible(true);
            Object element = elementField.get(xmlReader);
            Field attsField = element.getClass().getDeclaredField("theAtts");
            attsField.setAccessible(true);
            Object atts = attsField.get(element);
            Field dataField = atts.getClass().getDeclaredField("data");
            dataField.setAccessible(true);
            String[] data = (String[])dataField.get(atts);
            Field lengthField = atts.getClass().getDeclaredField("length");
            lengthField.setAccessible(true);
            int len = (Integer)lengthField.get(atts);

            /**
             * MSH: Look for supported attributes and add to hash map.
             * This is as tight as things can get :)
             * The data index is "just" where the keys and values are stored. 
             */
            for(int i = 0; i < len; i++)
                attributes.put(data[i * 5 + 1], data[i * 5 + 4]);
        }
        catch (Exception e) {
            Log.d(TAG, "Exception: " + e);
        }
    }
...

在handleTag函数内部执行以下操作:
    @Override
    public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {

        processAttributes(xmlReader);
...

然后,属性将被访问如下:

attributes.get("我的属性名称");


9
这是我使用反射获取xmlReader私有属性的代码:
Field elementField = xmlReader.getClass().getDeclaredField("theNewElement");
elementField.setAccessible(true);
Object element = elementField.get(xmlReader);
Field attsField = element.getClass().getDeclaredField("theAtts");
attsField.setAccessible(true);
Object atts = attsField.get(element);
Field dataField = atts.getClass().getDeclaredField("data");
dataField.setAccessible(true);
String[] data = (String[])dataField.get(atts);
Field lengthField = atts.getClass().getDeclaredField("length");
lengthField.setAccessible(true);
int len = (Integer)lengthField.get(atts);

String myAttributeA = null;
String myAttributeB = null;

for(int i = 0; i < len; i++) {
    if("attrA".equals(data[i * 5 + 1])) {
        myAttributeA = data[i * 5 + 4];
    } else if("attrB".equals(data[i * 5 + 1])) {
        myAttributeB = data[i * 5 + 4];
    }
}

请注意,您可以将这些值放入一个映射中,但对于我的使用来说,这会增加太多的开销。

嗯,这一直在抛出java.lang.NoSuchFieldException-我做错了什么? - slott
1
@slott 有三种可能性:该字段不再存在(可能是不兼容的版本);通常情况下,您正在访问私有字段并且忘记调用 setAccessible(true) 或者它是基类的一部分,在这种情况下,您需要检查其超类。 - rekire
我搞定了 - 诀窍是不要在 handleTag 方法的第一件事情中调用它 :) 虽然感谢你的帮助 - 当有人帮助你度过生活中的艰难时刻总是很有帮助的... - slott

1

除了其他解决方案不允许您使用自定义标签之外,还有一种替代方案,但具有相同的效果:

<string name="foobar">blah <annotation customTag="1234">inside blah</annotation> more blah</string>

那么就这样读:
CharSequence annotatedText = context.getText(R.string.foobar);
// wrap, because getText returns a SpannedString, which is not mutable
CharSequence processedText = replaceCustomTags(new SpannableStringBuilder(annotatedText));

public static <T extends Spannable> T replaceCustomTags(T text) {
    Annotation[] annotations = text.getSpans(0, text.length(), Annotation.class);
    for (Annotation a : annotations) {
        String attrName = a.getKey();
        if ("customTag".equals(attrName)) {
            String attrValue = a.getValue();
            int contentStart = text.getSpanStart(a);
            int contentEnd = text.getSpanEnd(a);
            int contentFlags = text.getSpanFlags(a);
            Object newFormat1 = new StyleSpan(Typeface.BOLD);
            Object newFormat2 = new ForegroundColorSpan(Color.RED);
            text.setSpan(newFormat1, contentStart, contentEnd, contentFlags);
            text.setSpan(newFormat2, contentStart, contentEnd, contentFlags);
            text.removeSpan(a);
        }
    }
    return text;
}

根据您对自定义标签的需求,以上内容可能会对您有所帮助。如果您只想读取它们,您不需要SpannableStringBuilder,只需将getText转换为Spanned接口进行调查即可。

请注意,表示<annotation foo="bar">...</annotation>Annotation是Android内置的自API级别1以来!这又是其中一个隐藏的宝石。它每个<annotation>标签只允许一个属性,但没有任何限制阻止您嵌套多个注释以实现多个属性:

<string name="gold_admin_user"><annotation user="admin"><annotation rank="gold">$$username$$</annotation></annotation></string>

如果您使用Editable接口而不是Spannable,您还可以修改每个注释周围的内容。例如,更改上面的代码:
String attrValue = a.getValue();
text.insert(text.getSpanStart(a), attrValue);
text.insert(text.getSpanStart(a) + attrValue.length(), " ");
int contentStart = text.getSpanStart(a);

将会得到与XML中以下代码相同的结果:

blah <b><font color="#ff0000">1234 inside blah</font></b> more blah

注意的一点是,当您进行影响文本长度的修改时,跨度会移动。确保在正确的时间读取跨度起始/结束索引,最好将它们内联到方法调用中。 Editable 还允许您进行简单的搜索和替换操作:
index = TextUtils.indexOf(text, needle); // for example $$username$$ above
text.replace(index, index + needle.length(), replacement);

0

如果你只需要一个属性,vorrtex的建议实际上非常不错。为了给你一个处理的简单示例,请看这里:

<xml>Click on <user1>Johnni<user1> or <user2>Jenny<user2> to see...</<xml>

在您的自定义TagHandler中,您不使用equals而是使用indexOf

final static String USER = "user";
if(tag.indexOf(USER) == 0) {
    // Extract tag postfix.
    String postfix = tag.substring(USER.length());
    Log.d(TAG, "postfix: " + postfix);
}

然后,您可以将后缀值作为标签传递给onClick视图参数,以使其通用化。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接