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),这里就不看代码了,他主要做了如下操作:
- 首先theScanner是HTMLScanner类型,因此这个scan是调用的HTMLScanner的scan函数,传入了Reader与ScanHander,ScanHander是在Parser中实现的。
- scan中读取每一个字符,出去特殊字符,对每一个字符根据statetable表进行处理
- 遇到某些字符时调用save方法处理,这里主要每次查看输入buffer,当大于20个字符才进行处理。调用ScanHander的pcdata函数。
- pcdata函数处理空白字符,之后调用rectify函数,从函数名可以知道是修正的意思。
- rectify函数中处理一个Element链表,因此会处理多次,最后还调用restart或者push函数,restart中也会继续调用push函数。
- 在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("<");
} else if (c == '>') {
out.append(">");
} else if (c == '&') {
out.append("&");
} 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(" ");
i++;
}
out.append(' ');
} else {
out.append(c);
}
}
}
这里根据不同的span生成对应的html内容,最后将生成了html返回。
总结
这里只是初步的解析了整个流程,其中还有很多内容可以继续如果,比如TagSoup,可以自行去看看代码。看看TagSoup是怎么解析令人发狂的html内容的。
一般简单的效果,用html就可以了,这样相对来说代码量少,了解html的人很容易就能明白需要实现什么效果,如果有很复杂的效果或者功能,或者同一段文本需要多种效果与功能,就需要采用SpannableString来实现了。实际开发中需要根据需求来实现对应的效果。
以上是关于Android Html解析的主要内容,如果未能解决你的问题,请参考以下文章