2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JavaScriptでXMLをイベント駆動(SAX)でパースする

Last updated at Posted at 2019-06-08

最近JavaScriptを始めました。右も左も分からない初心者です。どうかお手柔らかにお願いします!

筆者は、普段はC++で、デスクトップアプリやサーバーなどを作っています。C++が好きすぎて他の言語を習得するのが億劫になってしまう年頃(おっさん)です。これではいけない、最新のWeb系フロントエンドもやらなくては、というわけで、昔少しかじって挫折したJavaScriptを再開することにしました。

私は、JSONよりXMLが好きで、DOMが嫌いでSAXが好きです。自作WebアプリでXMLを使おうと思っています。JavaScriptで使えるSAX型のXMLパーサーを探してみましたが、いまいち、これというのが見つけられませんでした。

XMLパーサーにはDOM方式とSAX方式があります。DOMはXMLデータ全体を読み込んで木構造を構築します。SAXはXMLデータを先頭から逐次読み込んで、イベント駆動で処理します。

DOMのイメージ
  • トップのタグの~
  • 子供タグの~
  • 子供タグの~
  • 子供タグの~
  • データをくれ~
SAXのイメージ
  • ドキュメント開始ですよー
  • タグが開始しましたよー
  • 文字データですよー
  • タグ終了ですよー
  • ドキュメント終了ですよー

こんな感じです。DOMは木構造の枝を辿って、分岐しながら、目的の値を拾ってきます。SAXは頭からデータを読んでいって、順番に処理していきます。「△△が来ましたよー」というの一つ一つがイベントです。

最初のパーサー

パースする機能をパーサーといいますね。日本語だと構文解析器などといいます。

では、最初の叩き台です。

<html>
    <body>
        <div id="hoge"></div>
    </body>

    <script>
        var result = ""
        var at = window.document.getElementById("hoge")

        function parse(src)
        {
            var result = ""
            var pos = 0
            while (true) {
                var c = -1;
                if (pos < src.length) {
                    c = src[pos]
                    result += c.toUpperCase()
                    pos++;
                } else {
                    break;
                }
            }
            return result
        }

        var src = "Hello, world"
        result = parse(src)

        at.innerHTML = result
    </script>
</html>

parse関数の中にwhileループがあって、1文字ずつ読み込んでresultを更新していきます。入力データの終端に達したら、ループを抜けて結果を返します。

タグとテキストを区別してパースする

<html>
    <body>
        <div id="hoge"></div>
    </body>
    <script>
        var result = ""
        var at = window.document.getElementById("hoge")

        function parse(src)
        {
            var result = ""
            
            const State_Normal = 0
            const State_TagStart = 1
            const State_TagName = 2

            var isalnum = function(c) {
                if (c >= 'a' && c <= 'z') return true
                if (c >= 'A' && c <= 'Z') return true
                if (c >= '0' && c <= '9') return true
                return false
            }

            var pos = 0;
            var state = State_Normal
            var text = ""
            var tagname = ""

            while (true) {
                var c = -1;
                if (pos < src.length) {
                    c = src[pos]
                    pos++;
                }
                if (c < 0) break
                switch (state) {
                case State_Normal:
                    if (c == "<") {
                        if (text.length > 0) {
                            result += "chr: " + text + "<br>"
                        }
                        text = ""
                        state = State_TagStart
                    } else {
                        text += c
                    }
                    break
                case State_TagStart:
                    if (isalnum(c) || c == '/') {
                        text += c
                        state = State_TagName
                    }
                    break
                case State_TagName:
                    if (isalnum(c)) {
                        text += c
                    } else if (c == ">") {
                        result += "tag: " + text + "<br>"
                        state = State_Normal
                        text = ""
                    }
                    break
                }
            }
            return result
        }

        var src = "<tag>text</tag>"
        result = parse(src)
        
        at.innerHTML = result
    </script>
</html>

これを実行すると、次のような結果が得られます。

tag: tag
chr: text
tag: /tag

<>が現れた時に内部状態(state)を変更して、タグかテキストデータかを区別するようにしました。

単純なXMLに対応する

