やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

【textree】テキストから木構造データを作るPythonパッケージを公開した!

 Py2, 3対応。改行やタブで作ったツリーテキストとオブジェクトを相互変換する。ノードを取得・編集する多くのメソッドがある。ツリー構造の編集も可。

成果物

TexTree

インストール

pip install textree

 私の環境では以下のようなコマンドで行った。

sudo pip2 install textree
sudo pip3 install textree

使い方

基礎

 テキストからノードオブジェクトへ変換する。

import textree
tree_text = """
A
  A1
      A11
          A111
          A112
  A2
B
"""
tree = TexTree()
root = tree.to_node(tree_text)
print(root, root.Name)
for node in tree.Nodes:
    print(node.Line)
print(tree.to_text())

リファレンス

 テキストとオブジェクトを相互変換する。

root = tree.to_node(tree_text)
       tree.to_text()

 ノードをひとつずつ取得する。

tree.Nodes
for node in tree.Nodes:

 参照と代入。

node.Name
node.Parent
node.Children
node.Path
node.Attr
node.Index
node.Name = 'NewName'
node.Parent = Node('Parent')
node.Children.append(Node('Child'))

 移動。

node.to_first(path=None)
node.to_last(path=None)
node.to_next(path=None)
node.to_prev(path=None)
node.to_index(index, path=None)
node.to_children_first(path=None)
node.to_children_last(path=None)
node.to_children_index(index, path=None)
node.to_ancestor_first(indent=1)
node.to_ancestor_last(indent=1)
node.to_ancestor_prev(indent=1)
node.to_ancestor_next(indent=1)
node.to_ancestor_index(index, indent=1)

 取得。

node.select(path)
Path.select(root, 'A/A1/A11')
Path.select(A, 'A1/A11')

 挿入。

node.insert_first(Node('new'))
node.insert_last(Node('new'))
node.insert_next(Node('new'))
node.insert_prev(Node('new'))
node.insert_index(Node('new'), path, index)
node.insert_children_first(Node('new'), path='./')
node.insert_children_last(Node('new'), path='./')
node.insert_children_index(Node('new'), path, index)

 削除。

node.delete(path=None)

 更新。

node = root.select('A/A1/A11')
node.Name = 'UpdateName'

 APIは他にもある。詳細はコードまたはAPI一覧を参照すること。

属性

 同一行に属性を付与できる。

import textree
tree_text = """
A attrA
  A1  attrA1
      A11 attrA11
          A111    attrA111
          A112    attrA112
  A2  attrA2
B attrB
"""
tree = TexTree()
root = tree.to_node(tree_text)
print(root, root.Name)
for node in tree.Nodes:
    print(node.Name, node,Attr)

RootNode

 RootNodeに属性を付与できる。

import textree
tree_text = """
<ROOT>  root_attr
A attrA
  A1  attrA1
      A11 attrA11
          A111    attrA111
          A112    attrA112
  A2  attrA2
B attrB
"""
tree = TexTree()
root = tree.to_node(tree_text)
print(root, root.Name, root.Attr)
for node in tree.Nodes:
    print(node.Name, node,Attr)

シリアライズ・デシリアライズ

 ユーザは自由に属性を解析するコードを埋め込める。もちろんテキストへシリアライズするコードも書ける。

 以下のコードは、ノードにmy_nameを与える。

class MyNodeDeserializer(NodeDeserializer):
    def deserialize(self, ana, parent, parents=Node):
        node = Node(ana.Line, parent=parent)
        node.my_name = 'My name is ' + node.Name
        return node
tree = TexTree(node_deserializer=MyNodeDeserializer())
root = tree.to_node(tree_text)
for node in tree.Nodes:
    print(node.my_name)
class MyNodeAttributeSerializer(NodeAttributeSerializer):
    def serialize(self, attr): return 'my_name=' + attr
tree = TexTree(node_deserializer=MyNodeDeserializer(), node_serializer=NodeSerializer(MyNodeAttributeSerializer()))
root = tree.to_node(tree_text)
for node in tree.Nodes:
    print(node.my_name)
print(tree.to_text())

複雑な変換

JSON

tree_text

<ROOT> {"height": 444, "name": " \\" NAME \\" "}
A

a.py

import json, collections
class MyRootAttributeDeserializer(textree.RootAttributeDeserializer):
    def deserialize(self, line_attr):
        decoder = json.JSONDecoder(object_pairs_hook=collections.OrderedDict)
        attr = decoder.decode('{"width":640, "height":480, "name":"new.xcf"}') # default value
        if line_attr is None: return attr
        inpt = decoder.decode(line_attr)
        attr.update(inpt)
        return attr
