PythonでXMLをJSONに変換する

  • d:id:nagayoru:20110131:1296500717を多少綺麗にしてみました。
  • ソースは下
  • XMLtoJSONをimportして使えます。
  • Python 2.6.5 でテストしてあります。
  • 引き続き突っ込み募集です!
  • jsでevalする場合は eval("("+ str ")"); としないとラベル文に解釈されてシンタックスエラーになります。
  • 後でもうちょっと機能追加してしっかりテストしてライブラリにしたいな…
  • plain text : http://www.smihica.com/misc/xml_to_json.py.txt

クラス説明 Class Description

class XMLtoJSON( output=None, input=None, input_string=None, indent=2, output_file_append=False );
method parse()
method parse_string(string = None)
method parse_stream(stream = None)
method parse_file(path = None)
  • 「output」はJSONのアウトプット先を制御します。
  • 「input」はXMLのインプット先を制御します。
  • 「input_string」はXMLのインプットを文字列で行う場合に使います。
  • 'output' controls the direction of JSON output.
  • 'input' controls the direction of XML input.
  • 'input_string' controls the direction of XML input.
  • 「output」が与えられなければ、parseメソッドの戻り値で文字列として帰ってきます。
  • file/io_stream系のオブジェクトなら、そこに書き込まれます。
  • 文字列が指定された場合は、その文字列が指し示すパスのファイルに書き込まれます。
  • when 'output' is not given, the result will be returned by parse methods as string.
  • otherwise, it is an io_stream object, the result will be written in the stream.
  • and it is an string object, the result will be written in the file specified path of the string.
  • 「input」が file/io_stream系のオブジェクトなら、そこから読み込まれます。
  • 文字列が指定された場合は、その文字列が指し示すパスのファイルから読み出されます。
  • 指定されていない場合は、「input_string」で指定された文字列が使われます。
  • 両方指定された場合はinputが優先です。
  • when input is an io_stream object, parser will read from there.
  • otherwise, it is an string object, parser will read from the file specified path of the string.
  • when it is not given parser will use string specified as 'input_string'.
  • when both given, 'input' is given the priority.
  • 「indent」はインデントの深さを制御します。
  • デフォルトは2で、falseの場合はプリティプリント自体しません。
  • 'indent' controls indent depth of result. default is 2.
  • if set false, this will not do pretty printing.
  • 「output_file_append」は出力先がファイルだった場合に、ファイルに追記するかを指定します
  • 追記する場合は True 上書きする場合は False (デフォルト) です。

例 Example

p = XMLtoJSON( output=sys.stdout, input="./test.xml", indent=False )
p.parse()
  • "./test.xml"を読み込み、パースし、sys.stdoutに書き出す。
  • This means that, parse "./test.xml" and print result to sys.stdout with no indent.
p = XMLtoJSON( output="./test.json", input_string"<?xml version='1.0' encoding='UTF-8'?><abc version='0.1'></abc>" )
p.parse()
  • "string"をパースし、"./test.json"に書き出す。
  • "string" -> parse -> "./test.json"
p = XMLtoJSON( input_string="<?xml version='1.0' encoding='UTF-8'?><abc version='0.1'></abc>" )
print p.parse()
# => {"abc":
#      {"version":"0.1"}}
  • "string"をパースし、parse()メソッドの戻り値で文字列として返す。
  • "string" -> parse -> return as string.
p = XMLtoJSON( output="./test.json", output_file_append=True )
st = StringIO.StringIO()
st.write("<?xml version='1.0' encoding='UTF-8'?><abc version='0.1'><bcd>a</bcd>") # this value will be parsed.
p.set_input(st)
st.write("<bcd>b</bcd></abc>") # this operation is not meaningful when st is stringIO.
p.parse() # -> ERROR (SAXParseException)!!!
  • 「input」にStringIO.StringIOかio.StringIOを指定した場合、
  • 指定した時点までに書き込まれている値のみパースされます。
  • その後に書き込んだ値は無視されます。
  • when you set 'input' to StringIO.StringIO io.StringIO
  • The value that will be parsed is just the value that have already written by the time when set_input() called.
  • Following writing will be ignored.

