xmlビューワー

見るだけだけどちょいと必要だったので作った。
とりあえず貼り。

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

import wx
from wx.lib.mixins import treemixin
import xml.sax
import xml.sax.handler
import sys
import os


FS_ENCODING='cp932'


class Node(object):
    __slots__=[
            'name',
            'attrs',
            'body',
            'children',
            ]
    def __init__(self, name, attrs=[]):
        self.name=name
        self.attrs=attrs
        self.children=[]
        self.body=''


class TreeModel(object):
    def __init__(self):
        self.root=Node('root')

    # @param indices[in] 目的のノードへのパスをtupleで受ける
    # rootノードは ()
    # rootの一つ目の子ノードは (0, ) (1要素のタプルは不許可)
    # その2番目の子ノードは (0, 1) という具合
    def GetNode(self, indices):
        node=self.root
        # パスを辿っていく
        for index in indices:
            node=node.children[index]
        return node

    def GetText(self, indices):
        return self.GetNode(indices).name

    def GetNamePath(self, indices):
        path=['']
        self.__GetNamePath(self.root, indices, path)
        return path

    def __GetNamePath(self, node, indices, path):
        if len(indices)==0:
            return
        counter={}
        for i in xrange(indices[0]+1):
            child=node.children[i]
            if child.name in counter:
                counter[child.name]+=1
            else:
                counter[child.name]=0
        child=node.children[indices[0]]
        if counter[child.name]==0:
            path.append(child.name)
        else:
            path.append("%s[%d]" % (child.name, counter[child.name]))
        self.__GetNamePath(child, indices[1:], path)


class VTree(treemixin.VirtualTree, wx.TreeCtrl):
    def __init__(self, parent):
        super(VTree, self).__init__(parent)
        self.model=TreeModel()
        self.SetRoot(Node('root'))

    def SetRoot(self, root):
        self.model.root=root
        self.RefreshItems()

    def OnGetItemText(self, indices):
        return self.model.GetText(indices)
        
    def OnGetChildrenCount(self, indices):
        return len(self.model.GetNode(indices).children)

    def SelectPath(self, indices):
        item=self.GetRootItem()
        while len(indices)>0:
            index=indices.pop(0)
            item, cookie=self.GetFirstChild(item)
            for i in xrange(index):
                item=self.GetNextSibling(item)
        self.SelectItem(item)
        self.EnsureVisible(item)


class TreeBuildHandler(xml.sax.handler.ContentHandler):
    def __init__(self):
        self.root=Node('root', {})
        self.stack=[self.root]

    def startElement(self, name, attrs):
        attrs_list=[]
        for attr in attrs.items():
            attrs_list.append(attr)
        node=Node(name, attrs_list)
        self.stack[-1].children.append(node)
        self.stack.append(node)

    def endElement(self, name):
        self.stack[-1].body.strip()
        self.stack.pop()

    def characters(self, c):
        self.stack[-1].body+=c


class AttrList(wx.ListCtrl):
    def __init__(self, parent):
        super(AttrList, self).__init__(parent,
                style=wx.LC_REPORT|wx.LC_VIRTUAL)
        self.InsertColumn(0, "key")
        self.SetColumnWidth(0, 64)
        self.InsertColumn(1, "value")
        self.SetColumnWidth(1, 200)
        self.SetAttrs([])

    def SetAttrs(self, attrs):
        self.attrs=attrs
        self.attrs.sort()
        self.SetItemCount(len(self.attrs))
        self.Refresh()

    def OnGetItemText(self, index, column):
        attr=self.attrs[index]
        if column==0:
            return attr[0]
        elif column==1:
            return attr[1]


class RightView(wx.Panel):
    def __init__(self, parent):
        super(RightView, self).__init__(parent)
        box=wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(box)

        self.body=wx.TextCtrl(self, style=wx.TE_MULTILINE)
        box.Add(self.body, 2, wx.EXPAND)

        self.attrs=AttrList(self)
        box.Add(self.attrs, 1, wx.EXPAND)

    def SetNode(self, node):
        self.attrs.SetAttrs(node.attrs)
        self.body.SetValue(node.body)


def fileOpenDialog(parent, message):
    dialog = wx.FileDialog(parent, message, "", "", "", wx.OPEN)
    try:
        if dialog.ShowModal() == wx.ID_OK:
            return dialog.GetPath()
    finally:
        dialog.Destroy()


class MyFrame(wx.Frame):
    def __init__(self, parent):
        super(MyFrame, self).__init__(parent)
        # menu bar
        menubar = wx.MenuBar()
        self.SetMenuBar(menubar)
        menubar.Append(*self.CreateFileMenu())
        # status bar
        self.statusbar = self.CreateStatusBar()
        # view
        splitter=wx.SplitterWindow(self)
        self.tree=VTree(splitter)
        self.view=RightView(splitter)
        splitter.SplitVertically(self.tree, self.view)
        # bind event
        self.tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelect)
        self.Bind(wx.EVT_MENU, self.OnOpen, id=wx.ID_OPEN)
        self.Bind(wx.EVT_MENU, lambda e: self.Close(), id=wx.ID_CLOSE)
        self.keyMap={
                27: lambda event: self.Close(),
                }
        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
        self.SetFocus()

    def CreateFileMenu(self):
        menu = wx.Menu()
        menu.Append(wx.ID_OPEN, 'open', 'open a file')
        menu.AppendSeparator()
        menu.Append(wx.ID_CLOSE, 'quit', 'quit application')
        return menu, "&File"
    
    def OnOpen(self, event):
        path=fileOpenDialog(self, "open a file")
        if not path:
            return
        self.Load(path)

    def OnSelect(self, event):
        # wx.TreeItemIdからindex pathを得る
        # wx.treemixin.TreeHelper.GetIndexOfItem
        indices=self.tree.GetIndexOfItem(event.GetItem())

        node=self.tree.model.GetNode(indices)
        self.view.SetNode(node)
        self.statusbar.PushStatusText(
                '/'.join(self.tree.model.GetNamePath(indices)))

    def OnKeyDown(self, event):
        keyCode=event.GetKeyCode()
        try:
            self.keyMap[keyCode](event)
        except KeyError:
            print "OnKeyDown", keyCode

    def Load(self, file):
        global FS_ENCODING
        if not os.path.exists(file):
            print 'file not found', file
            return

        handler=TreeBuildHandler()
        parser = xml.sax.make_parser()
        parser.setContentHandler(handler)
        isParsed=False
        for encoding in ['utf-8', 'euc-jp', 'cp932']:
            # デスクトップなど日本語パスを開くのに注意
            io=xml.sax.xmlreader.InputSource(file.encode(FS_ENCODING))
            io.setEncoding(encoding)
            try:
                parser.parse(io)
                isParsed=True
                break
            except (UnicodeEncodeError, UnicodeDecodeError, 
                    xml.sax._exceptions.SAXParseException), e:
                continue

        if not isParsed:
            print "fail to load", file
            return

        self.tree.SetRoot(handler.root)
        self.SetTitle("%s[%s]" % (file, encoding))
        self.Refresh()


def usage():
    print "usage %s {xml}" % sys.argv[0]


if __name__=='__main__':
    app=wx.App(0)
    frame=MyFrame(None)
    if len(sys.argv)>1:
        frame.Load(sys.argv[1])
    frame.Show()
    app.MainLoop()