Android Html解析

Posted freeCodeSunny

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Html解析相关的知识,希望对你有一定的参考价值。

       在前一篇 Android SpannableString浅析中我们采用html实现了文本处理的效果。当时设置部分的代码如下:

private void setText() {
    String originText = "#重磅消息#近日谷歌放出android N的第二个开发者预览版(Developer Preview)";

    String effect1 = "<font color='#FF0000'>#重磅消息#</font> <br> 近日谷歌放出Android " +
            "N的第二个开发者预览版<a href='http://developer.android.com/index.html'>(Developer Preview)</a>";

    String effect2 = "<font color='#303F9F'>#重磅消息#</font> 近日谷歌放出Android " +
            "N的第二个开发者预览版<a href='http://developer.android.com/index.html'>(Developer Preview)</a>";
    StringBuilder sb = new StringBuilder(originText);
    sb.append("<br><br><br><br>");
    sb.append(effect1);
    sb.append("<br><br><br><br>");
    sb.append(effect2);
    textView.setText(Html.fromHtml(sb.toString()));
    textView.setMovementMethod(LinkMovementMethod.getInstance());
}

       这里我们改变了部分文字的显示颜色,同时对另外一部分内容添加了点击事件的处理,并且加入了下划线。今天我就来看看Html代码的解析。

解析过程

       在上面的代码中我们设置的时候调用了Html.fromHtml()函数,从这个函数就可以知道这个是将一段html内容解析成TextView可以展示的内容。我们就从这开始逐步看看该类做了哪些事情?能解析的内容是什么样的?

       在调用这个函数之前,我们首先看看Html的构造函数private Html() { },可以看到被private修饰,说明在外部不能构造Html实例,我们看到代码中也没有任何返回实例的地方,因此这里的使用方式都是采取静态方法调用。

       我们来看看fromHtml()函数:

public static Spanned fromHtml(String source) {
    return fromHtml(source, null, null);
}

public static Spanned fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) {
    Parser parser = new Parser();
    try {
        parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
    } catch (org.xml.sax.SAXNotRecognizedException e) {
        // Should not happen.
        throw new RuntimeException(e);
    } catch (org.xml.sax.SAXNotSupportedException e) {
        // Should not happen.
        throw new RuntimeException(e);
    }

    HtmlToSpannedConverter converter =
        new HtmlToSpannedConverter(source, imageGetter, tagHandler, parser);
    return converter.convert();
}

       Html.fromHtml函数继续调用了三参数的romHtml(String source, ImageGetter imageGetter,TagHandler tagHandler),参数分别为数据源,图片处理,tag处理 。首先构造了一个Parser实例,Parser在org.ccil.cowan.tagsoup包下,我本地的代码是不能导航到的,这里给该代码的一个连接Parser源码

       这里我们额外说一下TagSoup,我们来看看他的官方介绍:

This is the home page of TagSoup, a SAX-compliant parser written in Java that, instead of parsing well-formed or valid XML, parses HTML as it is found in the wild: poor, nasty and brutish, though quite often far from short. TagSoup is designed for people who have to process this stuff using some semblance of a rational application design. By providing a SAX interface, it allows standard XML tools to be applied to even the worst HTML. TagSoup also includes a command-line processor that reads HTML files and can generate either clean HTML or well-formed XML that is a close approximation to XHTML.

       对应大致的意思就是:

TagSoup是Java语言编写一个解析Html的工具,他通过SAX引擎解析结构糟糕、令人抓狂的不规范HTML文档。TagSoup可以将一个HTML文档转换为结构良好的XML文档,方便开发人员对获取的HTML文档进行解析等操作。同时TagSoup提供了命令行程序,可以运行TagSoup来对HTML文档进行解析。

       构造了Parser后调用了Parser.setProperty函数,传入了schemaProperty,这里是一个字符串,还传入了HTMLSchema对象,HTMLSchema对象罗列了HTML的所有属性节点,HTMLSchema也属于TagSoup,这里不对TagSoup做过多的介绍,知道他是干什么的就好了。

       最后构造了一个HtmlToSpannedConverter实例,传入了上面传递进来的参数,数据源,imageGetter, tagHandler, parser,最后调用了HtmlToSpannedConverter的 convert函数。这里我们看这个类名就能大致知道该类做了什么操作,主要是将Html内容转换为Span对象。这里就又回到了前一篇的内容,最终处理的都是Span类型。

       这里我们先看看HtmlToSpannedConverter的构造函数都干了什么?