コード Code

#!/usr/local/bin/python
# -*- coding: utf-8 -*-

import sys
import re
import xml.sax
import io        # for 2.6
import StringIO  # for 3.0

class XMLtoJSON_ContentHandler(xml.sax.handler.ContentHandler):

    def __init__(self, output=sys.stdout, pretty_print=True, indent=2):
        self.output = output
        self.indent = indent
        self.indent_space = ' '*self.indent
        self.pretty_print = pretty_print

        self.last_called = "__init__"

    def startDocument(self):
        self.data = {}
        self.p_data = [ self.data ]
        self.continuations = []
        self.push_text_node = []

        self.last_called = "startDocument"

    def endDocument(self):
        self.print_json()

    def characters(self, content):
        this_push_text_node = self.push_text_node[-1]
        line = re.match('^\s*(.*)$', content)
        if line and len(line.groups()[0]) > 0:
            this_push_text_node(line.groups()[0], self.last_called == "characters")
            self.last_called = "characters"

    def startElement(self, name, attr):
        this_p_data = self.p_data[-1]
        this_data = { 'd': {} }

        def this_push_text_node(node, continued_p):
            data = this_data['d']
            if not data.get('#text'):
                data['#text'] = node
            elif isinstance( (data['#text']) , list):
                if continued_p:
                    data['#text'][-1] = data['#text'][-1] + '\\n' + node
                else:
                    data['#text'].append(node)
            else:
                if continued_p:
                    data['#text'] = data['#text'] + '\\n' + node
                else:
                    data['#text'] = [data['#text'], node]

        self.push_text_node.append(this_push_text_node)

        for key in attr.getNames():
            value = attr.getValue(key)
            this_data['d'][key] = value

        def cont():
            keys = this_data['d'].keys()

            if keys == ['#text']:
                if not isinstance( (this_data['d']['#text']) , list) :
                    this_data['d'] = this_data['d']['#text']

            if keys:
                if not this_p_data.get(name):
                    this_p_data[name] = this_data['d']
                elif isinstance( (this_p_data[name]) , list) :
                    this_p_data[name].append(this_data['d'])
                else:
                    this_p_data[name] = [this_p_data[name], this_data['d']]

        self.continuations.append(cont)
        self.p_data.append(this_data['d'])

        self.last_called = "startElement"

    def endElement(self, name):
        self.p_data.pop()
        self.push_text_node.pop()
        cont = self.continuations.pop()
        cont()
        self.last_called = "endElement"

    def print_json(self):
        first_time = [1]
        def it (h, nesting=0):
            if isinstance(h, dict):
                if self.pretty_print:
                    if not first_time[0]:
                        self.output.write ( "\n" )
                    else:
                        first_time[0] = 0

                    for i in range(nesting):
                        self.output.write (self.indent_space)

                self.output.write ( "{" )
                l, i = len(h), 0
                for k, v in h.iteritems():
                    self.output.write ( '"' + k + '"' + ':' )
                    it(v, nesting+1)
                    if i < (l-1):
                        self.output.write ( "," )
                        if self.pretty_print:
                            self.output.write ( "\n " )
                            for j in range(nesting):
                                self.output.write (self.indent_space)
                    i+=1
                self.output.write( "}" )

            if isinstance(h, list):
                self.output.write ( "[" )
                l, i = len(h), 0
                for a in h:
                    it(a, nesting+1)
                    if i < (l-1):
                        self.output.write(',')
                    i+=1
                self.output.write ( "]" )

            if isinstance (h, basestring):
                h = h.replace('"', '\\"')
                self.output.write ('"' + h + '"')

        it(self.data)

        if self.pretty_print:
            self.output.write("\n")


