├── LICENSE ├── README.md ├── element.go ├── error.xml ├── go.mod ├── go.sum ├── query.go ├── sample.xml ├── xmlparser.go └── xmlparser_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Tamer Gur 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xml stream parser 2 | 3 | xml-stream-parser is xml parser for GO. It is efficient to parse large xml data with streaming fashion. 4 | 5 | ## Usage 6 | 7 | ```xml 8 | 9 | 10 | 11 | The Iliad and The Odyssey 12 | 12.95 13 | 14 | Best translation I've read. 15 | I like other versions better. 16 | 17 | 18 | 19 | Anthology of World Literature 20 | 24.95 21 | 22 | Needs more modern literature. 23 | Excellent overview of world literature. 24 | 25 | 26 | 27 | Journal of XML parsing 28 | 1 29 | 30 | 31 | ``` 32 | 33 | **Stream** over books and journals 34 | 35 | ```go 36 | f, _ := os.Open("input.xml") 37 | br := bufio.NewReaderSize(f,65536) 38 | parser := xmlparser.NewXMLParser(br, "book", "journal") 39 | 40 | for xml := range parser.Stream() { 41 | fmt.Println(xml.Childs["title"][0].InnerText) 42 | if xml.Name == "book" { 43 | fmt.Println(xml.Childs["comments"][0].Childs["userComment"][0].Attrs["rating"]) 44 | fmt.Println(xml.Childs["comments"][0].Childs["userComment"][0].InnerText) 45 | } 46 | } 47 | ``` 48 | 49 | **Skip** tags for speed 50 | 51 | ```go 52 | parser := xmlparser.NewXMLParser(br, "book").SkipElements([]string{"price", "comments"}) 53 | ``` 54 | 55 | **Attributes** only 56 | 57 | ```go 58 | parser := xmlparser.NewXMLParser(br, "bookstore", "book").ParseAttributesOnly("bookstore") 59 | ``` 60 | 61 | **Error** handlings 62 | 63 | ```go 64 | for xml := range parser.Stream() { 65 | if xml.Err !=nil { 66 | // handle error 67 | } 68 | } 69 | ``` 70 | 71 | **Progress** of parsing 72 | 73 | ```go 74 | // total byte read to calculate the progress of parsing 75 | parser.TotalReadSize 76 | ``` 77 | 78 | **Xpath** query provides alternative to default fast access for different usecases 79 | ```go 80 | 81 | parser := xmlparser.NewXMLParser(bufreader, "bookstore").EnableXpath() 82 | 83 | for xml := range p.Stream() { 84 | // select books 85 | xml.SelectElements("//book") 86 | xml.SelectElements("./book") 87 | xml.SelectElements("book") 88 | // select titles 89 | xml.SelectElements("./book/title") 90 | // select book with price condition 91 | xml.SelectElements("//book[price>=20.95]")) 92 | //comments with rating 4 93 | xml.SelectElements("//book/comments/userComment[@rating='4']") 94 | } 95 | // for evaluate function or reuse existing xpath expression 96 | // sum of all the book price 97 | expr, err := p.CompileXpath("sum(//book/price)") 98 | price := expr.Evaluate(p.CreateXPathNavigator(xml)).(float64) 99 | 100 | ``` 101 | xpath functionality implemented via [xpath](https://github.com/antchfx/xpath) library check more 102 | examples in its documentation 103 | 104 | If you interested check also [json parser](https://github.com/tamerh/jsparser) which works similarly 105 | -------------------------------------------------------------------------------- /element.go: -------------------------------------------------------------------------------- 1 | package xmlparser 2 | 3 | type XMLElement struct { 4 | Name string 5 | Attrs map[string]string 6 | InnerText string 7 | Childs map[string][]XMLElement 8 | Err error 9 | // filled when xpath enabled 10 | childs []*XMLElement 11 | parent *XMLElement 12 | attrs []*xmlAttr 13 | localName string 14 | prefix string 15 | } 16 | 17 | type xmlAttr struct { 18 | name string 19 | value string 20 | } 21 | 22 | // SelectElements finds child elements with the specified xpath expression. 23 | func (n *XMLElement) SelectElements(exp string) ([]*XMLElement, error) { 24 | return find(n, exp) 25 | } 26 | 27 | // SelectElement finds child elements with the specified xpath expression. 28 | func (n *XMLElement) SelectElement(exp string) (*XMLElement, error) { 29 | return findOne(n, exp) 30 | } 31 | 32 | func (n *XMLElement) FirstChild() *XMLElement { 33 | if len(n.childs) > 0 { 34 | return n.childs[0] 35 | } 36 | return nil 37 | } 38 | 39 | func (n *XMLElement) LastChild() *XMLElement { 40 | if l := len(n.childs); l > 0 { 41 | return n.childs[l-1] 42 | } 43 | return nil 44 | } 45 | 46 | func (n *XMLElement) PrevSibling() *XMLElement { 47 | if n.parent != nil { 48 | for i, c := range n.parent.childs { 49 | if c == n { 50 | if i >= 0 { 51 | return n.parent.childs[i-1] 52 | } 53 | return nil 54 | } 55 | } 56 | } 57 | return nil 58 | } 59 | 60 | func (n *XMLElement) NextSibling() *XMLElement { 61 | if n.parent != nil { 62 | for i, c := range n.parent.childs { 63 | if c == n { 64 | if i+1 < len(n.parent.childs) { 65 | return n.parent.childs[i+1] 66 | } 67 | return nil 68 | } 69 | } 70 | } 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /error.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | InnerText110 4 | InnerText111 5 | 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tamerh/xml-stream-parser 2 | 3 | go 1.12 4 | 5 | require github.com/tamerh/xpath v1.0.0 // indirect 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/tamerh/xpath v1.0.0 h1:NccMES/Ej8slPCFDff73Kf6V1xu9hdbuKf2RyDsxf5Q= 2 | github.com/tamerh/xpath v1.0.0/go.mod h1:t0wnh72FQlOVEO20f2Dl3EoVxso9GnLREh1WTpvNmJQ= 3 | -------------------------------------------------------------------------------- /query.go: -------------------------------------------------------------------------------- 1 | package xmlparser 2 | 3 | import ( 4 | "github.com/tamerh/xpath" 5 | ) 6 | 7 | // CreateXPathNavigator creates a new xpath.NodeNavigator for the specified html.Node. 8 | func (x *XMLParser) CreateXPathNavigator(top *XMLElement) *XmlNodeNavigator { 9 | return &XmlNodeNavigator{curr: top, root: top, attr: -1} 10 | } 11 | 12 | // Compile the given xpath expression 13 | func (x *XMLParser) CompileXpath(expr string) (*xpath.Expr, error) { 14 | 15 | exp, err := xpath.Compile(expr) 16 | if err != nil { 17 | return nil, err 18 | } 19 | return exp, nil 20 | 21 | } 22 | 23 | // CreateXPathNavigator creates a new xpath.NodeNavigator for the specified html.Node. 24 | func createXPathNavigator(top *XMLElement) *XmlNodeNavigator { 25 | return &XmlNodeNavigator{curr: top, root: top, attr: -1} 26 | } 27 | 28 | type XmlNodeNavigator struct { 29 | root, curr *XMLElement 30 | attr int 31 | } 32 | 33 | // Find searches the Node that matches by the specified XPath expr. 34 | func find(top *XMLElement, expr string) ([]*XMLElement, error) { 35 | exp, err := xpath.Compile(expr) 36 | if err != nil { 37 | return []*XMLElement{}, err 38 | } 39 | t := exp.Select(createXPathNavigator(top)) 40 | var elems []*XMLElement 41 | for t.MoveNext() { 42 | elems = append(elems, t.Current().(*XmlNodeNavigator).curr) 43 | } 44 | return elems, nil 45 | } 46 | 47 | // FindOne searches the Node that matches by the specified XPath expr, 48 | // and returns first element of matched. 49 | func findOne(top *XMLElement, expr string) (*XMLElement, error) { 50 | exp, err := xpath.Compile(expr) 51 | if err != nil { 52 | return nil, err 53 | } 54 | t := exp.Select(createXPathNavigator(top)) 55 | var elem *XMLElement 56 | if t.MoveNext() { 57 | elem = t.Current().(*XmlNodeNavigator).curr //getCurrentNode(t) 58 | } 59 | return elem, nil 60 | } 61 | 62 | func (x *XmlNodeNavigator) Current() *XMLElement { 63 | return x.curr 64 | } 65 | 66 | func (x *XmlNodeNavigator) NodeType() xpath.NodeType { 67 | 68 | if x.curr == x.root { 69 | return xpath.RootNode 70 | } 71 | if x.attr != -1 { 72 | return xpath.AttributeNode 73 | } 74 | return xpath.ElementNode 75 | } 76 | 77 | func (x *XmlNodeNavigator) LocalName() string { 78 | if x.attr != -1 { 79 | return x.curr.attrs[x.attr].name 80 | } 81 | 82 | return x.curr.localName 83 | 84 | } 85 | 86 | func (x *XmlNodeNavigator) Prefix() string { 87 | 88 | return x.curr.prefix 89 | 90 | } 91 | 92 | func (x *XmlNodeNavigator) Value() string { 93 | 94 | if x.attr != -1 { 95 | return x.curr.attrs[x.attr].value 96 | } 97 | return x.curr.InnerText 98 | 99 | } 100 | 101 | func (x *XmlNodeNavigator) Copy() xpath.NodeNavigator { 102 | n := *x 103 | return &n 104 | } 105 | 106 | func (x *XmlNodeNavigator) MoveToRoot() { 107 | x.curr = x.root 108 | } 109 | 110 | func (x *XmlNodeNavigator) MoveToParent() bool { 111 | if x.attr != -1 { 112 | x.attr = -1 113 | return true 114 | } else if node := x.curr.parent; node != nil { 115 | x.curr = node 116 | return true 117 | } 118 | return false 119 | } 120 | 121 | func (x *XmlNodeNavigator) MoveToNextAttribute() bool { 122 | if x.attr >= len(x.curr.attrs)-1 { 123 | return false 124 | } 125 | x.attr++ 126 | return true 127 | } 128 | 129 | func (x *XmlNodeNavigator) MoveToChild() bool { 130 | if node := x.curr.FirstChild(); node != nil { 131 | x.curr = node 132 | return true 133 | } 134 | return false 135 | } 136 | 137 | func (x *XmlNodeNavigator) MoveToFirst() bool { 138 | if x.curr.parent != nil { 139 | node := x.curr.parent.FirstChild() 140 | if node != nil { 141 | x.curr = node 142 | return true 143 | } 144 | } 145 | return false 146 | } 147 | 148 | func (x *XmlNodeNavigator) MoveToPrevious() bool { 149 | node := x.curr.PrevSibling() 150 | if node != nil { 151 | x.curr = node 152 | return true 153 | } 154 | return false 155 | } 156 | 157 | func (x *XmlNodeNavigator) MoveToNext() bool { 158 | node := x.curr.NextSibling() 159 | if node != nil { 160 | x.curr = node 161 | return true 162 | } 163 | return false 164 | } 165 | 166 | func (x *XmlNodeNavigator) String() string { 167 | return x.Value() 168 | } 169 | 170 | func (x *XmlNodeNavigator) MoveTo(other xpath.NodeNavigator) bool { 171 | node, ok := other.(*XmlNodeNavigator) 172 | if !ok || node.root != x.root { 173 | return false 174 | } 175 | 176 | x.curr = node.curr 177 | x.attr = node.attr 178 | return true 179 | } 180 | -------------------------------------------------------------------------------- /sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | ]> 9 | 10 | 11 | 12 | Hello 13 | InnerText111 14 | 15 | InnerText13 16 | 17 | 18 | 19 | 20 | 21 | InnerText2 22 | 24 | InnerText213 25 | 26 | 27 | 28 | 29 | tag31 30 | tag32 31 | 32 | SkipTag 33 | 34 | 35 | InnerText0 36 | 37 | 38 | SkipTag 39 | 40 | 41 | -------------------------------------------------------------------------------- /xmlparser.go: -------------------------------------------------------------------------------- 1 | package xmlparser 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "strings" 7 | "unicode/utf8" 8 | ) 9 | 10 | type XMLParser struct { 11 | reader *bufio.Reader 12 | loopElements map[string]bool 13 | resultChannel chan *XMLElement 14 | skipElements map[string]bool 15 | attrOnlyElements map[string]bool 16 | skipOuterElements bool 17 | xpathEnabled bool 18 | scratch *scratch 19 | scratch2 *scratch 20 | TotalReadSize uint64 21 | } 22 | 23 | func NewXMLParser(reader *bufio.Reader, loopElements ...string) *XMLParser { 24 | 25 | x := &XMLParser{ 26 | reader: reader, 27 | loopElements: map[string]bool{}, 28 | attrOnlyElements: map[string]bool{}, 29 | resultChannel: make(chan *XMLElement, 256), 30 | skipElements: map[string]bool{}, 31 | scratch: &scratch{data: make([]byte, 1024)}, 32 | scratch2: &scratch{data: make([]byte, 1024)}, 33 | } 34 | 35 | // Register loop elements 36 | for _, e := range loopElements { 37 | x.loopElements[e] = true 38 | } 39 | 40 | return x 41 | } 42 | 43 | func (x *XMLParser) SkipElements(skipElements []string) *XMLParser { 44 | 45 | if len(skipElements) > 0 { 46 | for _, s := range skipElements { 47 | x.skipElements[s] = true 48 | } 49 | } 50 | return x 51 | 52 | } 53 | 54 | func (x *XMLParser) ParseAttributesOnly(loopElements ...string) *XMLParser { 55 | for _, e := range loopElements { 56 | x.attrOnlyElements[e] = true 57 | } 58 | return x 59 | } 60 | 61 | // by default skip elements works for stream elements childs 62 | // if this method called parser skip also outer elements 63 | func (x *XMLParser) SkipOuterElements() *XMLParser { 64 | 65 | x.skipOuterElements = true 66 | return x 67 | 68 | } 69 | 70 | func (x *XMLParser) EnableXpath() *XMLParser { 71 | 72 | x.xpathEnabled = true 73 | return x 74 | 75 | } 76 | 77 | func (x *XMLParser) Stream() chan *XMLElement { 78 | 79 | go x.parse() 80 | 81 | return x.resultChannel 82 | 83 | } 84 | 85 | func (x *XMLParser) parse() { 86 | 87 | defer close(x.resultChannel) 88 | var element *XMLElement 89 | var tagClosed bool 90 | var err error 91 | var b byte 92 | var iscomment bool 93 | 94 | err = x.skipDeclerations() 95 | 96 | if err != nil { 97 | x.sendError() 98 | return 99 | } 100 | 101 | for { 102 | b, err = x.readByte() 103 | 104 | if err != nil { 105 | return 106 | } 107 | 108 | if x.isWS(b) { 109 | continue 110 | } 111 | 112 | if b == '<' { 113 | 114 | iscdata, _, err := x.isCDATA() 115 | 116 | if err != nil { 117 | x.sendError() 118 | return 119 | } 120 | if iscdata { 121 | continue 122 | } 123 | 124 | iscomment, err = x.isComment() 125 | 126 | if err != nil { 127 | x.sendError() 128 | return 129 | } 130 | 131 | if iscomment { 132 | continue 133 | } 134 | 135 | element, tagClosed, err = x.startElement() 136 | 137 | if err != nil { 138 | x.sendError() 139 | return 140 | } 141 | 142 | if _, found := x.loopElements[element.Name]; found { 143 | if tagClosed { 144 | x.resultChannel <- element 145 | continue 146 | } 147 | 148 | if _, ok := x.attrOnlyElements[element.Name]; !ok { 149 | element = x.getElementTree(element) 150 | } 151 | x.resultChannel <- element 152 | if element.Err != nil { 153 | return 154 | } 155 | } else if x.skipOuterElements { 156 | 157 | if _, ok := x.skipElements[element.Name]; ok && !tagClosed { 158 | 159 | err = x.skipElement(element.Name) 160 | if err != nil { 161 | x.sendError() 162 | return 163 | } 164 | continue 165 | 166 | } 167 | 168 | } 169 | 170 | } 171 | } 172 | 173 | } 174 | 175 | func (x *XMLParser) getElementTree(result *XMLElement) *XMLElement { 176 | 177 | if result.Err != nil { 178 | return result 179 | } 180 | 181 | var cur byte 182 | var next byte 183 | var err error 184 | var element *XMLElement 185 | var tagClosed bool 186 | x.scratch2.reset() // this hold the inner text 187 | var iscomment bool 188 | 189 | for { 190 | 191 | cur, err = x.readByte() 192 | 193 | if err != nil { 194 | result.Err = err 195 | return result 196 | } 197 | 198 | if cur == '<' { 199 | 200 | iscdata, cddata, err := x.isCDATA() 201 | 202 | if err != nil { 203 | result.Err = err 204 | return result 205 | } 206 | if iscdata { 207 | for _, cd := range cddata { 208 | x.scratch2.add(cd) 209 | } 210 | continue 211 | } 212 | 213 | iscomment, err = x.isComment() 214 | 215 | if err != nil { 216 | result.Err = err 217 | return result 218 | } 219 | 220 | if iscomment { 221 | continue 222 | } 223 | 224 | next, err = x.readByte() 225 | 226 | if err != nil { 227 | result.Err = err 228 | return result 229 | } 230 | 231 | if next == '/' { // close tag 232 | tag, err := x.closeTagName() 233 | 234 | if err != nil { 235 | result.Err = err 236 | return result 237 | } 238 | 239 | if tag == result.Name { 240 | if len(result.Childs) == 0 { 241 | result.InnerText = string(x.scratch2.bytes()) 242 | } 243 | return result 244 | } 245 | } else { 246 | x.unreadByte() 247 | } 248 | 249 | element, tagClosed, err = x.startElement() 250 | 251 | if err != nil { 252 | result.Err = err 253 | return result 254 | } 255 | 256 | if _, ok := x.skipElements[element.Name]; ok && !tagClosed { 257 | err = x.skipElement(element.Name) 258 | if err != nil { 259 | result.Err = err 260 | return result 261 | } 262 | continue 263 | } 264 | if !tagClosed { 265 | element = x.getElementTree(element) 266 | } 267 | 268 | if x.xpathEnabled { 269 | element.parent = result 270 | } 271 | 272 | if _, ok := result.Childs[element.Name]; ok { 273 | result.Childs[element.Name] = append(result.Childs[element.Name], *element) 274 | if x.xpathEnabled { 275 | result.childs = append(result.childs, element) 276 | } 277 | } else { 278 | var childs []XMLElement 279 | childs = append(childs, *element) 280 | if result.Childs == nil { 281 | result.Childs = map[string][]XMLElement{} 282 | } 283 | result.Childs[element.Name] = childs 284 | 285 | if x.xpathEnabled { 286 | result.childs = append(result.childs, element) 287 | } 288 | 289 | } 290 | 291 | } else { 292 | x.scratch2.add(cur) 293 | } 294 | 295 | } 296 | } 297 | 298 | func (x *XMLParser) skipElement(elname string) error { 299 | 300 | var c byte 301 | var next byte 302 | var err error 303 | var curname string 304 | for { 305 | 306 | c, err = x.readByte() 307 | 308 | if err != nil { 309 | return err 310 | } 311 | if c == '<' { 312 | 313 | next, err = x.readByte() 314 | 315 | if err != nil { 316 | return err 317 | } 318 | 319 | if next == '/' { 320 | curname, err = x.closeTagName() 321 | if err != nil { 322 | return err 323 | } 324 | if curname == elname { 325 | return nil 326 | } 327 | } 328 | 329 | } 330 | 331 | } 332 | } 333 | 334 | func (x *XMLParser) startElement() (*XMLElement, bool, error) { 335 | 336 | x.scratch.reset() 337 | 338 | var cur byte 339 | var prev byte 340 | var err error 341 | var result = &XMLElement{} 342 | // a tag have 3 forms * ** *** 343 | var attr string 344 | var attrVal string 345 | for { 346 | 347 | cur, err = x.readByte() 348 | 349 | if err != nil { 350 | return nil, false, x.defaultError() 351 | } 352 | 353 | if x.isWS(cur) { 354 | result.Name = string(x.scratch.bytes()) 355 | 356 | if x.xpathEnabled { 357 | names := strings.Split(result.Name, ":") 358 | if len(names) > 1 { 359 | result.prefix = names[0] 360 | result.localName = names[1] 361 | } else { 362 | result.localName = names[0] 363 | } 364 | } 365 | 366 | x.scratch.reset() 367 | goto search_close_tag 368 | } 369 | 370 | if cur == '>' { 371 | if prev == '/' { 372 | result.Name = string(x.scratch.bytes()[:len(x.scratch.bytes())-1]) 373 | 374 | if x.xpathEnabled { 375 | names := strings.Split(result.Name, ":") 376 | if len(names) > 1 { 377 | result.prefix = names[0] 378 | result.localName = names[1] 379 | } else { 380 | result.localName = names[0] 381 | } 382 | } 383 | 384 | return result, true, nil 385 | } 386 | result.Name = string(x.scratch.bytes()) 387 | 388 | if x.xpathEnabled { 389 | names := strings.Split(result.Name, ":") 390 | if len(names) > 1 { 391 | result.prefix = names[0] 392 | result.localName = names[1] 393 | } else { 394 | result.localName = names[0] 395 | } 396 | } 397 | 398 | return result, false, nil 399 | } 400 | x.scratch.add(cur) 401 | prev = cur 402 | } 403 | 404 | search_close_tag: 405 | for { 406 | 407 | cur, err = x.readByte() 408 | 409 | if err != nil { 410 | return nil, false, x.defaultError() 411 | } 412 | 413 | if x.isWS(cur) { 414 | continue 415 | } 416 | 417 | if cur == '=' { 418 | if result.Attrs == nil { 419 | result.Attrs = map[string]string{} 420 | } 421 | 422 | cur, err = x.readByte() 423 | 424 | if err != nil { 425 | return nil, false, x.defaultError() 426 | } 427 | 428 | for x.isWS(cur) { 429 | cur, err = x.readByte() 430 | if err != nil { 431 | return nil, false, x.defaultError() 432 | } 433 | } 434 | 435 | if !(cur == '"' || cur == '\'') { 436 | return nil, false, x.defaultError() 437 | } 438 | 439 | attr = string(x.scratch.bytes()) 440 | attrVal, err = x.string(cur) 441 | if err != nil { 442 | return nil, false, x.defaultError() 443 | } 444 | result.Attrs[attr] = attrVal 445 | if x.xpathEnabled { 446 | result.attrs = append(result.attrs, &xmlAttr{name: attr, value: attrVal}) 447 | } 448 | x.scratch.reset() 449 | continue 450 | } 451 | 452 | if cur == '>' { //if tag name not found 453 | if prev == '/' { //tag special close 454 | return result, true, nil 455 | } 456 | return result, false, nil 457 | } 458 | 459 | x.scratch.add(cur) 460 | prev = cur 461 | 462 | } 463 | 464 | } 465 | 466 | func (x *XMLParser) isComment() (bool, error) { 467 | 468 | var c byte 469 | var err error 470 | 471 | c, err = x.readByte() 472 | 473 | if err != nil { 474 | return false, err 475 | } 476 | 477 | if c != '!' { 478 | x.unreadByte() 479 | return false, nil 480 | } 481 | 482 | var d, e byte 483 | 484 | d, err = x.readByte() 485 | 486 | if err != nil { 487 | return false, err 488 | } 489 | 490 | e, err = x.readByte() 491 | 492 | if err != nil { 493 | return false, err 494 | } 495 | 496 | if d != '-' || e != '-' { 497 | err = x.defaultError() 498 | return false, err 499 | } 500 | 501 | // skip part 502 | x.scratch.reset() 503 | for { 504 | 505 | c, err = x.readByte() 506 | 507 | if err != nil { 508 | return false, err 509 | } 510 | 511 | if c == '>' && len(x.scratch.bytes()) > 1 && x.scratch.bytes()[len(x.scratch.bytes())-1] == '-' && x.scratch.bytes()[len(x.scratch.bytes())-2] == '-' { 512 | return true, nil 513 | } 514 | 515 | x.scratch.add(c) 516 | 517 | } 518 | 519 | } 520 | 521 | func (x *XMLParser) isCDATA() (bool, []byte, error) { 522 | 523 | var c byte 524 | var b []byte 525 | var err error 526 | 527 | b, err = x.reader.Peek(2) 528 | 529 | if err != nil { 530 | return false, nil, err 531 | } 532 | 533 | if b[0] != '!' { 534 | return false, nil, nil 535 | } 536 | 537 | if err != nil { 538 | return false, nil, err 539 | } 540 | 541 | if b[1] != '[' { 542 | // this means this is not CDDATA either comment or or invalid xml which will be check during isComment 543 | return false, nil, nil 544 | } 545 | 546 | // read peaked byte 547 | _, err = x.readByte() 548 | 549 | if err != nil { 550 | return false, nil, err 551 | } 552 | 553 | _, err = x.readByte() 554 | 555 | if err != nil { 556 | return false, nil, err 557 | } 558 | 559 | c, err = x.readByte() 560 | 561 | if err != nil { 562 | return false, nil, err 563 | } 564 | 565 | if c != 'C' { 566 | err = x.defaultError() 567 | return false, nil, err 568 | } 569 | 570 | c, err = x.readByte() 571 | 572 | if err != nil { 573 | return false, nil, err 574 | } 575 | 576 | if c != 'D' { 577 | err = x.defaultError() 578 | return false, nil, err 579 | } 580 | 581 | c, err = x.readByte() 582 | 583 | if err != nil { 584 | return false, nil, err 585 | } 586 | 587 | if c != 'A' { 588 | err = x.defaultError() 589 | return false, nil, err 590 | } 591 | 592 | c, err = x.readByte() 593 | 594 | if err != nil { 595 | return false, nil, err 596 | } 597 | 598 | if c != 'T' { 599 | err = x.defaultError() 600 | return false, nil, err 601 | } 602 | 603 | c, err = x.readByte() 604 | 605 | if err != nil { 606 | return false, nil, err 607 | } 608 | 609 | if c != 'A' { 610 | err = x.defaultError() 611 | return false, nil, err 612 | } 613 | 614 | c, err = x.readByte() 615 | 616 | if err != nil { 617 | return false, nil, err 618 | } 619 | 620 | if c != '[' { 621 | err = x.defaultError() 622 | return false, nil, err 623 | } 624 | 625 | // this is possibly cdata // ]]> 626 | x.scratch.reset() 627 | for { 628 | 629 | c, err = x.readByte() 630 | 631 | if err != nil { 632 | return false, nil, err 633 | } 634 | 635 | if c == '>' && len(x.scratch.bytes()) > 1 && x.scratch.bytes()[len(x.scratch.bytes())-1] == ']' && x.scratch.bytes()[len(x.scratch.bytes())-2] == ']' { 636 | return true, x.scratch.bytes()[:len(x.scratch.bytes())-2], nil 637 | } 638 | 639 | x.scratch.add(c) 640 | 641 | } 642 | 643 | } 644 | 645 | func (x *XMLParser) skipDeclerations() error { 646 | 647 | var a, b []byte 648 | var c, d byte 649 | var err error 650 | 651 | scan_declartions: 652 | for { 653 | 654 | // when identifying a xml declaration we need to know 2 bytes ahead. Unread works 1 byte at a time so we use Peek and read together. 655 | a, err = x.reader.Peek(1) 656 | 657 | if err != nil { 658 | return err 659 | } 660 | 661 | if a[0] == '<' { 662 | 663 | b, err = x.reader.Peek(2) 664 | 665 | if err != nil { 666 | return err 667 | } 668 | 669 | if b[1] == '!' || b[1] == '?' { // either comment or decleration 670 | 671 | // read 2 peaked byte 672 | _, err = x.readByte() 673 | 674 | if err != nil { 675 | return err 676 | } 677 | 678 | _, err = x.readByte() 679 | if err != nil { 680 | return err 681 | } 682 | 683 | c, err = x.readByte() 684 | 685 | if err != nil { 686 | return err 687 | } 688 | 689 | d, err = x.readByte() 690 | 691 | if err != nil { 692 | return err 693 | } 694 | 695 | if c == '-' && d == '-' { 696 | goto skipComment 697 | } else { 698 | goto skipDecleration 699 | } 700 | 701 | } else { // declerations ends. 702 | 703 | return nil 704 | 705 | } 706 | 707 | } 708 | 709 | // read peaked byte 710 | _, err = x.readByte() 711 | 712 | if err != nil { 713 | return err 714 | } 715 | 716 | } 717 | 718 | skipComment: 719 | x.scratch.reset() 720 | for { 721 | 722 | c, err = x.readByte() 723 | 724 | if err != nil { 725 | return err 726 | } 727 | 728 | if c == '>' && len(x.scratch.bytes()) > 1 && x.scratch.bytes()[len(x.scratch.bytes())-1] == '-' && x.scratch.bytes()[len(x.scratch.bytes())-2] == '-' { 729 | goto scan_declartions 730 | } 731 | 732 | x.scratch.add(c) 733 | 734 | } 735 | 736 | skipDecleration: 737 | depth := 1 738 | for { 739 | 740 | c, err = x.readByte() 741 | 742 | if err != nil { 743 | return err 744 | } 745 | 746 | if c == '>' { 747 | depth-- 748 | if depth == 0 { 749 | goto scan_declartions 750 | } 751 | continue 752 | } 753 | if c == '<' { 754 | depth++ 755 | } 756 | 757 | } 758 | 759 | } 760 | 761 | func (x *XMLParser) closeTagName() (string, error) { 762 | 763 | x.scratch.reset() 764 | var c byte 765 | var err error 766 | for { 767 | c, err = x.readByte() 768 | 769 | if err != nil { 770 | return "", err 771 | } 772 | 773 | if c == '>' { 774 | return string(x.scratch.bytes()), nil 775 | } 776 | if !x.isWS(c) { 777 | x.scratch.add(c) 778 | } 779 | } 780 | } 781 | 782 | func (x *XMLParser) readByte() (byte, error) { 783 | 784 | by, err := x.reader.ReadByte() 785 | 786 | x.TotalReadSize++ 787 | 788 | if err != nil { 789 | return 0, err 790 | } 791 | return by, nil 792 | 793 | } 794 | 795 | func (x *XMLParser) unreadByte() error { 796 | 797 | err := x.reader.UnreadByte() 798 | if err != nil { 799 | return err 800 | } 801 | x.TotalReadSize = x.TotalReadSize - 1 802 | return nil 803 | 804 | } 805 | 806 | func (x *XMLParser) isWS(in byte) bool { 807 | 808 | if in == ' ' || in == '\n' || in == '\t' || in == '\r' { 809 | return true 810 | } 811 | 812 | return false 813 | 814 | } 815 | 816 | func (x *XMLParser) sendError() { 817 | err := fmt.Errorf("Invalid xml") 818 | x.resultChannel <- &XMLElement{Err: err} 819 | } 820 | 821 | func (x *XMLParser) defaultError() error { 822 | err := fmt.Errorf("Invalid xml") 823 | return err 824 | } 825 | 826 | func (x *XMLParser) string(start byte) (string, error) { 827 | 828 | x.scratch.reset() 829 | 830 | var err error 831 | var c byte 832 | for { 833 | 834 | c, err = x.readByte() 835 | if err != nil { 836 | if err != nil { 837 | return "", err 838 | } 839 | } 840 | 841 | if c == start { 842 | return string(x.scratch.bytes()), nil 843 | } 844 | 845 | x.scratch.add(c) 846 | 847 | } 848 | 849 | } 850 | 851 | // scratch taken from 852 | // https://github.com/bcicen/jstream 853 | type scratch struct { 854 | data []byte 855 | fill int 856 | } 857 | 858 | // reset scratch buffer 859 | func (s *scratch) reset() { s.fill = 0 } 860 | 861 | // bytes returns the written contents of scratch buffer 862 | func (s *scratch) bytes() []byte { return s.data[0:s.fill] } 863 | 864 | // grow scratch buffer 865 | func (s *scratch) grow() { 866 | ndata := make([]byte, cap(s.data)*2) 867 | copy(ndata, s.data[:]) 868 | s.data = ndata 869 | } 870 | 871 | // append single byte to scratch buffer 872 | func (s *scratch) add(c byte) { 873 | if s.fill+1 >= cap(s.data) { 874 | s.grow() 875 | } 876 | 877 | s.data[s.fill] = c 878 | s.fill++ 879 | } 880 | 881 | // append encoded rune to scratch buffer 882 | func (s *scratch) addRune(r rune) int { 883 | if s.fill+utf8.UTFMax >= cap(s.data) { 884 | s.grow() 885 | } 886 | 887 | n := utf8.EncodeRune(s.data[s.fill:], r) 888 | s.fill += n 889 | return n 890 | } 891 | -------------------------------------------------------------------------------- /xmlparser_test.go: -------------------------------------------------------------------------------- 1 | package xmlparser 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "os" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func getparser(prop ...string) *XMLParser { 13 | 14 | return getparserFile("sample.xml", prop...) 15 | } 16 | 17 | func getparserFile(filename string, prop ...string) *XMLParser { 18 | 19 | file, _ := os.Open(filename) 20 | 21 | br := bufio.NewReader(file) 22 | 23 | p := NewXMLParser(br, prop...) 24 | 25 | return p 26 | 27 | } 28 | 29 | func TestBasics(t *testing.T) { 30 | 31 | p := getparser("tag1") 32 | 33 | var results []*XMLElement 34 | for xml := range p.Stream() { 35 | results = append(results, xml) 36 | } 37 | if len(results) != 2 { 38 | panic("Test failed result must be 2") 39 | } 40 | 41 | if len(results[0].Childs) != 4 || len(results[1].Childs) != 4 { 42 | panic("Test failed") 43 | } 44 | // result 1 45 | if results[0].Attrs["att1"] != "" || results[0].Attrs["att2"] != "att0" { 46 | panic("Test failed") 47 | } 48 | 49 | if results[0].Childs["tag11"][0].Attrs["att1"] != "att0" { 50 | panic("Test failed") 51 | } 52 | 53 | if results[0].Childs["tag11"][0].InnerText != "Hello 你好 Gür" { 54 | panic("Test failed") 55 | } 56 | 57 | if results[0].Childs["tag11"][1].InnerText != "InnerText111" { 58 | panic("Test failed") 59 | } 60 | 61 | if results[0].Childs["tag12"][0].Attrs["att1"] != "att0" { 62 | panic("Test failed") 63 | } 64 | 65 | if results[0].Childs["tag12"][0].InnerText != "" { 66 | panic("Test failed") 67 | } 68 | 69 | if results[0].Childs["tag13"][0].Attrs != nil && results[0].Childs["tag13"][0].InnerText != "InnerText13" { 70 | panic("Test failed") 71 | } 72 | 73 | if results[0].Childs["tag14"][0].Attrs != nil && results[0].Childs["tag14"][0].InnerText != "" { 74 | panic("Test failed") 75 | } 76 | 77 | //result 2 78 | if results[1].Attrs["att1"] != "" || results[1].Attrs["att2"] != "att1" { 79 | panic("Test failed") 80 | } 81 | 82 | if results[1].Childs["tag11"][0].Attrs["att1"] != "att1" { 83 | panic("Test failed") 84 | } 85 | 86 | if results[1].Childs["tag11"][0].InnerText != "InnerText2" { 87 | panic("Test failed") 88 | } 89 | 90 | if results[1].Childs["tag12"][0].Attrs["att1"] != "att1" { 91 | panic("Test failed") 92 | } 93 | 94 | if results[1].Childs["tag12"][0].InnerText != "" { 95 | panic("Test failed") 96 | } 97 | if results[1].Childs["tag13"][0].Attrs != nil && results[1].Childs["tag13"][0].InnerText != "InnerText213" { 98 | panic("Test failed") 99 | } 100 | 101 | if results[1].Childs["tag14"][0].Attrs != nil && results[1].Childs["tag14"][0].InnerText != "" { 102 | panic("Test failed") 103 | } 104 | 105 | } 106 | 107 | func TestTagWithNoChild(t *testing.T) { 108 | 109 | p := getparser("tag2") 110 | 111 | var results []*XMLElement 112 | for xml := range p.Stream() { 113 | results = append(results, xml) 114 | } 115 | if len(results) != 2 { 116 | panic("Test failed") 117 | } 118 | if results[0].Childs != nil || results[1].Childs != nil { 119 | panic("Test failed") 120 | } 121 | if results[0].Attrs["att1"] != "testattr<" || results[1].Attrs["att1"] != "testattr<2" { 122 | panic("Test failed") 123 | } 124 | // with inner text 125 | p = getparser("tag3") 126 | 127 | results = results[:0] 128 | for xml := range p.Stream() { 129 | results = append(results, xml) 130 | } 131 | 132 | if len(results) != 2 { 133 | panic("Test failed") 134 | } 135 | if results[0].Childs != nil || results[1].Childs != nil { 136 | panic("Test failed") 137 | } 138 | 139 | if results[0].Attrs != nil || results[0].InnerText != "tag31" { 140 | panic("Test failed") 141 | } 142 | 143 | if results[1].Attrs["att1"] != "testattr<2" || results[1].InnerText != "tag32 " { 144 | panic("Test failed") 145 | } 146 | 147 | } 148 | 149 | func TestTagWithSpaceAndSkipOutElement(t *testing.T) { 150 | 151 | p := getparser("tag4").SkipElements([]string{"skipOutsideTag"}).SkipOuterElements() 152 | 153 | var results []*XMLElement 154 | for xml := range p.Stream() { 155 | results = append(results, xml) 156 | } 157 | 158 | if len(results) != 1 { 159 | panic("Test failed") 160 | } 161 | 162 | if results[0].Childs["tag11"][0].Attrs["att1"] != "att0 " { 163 | panic("Test failed") 164 | } 165 | 166 | if results[0].Childs["tag11"][0].InnerText != "InnerText0 " { 167 | panic("Test failed") 168 | } 169 | 170 | } 171 | 172 | func TestQuote(t *testing.T) { 173 | 174 | p := getparser("quotetest") 175 | 176 | var results []*XMLElement 177 | for xml := range p.Stream() { 178 | results = append(results, xml) 179 | } 180 | 181 | if len(results) != 1 { 182 | panic("Test failed") 183 | } 184 | 185 | if results[0].Attrs["att1"] != "test" || results[0].Attrs["att2"] != "test\"" || results[0].Attrs["att3"] != "test'" { 186 | panic("Test failed") 187 | } 188 | 189 | } 190 | 191 | func TestSkip(t *testing.T) { 192 | 193 | p := getparser("tag1").SkipElements([]string{"tag11", "tag13"}) 194 | 195 | var results []*XMLElement 196 | for xml := range p.Stream() { 197 | results = append(results, xml) 198 | } 199 | 200 | if len(results[0].Childs) != 2 { 201 | panic("Test failed") 202 | } 203 | 204 | if len(results[1].Childs) != 2 { 205 | panic("Test failed") 206 | } 207 | 208 | if results[0].Childs["tag11"] != nil { 209 | panic("Test failed") 210 | } 211 | 212 | if results[0].Childs["tag13"] != nil { 213 | panic("Test failed") 214 | } 215 | 216 | if results[1].Childs["tag11"] != nil { 217 | panic("Test failed") 218 | } 219 | 220 | if results[1].Childs["tag13"] != nil { 221 | panic("Test failed") 222 | } 223 | 224 | } 225 | 226 | func TestError(t *testing.T) { 227 | 228 | p := getparserFile("error.xml", "tag1") 229 | 230 | for xml := range p.Stream() { 231 | if xml.Err == nil { 232 | panic("It must give error") 233 | } 234 | } 235 | 236 | } 237 | 238 | func TestMultipleTags(t *testing.T) { 239 | p := getparser("tag1", "tag2") 240 | 241 | tagCount := map[string]int{} 242 | for xml := range p.Stream() { 243 | if xml.Name != "tag1" && xml.Name != "tag2" { 244 | t.Errorf("Only 'tag1' and 'tag2' expected, but '%s' returned", xml.Name) 245 | } 246 | tagCount[xml.Name]++ 247 | } 248 | 249 | if tagCount["tag1"] != 2 { 250 | t.Errorf("There should be 2 parsed 'tag1', but %d found", tagCount["tag1"]) 251 | } 252 | if tagCount["tag2"] != 2 { 253 | t.Errorf("There should be 2 parsed 'tag2', but %d found", tagCount["tag2"]) 254 | } 255 | } 256 | 257 | func TestMultipleTagsNested(t *testing.T) { 258 | p := getparser("tag1", "tag11") 259 | 260 | tagCount := map[string]int{} 261 | for xml := range p.Stream() { 262 | if xml.Name != "tag1" && xml.Name != "tag11" { 263 | t.Errorf("Only 'tag1' and 'tag11' expected, but '%s' returned", xml.Name) 264 | } 265 | tagCount[xml.Name]++ 266 | } 267 | 268 | if tagCount["tag1"] != 2 { 269 | t.Errorf("There should be 2 parsed 'tag1', but %d found", tagCount["tag1"]) 270 | } 271 | if tagCount["tag11"] != 1 { 272 | if tagCount["tag11"] == 4 { 273 | t.Errorf("There should be only 1 parsed 'tag11', but 'tag11' nested under 'tag1' were parsed too") 274 | } 275 | t.Errorf("There should be 1 parsed 'tag11', but %d found", tagCount["tag11"]) 276 | } 277 | } 278 | 279 | func TestXpath(t *testing.T) { 280 | xmlDoc := ` 281 | 282 | 283 | The Iliad and The Odyssey 284 | 12.95 285 | 286 | Best translation I've read. 287 | I like other versions better. 288 | 289 | 290 | 291 | Anthology of World Literature 292 | 24.95 293 | 294 | Needs more modern literature. 295 | Excellent overview of world literature. 296 | 297 | 298 | 299 | Journal of XML parsing 300 | 1 301 | 302 | ` 303 | 304 | sreader := strings.NewReader(xmlDoc) 305 | 306 | bufreader := bufio.NewReader(sreader) 307 | 308 | p := NewXMLParser(bufreader, "bookstore").EnableXpath() 309 | 310 | for xml := range p.Stream() { 311 | 312 | if list, err := xml.SelectElements("//book"); len(list) != 2 || err != nil { 313 | t.Fatal("//book != 2") 314 | } 315 | 316 | if list, err := xml.SelectElements("./book"); len(list) != 2 || err != nil { 317 | t.Fatal("./book != 2") 318 | } 319 | 320 | if list, err := xml.SelectElements("book"); len(list) != 2 || err != nil { 321 | t.Fatal("book != 2") 322 | } 323 | 324 | list, err := xml.SelectElements("./book/title") 325 | if len(list) != 2 || err != nil { 326 | t.Fatal("book != 2") 327 | } 328 | 329 | title, err := xml.SelectElement("./book/title") 330 | if err != nil && title.InnerText != "The Iliad and The Odyssey" { 331 | t.Fatal("./book/title") 332 | } 333 | 334 | el, err := xml.SelectElement("//book[@id='bk101']") 335 | if el == nil || err != nil { 336 | t.Fatal("//book[@id='bk101] is not found") 337 | } 338 | list, err = xml.SelectElements("//book[price>=10.95]") 339 | if list == nil || err != nil || len(list) != 2 { 340 | t.Fatal("//book[price>=10.95]") 341 | } 342 | 343 | list, err = xml.SelectElements("//book/comments/userComment[@rating='2']") 344 | if len(list) != 1 || err != nil { 345 | t.Fatal("//book/comments/userComment[@rating='2']") 346 | } 347 | 348 | // all books total price 349 | expr, err := p.CompileXpath("sum(//book/price)") 350 | if err != nil { 351 | t.Fatal("sum(//book/price) xpath expression compile error") 352 | } 353 | price := expr.Evaluate(p.CreateXPathNavigator(xml)).(float64) 354 | 355 | if fmt.Sprintf("%.2f", price) != "37.90" { 356 | t.Fatal("invalid total price->", price) 357 | } 358 | 359 | } 360 | } 361 | 362 | func TestXpathNS(t *testing.T) { 363 | 364 | br := bufio.NewReader(bytes.NewReader([]byte(` 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | `))) 382 | 383 | str := NewXMLParser(br, "soap:Envelope").EnableXpath() 384 | for xml := range str.Stream() { 385 | 386 | if list, err := xml.SelectElements("soap:Body"); len(list) != 2 || err != nil { 387 | t.Fatal("soap:Body != 2") 388 | } 389 | 390 | if list, err := xml.SelectElements("./soap:Body/soap:BodyNest1"); len(list) != 2 || err != nil { 391 | t.Fatal("/soap:Body/soap:BodyNest1 != 2") 392 | } 393 | 394 | if list, err := xml.SelectElements("./soap:Body/soap:BodyNest1/soap:BodyNest2"); len(list) != 1 || err != nil { 395 | t.Fatal("/soap:Body/soap:BodyNest1/soap:BodyNest2 != 1") 396 | } 397 | 398 | list, err := xml.SelectElements("./soap:Body/soap:BodyNest1/soap:BodyNest3") 399 | if len(list) != 1 || err != nil { 400 | t.Fatal("/soap:Body/soap:BodyNest1/soap:BodyNest3 != 1") 401 | } 402 | 403 | if list[0].Attrs["nestatt3"] != "nestatt3val" { 404 | t.Fatal("nestatt3 attiribute test failed") 405 | } 406 | 407 | } 408 | 409 | } 410 | 411 | func TestAttrOnly(t *testing.T) { 412 | p := getparser("examples", "tag1").ParseAttributesOnly("examples") 413 | for xml := range p.Stream() { 414 | if xml.Err != nil { 415 | t.Fatal(xml.Err) 416 | } 417 | if xml.Name == "examples" { 418 | if len(xml.Childs) != 0 { 419 | t.Fatal("Childs not empty for ParseAttributesOnly tags") 420 | } 421 | fmt.Printf("Name: \t%s\n", xml.Name) 422 | fmt.Printf("Attrs: \t%v\n\n", xml.Attrs) 423 | } 424 | if xml.Name == "tag1" { 425 | if len(xml.Childs) == 0 { 426 | t.Fatal("Childs not empty for ParseAttributesOnly tags") 427 | } 428 | fmt.Printf("Name: \t%s\n", xml.Name) 429 | fmt.Printf("Attrs: \t%v\n", xml.Attrs) 430 | fmt.Printf("Childs: %v\n", xml.Childs) 431 | } 432 | } 433 | } 434 | 435 | func Benchmark1(b *testing.B) { 436 | 437 | for n := 0; n < b.N; n++ { 438 | p := getparser("tag4").SkipElements([]string{"skipOutsideTag"}).SkipOuterElements() 439 | for xml := range p.Stream() { 440 | nothing(xml) 441 | } 442 | } 443 | } 444 | 445 | func Benchmark2(b *testing.B) { 446 | 447 | for n := 0; n < b.N; n++ { 448 | p := getparser("tag4") 449 | for xml := range p.Stream() { 450 | nothing(xml) 451 | } 452 | } 453 | 454 | } 455 | 456 | func Benchmark3(b *testing.B) { 457 | 458 | for n := 0; n < b.N; n++ { 459 | p := getparser("tag4").EnableXpath() 460 | for xml := range p.Stream() { 461 | nothing(xml) 462 | } 463 | } 464 | 465 | } 466 | 467 | func nothing(...interface{}) { 468 | } 469 | --------------------------------------------------------------------------------