public HtmlToSpannedConverter(String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler,
        Parser parser) {
    mSource = source;
    mSpannableStringBuilder = new SpannableStringBuilder();
    mImageGetter = imageGetter;
    mTagHandler = tagHandler;
    mReader = parser;
}

       可以看到将传入的参数赋值给对应的变量,同时构造了一个SpannableStringBuilder对象,该对象与StringBuilder类型,StringBuilder主要是连接字符串,减少不必要的空间浪费,SpannableStringBuilder当然就是连接SpannableString,SpannableString的主要内容看前一篇 Android SpannableString浅析,我们继续看看convert函数干了什么?

public Spanned convert() {
    mReader.setContentHandler(this);
    try {
        mReader.parse(new InputSource(new StringReader(mSource)));
    } catch (IOException e) {
        // We are reading from a string. There should not be IO problems.
        throw new RuntimeException(e);
    } catch (SAXException e) {
        // TagSoup doesn't throw parse exceptions.
        throw new RuntimeException(e);
    }

    // Fix flags and range for paragraph-type markup.
    Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
    for (int i = 0; i < obj.length; i++) {
        int start = mSpannableStringBuilder.getSpanStart(obj[i]);
        int end = mSpannableStringBuilder.getSpanEnd(obj[i]);

        // If the last line of the range is blank, back off by one.
        if (end - 2 >= 0) {
            if (mSpannableStringBuilder.charAt(end - 1) == '\\n' &&
                mSpannableStringBuilder.charAt(end - 2) == '\\n') {
                end--;
            }
        }

        if (end == start) {
            mSpannableStringBuilder.removeSpan(obj[i]);
        } else {
            mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
        }
    }

    return mSpannableStringBuilder;
}

        这里首先调用了mReader. setContentHandler函数,mReader就是前面构造的Parser实例,之后调用了mReader.parse函数,将传入的source构造成一个InputSource对象。我们先去看看parse对象,之后再接着往下看。

public void parse(InputSource input) throws IOException, SAXException {
    setup();
    Reader r = getReader(input);
    theContentHandler.startDocument();
    theScanner.resetDocumentLocator(input.getPublicId(), input.getSystemId());
    if (theScanner instanceof Locator) {
        theContentHandler.setDocumentLocator((Locator) theScanner);
    }
    if (!(theSchema.getURI().equals("")))
        theContentHandler.startPrefixMapping(theSchema.getPrefix(), theSchema.getURI());
    theScanner.scan(r, this);
}

       首先调用了setUp,这里主要做一些赋值,初始化操作。接着将传入的InputSource对象转换成一个Reader对象,接着调用了ContentHander的startDocument,这里就是调用的就是HtmlToSpannedConverter的startDocument,可以看到是一个空函数,啥都没有做。 这里我们主要来看看最主要的的部分theScanner.scan(r, this),这里就不看代码了,他主要做了如下操作:

  1. 首先theScanner是HTMLScanner类型,因此这个scan是调用的HTMLScanner的scan函数,传入了Reader与ScanHander,ScanHander是在Parser中实现的。
  2. scan中读取每一个字符,出去特殊字符,对每一个字符根据statetable表进行处理
  3. 遇到某些字符时调用save方法处理,这里主要每次查看输入buffer,当大于20个字符才进行处理。调用ScanHander的pcdata函数。
  4. pcdata函数处理空白字符,之后调用rectify函数,从函数名可以知道是修正的意思。
  5. rectify函数中处理一个Element链表,因此会处理多次,最后还调用restart或者push函数,restart中也会继续调用push函数。
  6. 在push函数中我们终于见到了属性的theContentHandler,调用了theContentHandler.startElement(namespace, localName, name, e.atts());这里是在HtmlToSpannedConverter中实现的,因此这里实际上调用的是HtmlToSpannedConverter的startElement函数,startElement函数又继续调用了handleStartTag函数。

       我们来看看handleStartTag函数:

private void handleStartTag(String tag, Attributes attributes) {
    if (tag.equalsIgnoreCase("br")) {
        // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
        // so we can safely emite the linebreaks when we handle the close tag.
    } else if (tag.equalsIgnoreCase("p")) {
        handleP(mSpannableStringBuilder);
    } else if (tag.equalsIgnoreCase("div")) {
        handleP(mSpannableStringBuilder);
    } else if (tag.equalsIgnoreCase("strong")) {
        start(mSpannableStringBuilder, new Bold());
    } else if (tag.equalsIgnoreCase("b")) {
        start(mSpannableStringBuilder, new Bold());
    } else if (tag.equalsIgnoreCase("em")) {
        start(mSpannableStringBuilder, new Italic());
    } else if (tag.equalsIgnoreCase("cite")) {
        start(mSpannableStringBuilder, new Italic());
    } else if (tag.equalsIgnoreCase("dfn")) {
        start(mSpannableStringBuilder, new Italic());
    } else if (tag.equalsIgnoreCase("i")) {
        start(mSpannableStringBuilder, new Italic());
    } else if (tag.equalsIgnoreCase("big")) {
        start(mSpannableStringBuilder, new Big());
    } else if (tag.equalsIgnoreCase("small")) {
        start(mSpannableStringBuilder, new Small());
    } else if (tag.equalsIgnoreCase("font")) {
        startFont(mSpannableStringBuilder, attributes);
    } else if (tag.equalsIgnoreCase("blockquote")) {
        handleP(mSpannableStringBuilder);
        start(mSpannableStringBuilder, new Blockquote());
    } else if (tag.equalsIgnoreCase("tt")) {
        start(mSpannableStringBuilder, new Monospace());
    } else if (tag.equalsIgnoreCase("a")) {
        startA(mSpannableStringBuilder, attributes);
    } else if (tag.equalsIgnoreCase("u")) {
        start(mSpannableStringBuilder, new Underline());
    } else if (tag.equalsIgnoreCase("sup")) {
        start(mSpannableStringBuilder, new Super());
    } else if (tag.equalsIgnoreCase("sub")) {
        start(mSpannableStringBuilder, new Sub());
    } else if (tag.length() == 2 &&
               Character.toLowerCase(tag.charAt(0)) == 'h' &&
               tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
        handleP(mSpannableStringBuilder);
        start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
    } else if (tag.equalsIgnoreCase("img")) {
        startImg(mSpannableStringBuilder, attributes, mImageGetter);
    } else if (mTagHandler != null) {
        mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
    }
}

       从上述的代码中也可以看出我们能够解析html中的那些标签,这里仅仅只处理了所有Element的start标签,还没有处理end标签,start主要做对text设置了Span,初始与结束为止都是同一个,设置了一个空了类,作为标识对象,但是对于标签p,div,img等做了不同的设置,尤其是img这里主要调用了设置替换图标,这个功能是前面ImageGetter来实现的。

private static void start(SpannableStringBuilder text, Object mark) {
   int len = text.length();
   text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
}

       上面只是做了start标签,那end标签又做了上面,在前面我们说rectify调用了push进行压栈,当时略过了其他的代码,这里还做了另一项处理,当一个Element扫描完后还进行了pop出栈,pop中调用了endElement函数,这里实际调用了HtmlToSpannedConverter的endElement函数,endElement函数中又继续调用了handleEndTag函数。

       我们来看看handleEndTag函数:

private void handleEndTag(String tag) {
        if (tag.equalsIgnoreCase("br")) {
            handleBr(mSpannableStringBuilder);
        } else if (tag.equalsIgnoreCase("p")) {
            handleP(mSpannableStringBuilder);
        } else if (tag.equalsIgnoreCase("div")) {
            handleP(mSpannableStringBuilder);
        } else if (tag.equalsIgnoreCase("strong")) {
            end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
        } else if (tag.equalsIgnoreCase("b")) {
            end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
        } else if (tag.equalsIgnoreCase("em")) {
            end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
        } else if (tag.equalsIgnoreCase("cite")) {
            end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
        } else if (tag.equalsIgnoreCase("dfn")) {
            end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
        } else if (tag.equalsIgnoreCase("i")) {
            end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
        } else if (tag.equalsIgnoreCase("big")) {
            end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
        } else if (tag.equalsIgnoreCase("small")) {
            end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
        } else if (tag.equalsIgnoreCase("font")) {
            endFont(mSpannableStringBuilder);
        } else if (tag.equalsIgnoreCase("blockquote")) {
            handleP(mSpannableStringBuilder);
            end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());
        } else if (tag.equalsIgnoreCase("tt")) {
            end(mSpannableStringBuilder, Monospace.class,
                    new TypefaceSpan("monospace"));
        } else if (tag.equalsIgnoreCase("a")) {
            endA(mSpannableStringBuilder);
        } else if (tag.equalsIgnoreCase("u")) {
            end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
        } else if (tag.equalsIgnoreCase("sup")) {
            end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
        } else if (tag.equalsIgnoreCase("sub")) {
            end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
        } else if (tag.length() == 2 &&
                Character.toLowerCase(tag.charAt(0)) == 'h' &&
                tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
            handleP(mSpannableStringBuilder);
            endHeader(mSpannableStringBuilder);
        } else if (mTagHandler != null) {
            mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
        }
    }

       这里与handleStartTag成对处理,主要做了替换处理,根据stat中传入的类标识,重新设置span对象。