class XMLtoJSON():

    def __init__( self, output=None, input=None, input_string=None, indent=2, output_file_append=False ):

        self.indent       = indent
        pretty_print = (self.indent != False) and (self.indent > -1);
        self.handler = XMLtoJSON_ContentHandler( None,
                                                 pretty_print,
                                                 self.indent or 0 )
        self.output_file_append = output_file_append
        self.set_output(output)

        if input != None: # input is given priority.
            self.set_input(input)
        else:
            self.set_input_string(input_string or "")

    def set_output(self, output=None):
        if output == None:
            self.output_type  = "s" # string
        elif isinstance(output, (file, StringIO.StringIO, io.StringIO)):
            self.output_type  = "i" # io_stream
            self.handler.output = output
        elif isinstance(output, (str, basestring)):
            self.output_type  = "f" # file
        else:
            raise TypeError("The specified 'output' type is not surported.")

        self.output           = output

    def set_input(self, input):
        if isinstance(input, file):
            self.input_type = "i" # io_stream
        elif isinstance(input, (str, basestring)):
            self.input_type = "f" # file
        elif isinstance(input, (StringIO.StringIO, io.StringIO)):
            self.set_input_string(input.getvalue())
            return
        else:
            raise TypeError("The specified 'input' type is not surported.")

        self.input        = input

    def set_input_string(self, input_string):
        if isinstance(input_string, (str, basestring)):
            self.input_type = "s" # string
        else:
            raise TypeError("The specified value is not a string.")

        self.input_string = input_string

    def parse_base(self, parsing):
        if self.output_type == "s": #string
            o = StringIO.StringIO()
            self.handler.output = o
            parsing()
            try:
                c = o.getvalue()
            finally:
                o.close()
            return c

        if self.output_type == "f":
            if self.output_file_append:
                mode = 'a'
            else:
                mode = 'w'

            with file(self.output, mode) as f:
                self.handler.output = f
                parsing()

            return

        parsing()

    def parse_string(self, string = None):
        # argument is given the priority.
        def parsing():
            xml.sax.parseString((string or self.input_string), self.handler)

        return self.parse_base(parsing)

    def parse_stream(self, stream = None):
        # argument is given the priority.
        def parsing():
            xml.sax.parse((stream or self.input), self.handler)

        return self.parse_base(parsing)

    def parse_file(self, path = None):
        # argument is given the priority.
        def parsing():
            with file((path or self.input), 'r') as f:
                xml.sax.parse(f, self.handler)

        return self.parse_base(parsing)

    def parse(self):
        if(self.input_type == "s"): # string
            return self.parse_string()
        if(self.input_type == "i"): # io_stream
            return self.parse_stream()
        if(self.input_type == "f"): # file
            return self.parse_file()
        else:
            raise StandardError("input_type is unknown type. -> '"+self.input_type+"' .")

変換例 Translation Example

// 例1,2,3は JKL.ParseXML(http://www.kawa.net/works/js/jkl/parsexml.html) から頂きました。
// 1(基本)
// xml /////////////
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<items>
  <item>
    <jcity>Chiyoda-ku</jcity>
    <jlocal>Chiyoda</jlocal>
    <jpref>TOKYO</jpref>
    <pref_cd>13</pref_cd>
    <zip_cd>1000001</zip_cd>
  </item>
</items>

// json /////////////
{"items":
     {"item":
          {"jlocal":"Chiyoda",
           "zip_cd":"1000001",
           "jpref":"TOKYO",
           "pref_cd":"13",
           "jcity":"Chiyoda-ku"}}}

// 2(結合)
// xml /////////////
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<children>
  <girl>HANAKO</girl>
  <boy>TARO</boy>
  <boy>JIRO</boy>
</children>

// json /////////////
{"children":
  {"boy":["TARO","JIRO"],
   "girl":"HANAKO"}}

