import "code.google.com/p/go.net/html"
html包实现了兼容HTML5的token(标记)读取器和解析器。
文本的token化是通过使用io.Reader接口创建Tokenizer实现的。编程者有责任保证r提供utf-8编码的HTML文本。
z := html.NewTokenizer(r)
上述代码提供了Tokenizer类型值z,通过重复调用z.Next()可以获取HTML的所有token,该方法会解析下一个token,返回其类型或者一个错误。
for { tt := z.Next() if tt == html.ErrorToken { // ... return ... } // 处理当前token }
有两组API可以获取当前的token。高水平的API是Token方法,低水平的API是Text或TagName/TagAttr方法。两组API都允许在Next后,Text、Token、TagName或TagAttr之前,调用Raw方法。用EBNF范式表示每个token的合法调用序列如下:
Next {Raw} [ Token | Text | TagName {TagAttr} ]
Token方法返回完全描述一个token的独立的数据结构。(字符)实体(如"<")不会被反转义,标签名和属性的键是小写的,属性会被收集进一个[]Attribute类型字段里。例如:
for { if z.Next() == html.ErrorToken { // 返回io.EOF表示成功 return z.Err() } emitToken(z.Token()) }
低水平API会进行较少的内存申请和拷贝;但是Text、TagName、TagAttr方法返回的[]byte类型值得内容可能在下一次对Next的调用后被修改。例如,要提取一个HTML页面的锚定文本:
depth := 0 for { tt := z.Next() switch tt { case ErrorToken: return z.Err() case TextToken: if depth > 0 { // emitBytes应拷贝它接收到的文本,如果不立刻处理文本的话 emitBytes(z.Text()) } case StartTagToken, EndTagToken: tn, _ := z.TagName() if len(tn) == 1 && tn[0] == 'a' { if tt == StartTagToken { depth++ } else { depth-- } } } }
使用io.Reader调用Parse函数可以一次完成解析,并返回解析树(文档元素)的*Node类型根节点。调用者有责任保证Reader接口提供的文本时utf-8编码的。下面是一个采用深度优先顺序处理锚定节点的例子:
doc, err := html.Parse(r) if err != nil { // ... } var f func(*html.Node) f = func(n *html.Node) { if n.Type == html.ElementNode && n.Data == "a" { // 对n这样那样 } for c := n.FirstChild; c != nil; c = c.NextSibling { f(c) } } f(doc)
相关规范参见:http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html
和:http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html
var ErrBufferExceeded = errors.New("max buffer exceeded")
ErrBufferExceeded表示超出了缓冲限制。
func EscapeString(s string) string
EscapeString将特定字符进行转义,如"<"变成"<"。本函数只转义5个字符:<, >, &, '和"。
UnescapeString(EscapeString(s)) == s总是成立,但反过来这不一定。
func UnescapeString(s string) string
UnescapeString将转义实体反转义为对应字符,如"<"变成"<"。本函数会反转义远比EscapeString函数转义的字符多得多的字符实体。例如,"á"反转义为"á",字符实体"á"和"&xE1;"也会反转义为该字符。
type Attribute struct { Namespace, Key, Val string }
Attribute是属性的【名字空间-键-值】三元组。Namespace对外来属性如xlink是非空的,Key是按字符顺序的(因此不含转义字符如'&'、'<'或'>'),Val是解转义的(看起来更类似"a<b"而非"a<b")。
Namespace只用于解析器,token提取器不是使用该字段。
type TokenType uint32
TokenType表示token的类型。
const ( // ErrorToken表示解析token时出现了错误。 ErrorToken TokenType = iota // TextToken表示文本节点。 TextToken // StartTagToken表示形如<a>的token。 StartTagToken // EndTagToken表示形如</a>的token。 EndTagToken // SelfClosingTagToken表示形如<br/>的自闭合token。 SelfClosingTagToken // CommentToken表示形如<!--x-->的注释。 CommentToken // DoctypeToken表示形如<!DOCTYPE x>的文档说明。 DoctypeToken )
func (t TokenType) String() string
String返回TokenType的字符串表示。
type Token struct { Type TokenType DataAtom atom.Atom Data string Attr []Attribute }
Token包含一个TokenType类型字段和一些信息(起始和结束标签的标签名,文本、注释和文档说明的内容)。标签token还会包含一个属性的切片。所有token的Data字段都是解转义的(其内容更类似"a<b"而非"a<b")。对标签token,DataAtom字段是Data字段的atom信息,如果Data不是已知的标签名,会设置该字段为零值。
func (t Token) String() string
String返回token的字符串表示。
type Tokenizer struct {
// 内含隐藏或非导出字段
}
Tokenizer返回HTML token流。
func NewTokenizer(r io.Reader) *Tokenizer
NewTokenizer使用r创建并返回一个解析HTML的新*Tokenizer。输入应该是utf-8编码的。
func NewTokenizerFragment(r io.Reader, contextTag string) *Tokenizer
NewTokenizerFragment使用r创建并返回一个解析HTML的新*Tokenizer,以提取一个已存在元素的InnerHTML片段。contextTag是该元素的标签,如"div""或iframe"。
例如,InnerHTML片段"a<b"的token提取依赖于它是一个<p>标签还是一个<script>标签的内容。输入应该是utf-8编码的。
func (z *Tokenizer) Buffered() []byte
Buffered返回一个已经读取进缓冲但尚未处理的数据的切片。
func (z *Tokenizer) SetMaxBuf(n int)
SetMaxBuf设置提取token时缓冲中数据的数量限制。0表示没有限制。
func (z *Tokenizer) AllowCDATA(allowCDATA bool)
AllowCDATA设置z是否将<![CDATA[foo]]>识别为文本"foo"。默认值是假,表示会将该标签识别为一个伪造注释"<!-- [CDATA[foo]] -->"。
严格来说,HTML兼容的Tokenizer应该当且仅当提取外部内容(如MathML和SVG)的token时允许CDATA。然而,和解析器不同,只使用Tokenizer追踪外部内容是很困难的:一个<svg>标签可能包含一个相对SVG而非相对HTML的外部的<foreignObject>。对于严格兼容HTML5的token提取算法,恰当的调用本函数是使用者的责任。某些情况下,如果不关心MathML或SVG的CDATA是文本还是注释,忽略恰当调用本函数的任务是可以接受的。
func (z *Tokenizer) NextIsNotRawText()
NextIsNotRawText方法通知z下一个token不应被视为原始文本。一些元素,如script和title元素,一般要求起始标签后跟的token是原始文本(即不含子元素)。例如,"<title>a<b>c</b>d</title>"进行token的提取会生成起始标签:"<title>",文本:"a<b>c</b>d"和结束标签"</title>"。不会抽提出起始标签"<b>"和结束标签"</b>"。
token提取器的实现一般会在正确的时机查找原始文本。严格来说,HTML5兼容的token提取器不应在外部内容中查找原始文本:<title> 一般需要原始文本,但<svg>中的<title>不需要。另一个例子是<textarea>一般需要原始文本,但<textarea>不允许作为<select>标签的直接子元素;同常解析时,<textarea>隐含着</select>,但在解析<select>的InnerHTML时不能隐式关闭元素。 类似于AllowCDATA方法,和解析器不同,只使用Tokenizer追踪重置原始文本的时机是很困难的。对于严格兼容HTML5的token提取算法, 恰当的调用本函数是使用者的责任。某些情况下,类似AllowCDATA方法,基本使用中忽略恰当调用本函数的任务是可以接受的。
注意,此处"原始文本"的概念和Tokenizer.Raw方法返回的文本是不同的。
func (z *Tokenizer) Next() TokenType
Next扫描下一个token并返回其类型。
func (z *Tokenizer) Raw() []byte
Raw返回当前token未经修改的文本。调用Next、Token、Text、TagName/TagAttr方法可能会修改返回的切片的内容。
func (z *Tokenizer) Text() []byte
Text返回文本、注释和文档说明token的解转义的内容。返回切片的内容可能会在下一次调用Next方法时改变。
func (z *Tokenizer) TagAttr() (key, val []byte, moreAttr bool)
TagAttr返回当前标签token下一个未解析的小写的键和解转义的值,以及是否有更多属性。返回切片的内容可能在下一次调用Next方法时被修改。
func (z *Tokenizer) TagName() (name []byte, hasAttr bool)
TagName返回标签token的小写的名称(`<IMG SRC="foo">`会返回`img`)以及是否该标签具有属性。返回切片的内容可能在下一次调用Next方法时被修改。
func (z *Tokenizer) Token() Token
Token返回下一个token。返回值的Data和Attr字段值在之后调用Next方法后仍会保持合法。
func (z *Tokenizer) Err() error
Err返回最近一次ErrorToken类型token对应的错误。一般是io.EOF,表示token提取结束。
type NodeType uint32
NodeType是Node的类型。
const ( ErrorNode NodeType = iota TextNode DocumentNode ElementNode CommentNode DoctypeNode )
type Node struct { Parent, FirstChild, LastChild, PrevSibling, NextSibling *Node Type NodeType DataAtom atom.Atom Data string Namespace string Attr []Attribute }
Node类型包含一个NodeType字段和一些信息(元素节点的标签名、文本等的内容 )是Node树的一部分。元素节点还有Namespace和Attribute的切片。Data是解转义的,因此其内容更接近"a<b"而非"a<b"。对元素节点,DataAtom是Data的atom,或者零值表示未知标签名。
空的Namespace隐式表示"http://www.w3.org/1999/xhtml"名字空间。
类似的,"math" 是"http://www.w3.org/1998/Math/MathML","svg"是"http://www.w3.org/2000/svg"。
func (n *Node) AppendChild(c *Node)
AppendChild将c添加为n的子节点。如果c已经有父节点和兄弟节点,会panic。
func (n *Node) InsertBefore(newChild, oldChild *Node)
InsertBefore向n的子节点序列中oldChild的前面插入newChild作为其子节点。如果oldChild为nil,会将newChild添加到子节点序列的结尾。如果newChild已经有父节点和兄弟节点,会panic。
func (n *Node) RemoveChild(c *Node)
RemoveChild删除n的子节点c。删除后,c将没有父节点和兄弟节点。如果c不是n的子节点,会panic。
func Parse(r io.Reader) (*Node, error)
Parse函数返回从r中读取的HTML文档的解析书。输入必须是utf-8编码的。
s := `<ul><li><a href="foo">Foo</a><li><a href="/bar/baz">BarBaz</a></ul>` doc, err := html.Parse(strings.NewReader(s)) if err != nil { log.Fatal(err) } var f func(*html.Node) f = func(n *html.Node) { if n.Type == html.ElementNode && n.Data == "a" { for _, a := range n.Attr { if a.Key == "href" { fmt.Println(a.Val) break } } } for c := n.FirstChild; c != nil; c = c.NextSibling { f(c) } } f(doc)
Output:
foo /bar/baz
func ParseFragment(r io.Reader, context *Node) ([]*Node, error)
ParseFragment解析一个HTML片段,返回发现的节点。如果该片段式已存在的元素context的InnerHTML,会将解析出的元素传递给context。
func Render(w io.Writer, n *Node) error
Render将解析树n翻译后写入接口w中。
翻译是在'最大努力'模块上进行的:对Render函数生成的输出调用Parse函数总是生成某些元素和原始的解析树类似的树,但只有原始解析树是格式正确的情况下,才会也确定会保证Parse返回的解析树是n的精确克隆。格式正确并不容易说明,兼容HTML5规范。
对任意输入调用Parse函数一般总是生成格式正确的解析树。但是,也有可能生成格式错误的解析树。例如,在一个格式正确的解析树中,没有<a>元素是另一个<a>元素的子元素:解析"<a><a>"生成两个兄弟元素。类似的,格式正确的解析树中,没有<a>元素是<table>元素的子元素:解析"<p><table><a>"生成一个具有两个互为兄弟元素的子元素的<p>元素;<a>元素的父元素被重设为<table>元素的父元素。但是,对"<a><table><a>"调用Parse函数不会返回错误,但是结果会含有一个具有<a>子元素的<a>元素,导致格式错误。
从编程角度看,生成的树仍然是格式正确的。然而,有可能构造出一个看起来无害的树,但在翻译后再解析时却会生成不同的树。一个简单的例子,一个单独的文本元素会变成一个包含<html>、<head>和<body>元素的树。另一个例子, 标题等价的"a<head>b</head>c"会变成"<html><head><head/><body>abc</body></html>"。