private static void end(SpannableStringBuilder text, Class kind, Object repl) {
    int len = text.length();
    Object obj = getLast(text, kind);
    int where = text.getSpanStart(obj);

    text.removeSpan(obj);

    if (where != len) {
       text.setSpan(repl, where, len,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
    }

       这里的kind就是前面mark,repl为重新替换的span对象,这里就看到主要可以使用如下的span对象:StyleSpan,RelativeSizeSpan,QuoteSpan,UnderlineSpan,SuperscriptSpan。可以看到他比SpannableString能够处理的东西要少很多,我们这里我们可以看到header里面进行了两种span处理。最后如果所有的节点都没有匹配,如果你自己实现了mTagHandler,则采用mTagHandler进行处理。

       这里我们再回到convert函数,parse处理完后,继续处理了mSpannableStringBuilder,循环处理设置的span,忽略’\\n’换行符,之后如果span的start与end为同一个位置,说明该节点没有任何内容处理,将该span remove掉。最后将处理完成的SpannableStringBuilder返回给TextView进行展示。

反解

       上面将html转换成了SpannableStringBuilder,Html同时还能降SpannableStringBuilder内容转换为html内容。这里主要调用toHtml函数。

public static String toHtml(Spanned text) {
    StringBuilder out = new StringBuilder();
    withinHtml(out, text);
    return out.toString();
}

private static void withinHtml(StringBuilder out, Spanned text) {
    int len = text.length();

    int next;
    for (int i = 0; i < text.length(); i = next) {
        next = text.nextSpanTransition(i, len, ParagraphStyle.class);
        ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class);
        String elements = " ";
        boolean needDiv = false;

        for(int j = 0; j < style.length; j++) {
            if (style[j] instanceof AlignmentSpan) {
                Layout.Alignment align =
                    ((AlignmentSpan) style[j]).getAlignment();
                needDiv = true;
                if (align == Layout.Alignment.ALIGN_CENTER) {
                    elements = "align=\\"center\\" " + elements;
                } else if (align == Layout.Alignment.ALIGN_OPPOSITE) {
                    elements = "align=\\"right\\" " + elements;
                } else {
                    elements = "align=\\"left\\" " + elements;
                }
            }
        }
        if (needDiv) {
            out.append("<div ").append(elements).append(">");
        }

        withinDiv(out, text, i, next);

        if (needDiv) {
            out.append("</div>");
        }
    }
}

       toHtml函数中又调用了withinHtml函数,withinHtml函数中循环处理了span对象。如果是AlignmentSpan对象则外层嵌套一层div,之后调用withinDiv继续处理。

private static void withinDiv(StringBuilder out, Spanned text, int start, int end) {
    int next;
    for (int i = start; i < end; i = next) {
        next = text.nextSpanTransition(i, end, QuoteSpan.class);
        QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);

        for (QuoteSpan quote : quotes) {
            out.append("<blockquote>");
        }

        withinBlockquote(out, text, i, next);

        for (QuoteSpan quote : quotes) {
            out.append("</blockquote>\\n");
        }
    }
}

       withinDiv中继续对文本进行处理。最终会调用withinParagraph对文本进行处理。withinParagraph处理了对应的span对象:

private static boolean withinParagraph(StringBuilder out, Spanned text, int start, int end, int nl, boolean last) {
    int next;
    for (int i = start; i < end; i = next) {
        next = text.nextSpanTransition(i, end, CharacterStyle.class);
        CharacterStyle[] style = text.getSpans(i, next, CharacterStyle.class);

        for (int j = 0; j < style.length; j++) {
            if (style[j] instanceof StyleSpan) {
                int s = ((StyleSpan) style[j]).getStyle();

                if ((s & Typeface.BOLD) != 0) {
                    out.append("<b>");
                }
                if ((s & Typeface.ITALIC) != 0) {
                    out.append("<i>");
                }
            }
            if (style[j] instanceof TypefaceSpan) {
                String s = ((TypefaceSpan) style[j]).getFamily();

                if ("monospace".equals(s)) {
                    out.append("<tt>");
                }
            }
            if (style[j] instanceof SuperscriptSpan) {
                out.append("<sup>");
            }
            if (style[j] instanceof SubscriptSpan) {
                out.append("<sub>");
            }
            if (style[j] instanceof UnderlineSpan) {
                out.append("<u>");
            }
            if (style[j] instanceof StrikethroughSpan) {
                out.append("<strike>");
            }
            if (style[j] instanceof URLSpan) {
                out.append("<a href=\\"");
                out.append(((URLSpan) style[j]).getURL());
                out.append("\\">");
            }
            if (style[j] instanceof ImageSpan) {
                out.append("<img src=\\"");
                out.append(((ImageSpan) style[j]).getSource());
                out.append("\\">");
                i = next;
            }
            if (style[j] instanceof AbsoluteSizeSpan) {
                out.append("<font size =\\"");
                out.append(((AbsoluteSizeSpan) style[j]).getSize() / 6);
                out.append("\\">");
            }
            if (style[j] instanceof ForegroundColorSpan) {
                out.append("<font color =\\"#");
                String color = Integer.toHexString(((ForegroundColorSpan)style[j]).getForegroundColor() + 0x01000000);
                while (color.length() < 6) {
                    color = "0" + color;
                }
                out.append(color);
                out.append("\\">");
            }
        }

        withinStyle(out, text, i, next);

        for (int j = style.length - 1; j >= 0; j--) {
            if (style[j] instanceof ForegroundColorSpan) {
                out.append("</font>");
            }
            if (style[j] instanceof AbsoluteSizeSpan) {
                out.append("</font>");
            }
            if (style[j] instanceof URLSpan) {
                out.append("</a>");
            }
            if (style[j] instanceof StrikethroughSpan) {
                out.append("</strike>");
            }
            if (style[j] instanceof UnderlineSpan) {
                out.append("</u>");
            }
            if (style[j] instanceof SubscriptSpan) {
                out.append("</sub>");
            }
            if (style[j] instanceof SuperscriptSpan) {
                out.append("</sup>");
            }
            if (style[j] instanceof TypefaceSpan) {
                String s = ((TypefaceSpan) style[j]).getFamily();

                if (s.equals("monospace")) {
                    out.append("</tt>");
                }
            }
            if (style[j] instanceof StyleSpan) {
                int s = ((StyleSpan) style[j]).getStyle();

                if ((s & Typeface.BOLD) != 0) {
                    out.append("</b>");
                }
                if ((s & Typeface.ITALIC) != 0) {
                    out.append("</i>");
                }
            }
        }
    }

    if (nl == 1) {
        out.append("<br>\\n");
        return false;
    } else {
        for (int i = 2; i < nl; i++) {
            out.append("<br>");
        }
        return !last;
    }
}

private static void withinStyle(StringBuilder out, CharSequence text, int start, int end) {
    for (int i = start; i < end; i++) {
        char c = text.charAt(i);

        if (c == '<') {
            out.append("&lt;");
        } else if (c == '>') {
            out.append("&gt;");
        } else if (c == '&') {
            out.append("&amp;");
        } else if (c >= 0xD800 && c <= 0xDFFF) {
            if (c < 0xDC00 && i + 1 < end) {
                char d = text.charAt(i + 1);
                if (d >= 0xDC00 && d <= 0xDFFF) {
                    i++;
                    int codepoint = 0x010000 | (int) c - 0xD800 << 10 | (int) d - 0xDC00;
                    out.append("&#").append(codepoint).append(";");
                }
            }
        } else if (c > 0x7E || c < ' ') {
            out.append("&#").append((int) c).append(";");
        } else if (c == ' ') {
            while (i + 1 < end && text.charAt(i + 1) == ' ') {
                out.append("&nbsp;");
                i++;
            }

            out.append(' ');
        } else {
            out.append(c);
        }
    }
}

       这里根据不同的span生成对应的html内容,最后将生成了html返回。

总结

       这里只是初步的解析了整个流程,其中还有很多内容可以继续如果,比如TagSoup,可以自行去看看代码。看看TagSoup是怎么解析令人发狂的html内容的。

       一般简单的效果,用html就可以了,这样相对来说代码量少,了解html的人很容易就能明白需要实现什么效果,如果有很复杂的效果或者功能,或者同一段文本需要多种效果与功能,就需要采用SpannableString来实现了。实际开发中需要根据需求来实现对应的效果。

以上是关于Android Html解析的主要内容,如果未能解决你的问题,请参考以下文章

Android 实用代码片段

Android 实用代码片段

Android课程---Android Studio使用小技巧:提取方法代码片段

android小知识点代码片段

详解Android WebView加载html片段

Android 片段和活动