// 3(長文)
// xml /////////////
<?xml version="1.0" encoding="UTF-8" ?>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://purl.org/rss/1.0/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel rdf:about="http://www.kawa.net/xp/index-j.html">
<title>Kawa.net xp - Ajax&Perl技術情報(川崎有亮)</title>
<link>http://www.kawa.net/xp/index-j.html</link>
<dc:date>2010-10-31T20:57:00+09:00</dc:date>
<dc:language>ja</dc:language>
<dc:rights>Copyright 1995-2010 Yusuke Kawasaki. All rights reserved.</dc:rights>
<description>川崎有亮の制作したプログラムのご紹介・技術情報など。ajax/JavaScript/Perl/CGI/...</description>
<image rdf:resource="http://www.kawa.net/xp/images/xp-title-128x32.gif" />
<items>
<rdf:Seq>
<rdf:li rdf:resource="http://kawa.at.webry.info/201101/article_3.html" />
<rdf:li rdf:resource="http://kawa.at.webry.info/201101/article_2.html" />
................................以下略

// json /////////////
{"rdf:RDF":
  {"xmlns:rdf":"http://www.w3.org/1999/02/22-rdf-syntax-ns#",
   "xmlns":"http://purl.org/rss/1.0/",
   "image":
    {"url":"http://www.kawa.net/xp/images/xp-title-128x32.gif",
     "link":"http://www.kawa.net/xp/index-j.html",
     "rdf:about":"http://www.kawa.net/xp/images/xp-title-128x32.gif",
     "title":"Kawa.net xp - Ajax&Perl技術情報(川崎有亮)"},
   "item":[
      {"dc:date":"2011-01-12T02:55:00+09:00",
       "description":"openFrameworks では、Windows・Mac 向けアプリに限らず、ofxiPhone アドオンでiPhone・iPod touch・iPad 上で稼働するアプリケーションを開発することができます。OpenCV など openFrameworks のライブラリを手軽に iPhone 上で使えるらしい。",
       "dc:creator":"Kawanet Tech Blog",
       "title":"openFrameworks+ofxiPhoneでiPhone・iPad用Hello, world!",
       "link":"http://kawa.at.webry.info/201101/article_3.html",
       "rdf:about":"http://kawa.at.webry.info/201101/article_3.html",
       "dc:subject":["Xcode","openFrameworks","iPhone"]},
................................以下略

// 4 (複合)
// xml /////////////
<?xml version='1.0' encoding='UTF-8'?>
<abc version='0.1'>
  auuauauuaau
  byaaaaaaaaaaaaaaaou
  this is test text.
  <version><moe>gyoeee uhehe</moe></version>
  <list>
    uiuiueoooo
    <file permisssion='0777' size='10485760'><![CDATA[/var/test.dat]]></file>
    <file><![CDATA[/var/test0.dat]]></file>
    <file permisssion='0644' modifiedTime='1296081769'><![CDATA[/var/test1.dat]]></file>
    <file></file>
    <file permisssion='0744' owner='0' group='0' checksum='0'><![CDATA[/var/test2.dat]]></file>
    myaoooou
    <file><![CDATA[%2f%2f%2f"\\""%2f%2f%2f]]></file>
  </list>
  hieeeeeeeeeeeee
</abc>

// json /////////////
{"abc":
  {"#text":["auuauauuaau\nbyaaaaaaaaaaaaaaaou\nthis is test text.","hieeeeeeeeeeeee"],
   "version":["0.1",
      {"moe":"gyoeee uhehe"}],
   "list":
    {"#text":["uiuiueoooo","myaoooou"],
     "file":[
        {"permisssion":"0777",
         "#text":"/var/test.dat",
         "size":"10485760"},"/var/test0.dat",
        {"permisssion":"0644",
         "#text":"/var/test1.dat",
         "modifiedTime":"1296081769"},
        {"owner":"0",
         "permisssion":"0744",
         "#text":"/var/test2.dat",
         "group":"0",
         "checksum":"0"},"%2f%2f%2f\"\\"\"%2f%2f%2f"]}}}
  • 連続したテキストは\nで結合する。
  • attributeとnodeに同じものがあったら結合する。
  • 空ノードは捨てる。
  • CDATAは普通のテキストにする。