<html>
    <body>
        <div id="hoge"></div>
    </body>
    <script>
        var result = ""
        var at = window.document.getElementById("hoge")
        SAXParser = function () {
            this.onStartElement = function(text) {}
            this.onEndElement = function(text) {}
            this.onCharacters = function(text) {}
        }
        SAXParser.prototype.parse = function(src) {
            const State_Normal = 0
            const State_TagStart = 1
            const State_TagName = 2
            const State_AttrNameStart = 3
            const State_AttrName = 4
            const State_AttrValueStart = 4
            const State_AttrValue = 5
            const State_TagEnd = 6

            const TagType_Start = 0
            const TagType_StartEnd = 1
            const TagType_End = 2
            const TagType_Question = 3
            const TagType_Exclamation = 4

            var my = this

            var pos = 0;
            var state = State_Normal
            var text = ""
            var tagname = ""
            var tagtype = ""
            var entity = ""

            var istagsym = function(c) {
                if (c >= 'a' && c <= 'z') return true
                if (c >= 'A' && c <= 'Z') return true
                if (c >= '0' && c <= '9') return true
                if (c == '-') return true
                if (c == '_') return true
                if (c == ':') return true
                if (c >= 0x100) return true
                return false
            }

            var dispatch = function() {
                if (tagtype == TagType_Start || tagtype == TagType_StartEnd) {
                    my.onStartElement(tagname)
                }
                if (tagtype == TagType_End || tagtype == TagType_StartEnd) {
                    my.onEndElement(tagname)
                }
            }

            var puttext = function(c) {
                if (entity == "") {
                    if (c == '&') {
                        entity += c
                    } else {
                        text += c
                    }
                } else {
                    entity += c
                    if (c == ';') {
                        if (entity.length >= 5 && entity[1] == '#') {
                            if (entity[2] == 'x' || entity[2] == 'X') {
                                var s = entity.substr(3, 2)
                                var i = parseInt(s, 16)
                                text += String.fromCharCode(i)
                            }
                        } else if (entity == "&lt;") {
                            text += '<'
                        } else if (entity == "&gt;") {
                            text += '>'
                        } else if (entity == "&amp;") {
                            text += '&'
                        } else if (entity == "&quot;") {
                            text += '\"'
                        }
                        entity = ""
                    }
                }
            }

            while (true) {
                var c = -1;
                if (pos < src.length) {
                    c = src[pos]
                    pos++;
                }
                if (c < 0) break
                switch (state) {
                case State_Normal:
                    if (c == "<") {
                        if (text.length > 0) {
                            this.onCharacters(text)
                        }
                        text = ""
                        state = State_TagStart
                        tagtype = TagType_Start
                    } else {
                        puttext(c)
                    }
                    break
                case State_TagStart:
                    if (istagsym(c)) {
                        text += c
                        state = State_TagName
                    } else if (c == "/") {
                        state = State_TagName
                        tagtype = TagType_End
                    } else if (c == "?") {
                        state = State_TagName
                        tagtype = TagType_Question
                    } else if (c == "!") {
                        state = State_TagName
                        tagtype = TagType_Exclamation
                    }
                    break
                case State_TagName:
                    if (state == "Exclamation") {

                    } else {
                        if (istagsym(c)) {
                            text += c
                        } else {
                            tagname = text
                            text = ""
                            if (c == "?") {
                            } else if (c == ">") {
                                dispatch()
                                state = State_Normal
                            }
                        }
                    }
                    break
                case State_AttrNameStart:
                    break
                case State_AttrName:
                    break
                case State_AttrValueStart:
                    break
                case State_AttrValue:
                    break
                case State_TagEnd:
                    break
                }
            }
        }

        var src = "<item>Hello, world</item>"
        var parser = new SAXParser()
        parser.onStartElement = function(text) {
            result += "[start element]  " + text + "<br>"
        }
        parser.onEndElement = function(text) {
            result += "[end element]  " + text + "<br>"
        }
        parser.onCharacters = function(text) {
            result += "[characters]  " + text + "<br>"
        }
        parser.parse(src)
        at.innerHTML = result
    </script>
</html>

<item>Hello, world</item>というXMLをパースして、下記のような結果を得られるようになりました。

[start element] item
[characters] Hello, world
[end element] item

SAXParserという名前でオブジェクト化して、onStartElementonEndElementonCharactersというコールバック関数で、各イベントに応答できるようにしてみました。

文字実体参照にも対応しましたので、&lt;&amp;&gt;の様に書けば<&>に変換されます。

今後

まず、最低でも属性値の取得に対応しないと、実用になりません。そして、コメントや<![CDATA[~~~]]>記法も実装する必要があります。あと、特殊なタグで!?から始まるものがあるので、それらに対応する必要があります。

2
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?