class MyRootAttributeSerializer(textree.RootAttributeSerializer):
    def serialize(self, node):
        return json.dumps(node.Attr)

tree = TexTree(root_deserializer=textree.RootDeserializer(MyRootAttributeDeserializer()), root_serializer=textree.RootSerializer(MyRootAttributeSerializer()))
tree_text = '<ROOT>\t{"height": 444, "name": " \\" NAME \\" "}\nA'
root = tree.to_node(tree_text)
self.assertEqual(640, root.Attr['width'])
self.assertEqual(444, root.Attr['height'])
self.assertEqual(' " NAME " ', root.Attr['name'])
代入風

tree_text

<ROOT> height=444 name=" \" NAME \" "
A

b.py

import json, collections
class MyRootAttributeDeserializer(textree.RootAttributeDeserializer):
    def deserialize(self, line_attr):
        def parse_kv_pairs(text, item_sep=" ", value_sep="="):
            lexer = shlex.shlex(text, posix=True)
            lexer.whitespace = item_sep
            lexer.wordchars += value_sep
            return dict(word.split(value_sep) for word in lexer)
        decoder = json.JSONDecoder(object_pairs_hook=collections.OrderedDict)
        attr = decoder.decode('{"width":640, "height":480, "name":"new.xcf"}') # default value
        if line_attr is None: return attr
        inpt = parse_kv_pairs(line_attr)
        attr.update(inpt)
        attr['width'] = int(attr['width'])
        attr['height'] = int(attr['height'])
        return attr
class MyRootAttributeSerializer(textree.RootAttributeSerializer):
    def serialize(self, attr):
        def delete_braces(attr_str):
            if '{' == attr_str[0]: attr_str = attr_str[1:]
            if '}' == attr_str[-1]: attr_str = attr_str[:-1]
            return attr_str
        def delete_key_quotes(attr_str):
            rmidxs = []
            idxs = [i for i, x in enumerate(attr_str) if '=' == x]
            for idx in idxs:
                if '"' == attr_str[idx-1]:
                    start_quote_idx = idx-1-1
                    while -1 < start_quote_idx:
                        if '"' == attr_str[start_quote_idx]: break
                        else: start_quote_idx-=1
                    if -1 < start_quote_idx:
                        rmidxs.append(start_quote_idx)
                        rmidxs.append(idx-1)
            res = str(attr_str)
            diff = 0
            for rmidx in rmidxs:
                if   0 == rmidx: res = res[rmidx+1-diff:]
                elif rmidx == len(attr_str)-1: res = res[:rmidx-diff]
                else: res = res[:rmidx-diff] + res[rmidx+1-diff:]
                diff+=1
            return res
        attr_str = json.dumps(attr, False, True, True, True, None, None, (' ','='), "utf-8", None, False)
        attr_str = delete_key_quotes(delete_braces(attr_str))
        attr_str = attr_str.replace('width=640', '')
        attr_str = attr_str.replace('height=480', '')
        attr_str = attr_str.replace('name="new.xcf"', '')
        return None if attr_str is None else attr_str.strip()

tree = TexTree(root_deserializer=textree.RootDeserializer(attr_des=MyRootAttributeDeserializer()), root_serializer=textree.RootSerializer(attr_ser=MyRootAttributeSerializer()))
tree_text = '<ROOT>\theight=444 name=" \\" NAME \\" "\nA'
root = tree.to_node(tree_text)
self.assertEqual(640, root.Attr['width'])
self.assertEqual(444, root.Attr['height'])
self.assertEqual(' " NAME " ', root.Attr['name'])
self.assertEqual(tree_text, tree.to_text())

所感

 本ブログ開設してから初の偉業達成!

 初めてPyPIに再利用可能なパッケージを公開できた。

  • 初めてのPyPI
  • 初めてのAGPLv3
  • 英語のREADME.md(100% GoogleTranslate)

 プログラマになれた気分。このブログを始めてから3年以上が経ち、ようやく再利用できそうなコードを形にできた。

 英語で書いたので世界に対してコミットできたような気分に浸れた。もっとも、英文はすべてGoogleTranslateで翻訳したものだが。

 ライセンスはAGPLv3。最強のコピーレフト。世界のOSSコミュニティに参加できたつもりになれた。

 ただし英語はさっぱりわからない。英語で何か言われても反応できない。GitHubの使い方もよくわかっていない。プルリクエストなどが来たらパニクる。怖いやめて。でもたぶん誰も触らないはず。2分木やB木など効率的なアルゴリズムを使うだろうから。

 自己完結した自画自賛による自己満足。俺SUGEEEEE!

対象環境

$ uname -a
Linux raspberrypi 4.19.75-v7l+ #1270 SMP Tue Sep 24 18:51:41 BST 2019 armv7l GNU/Linux