【textree】テキストから木構造データを作るPythonパッケージを公開した!
Py2, 3対応。改行やタブで作ったツリーテキストとオブジェクトを相互変換する。ノードを取得・編集する多くのメソッドがある。ツリー構造の編集も可。
成果物
インストール
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!
対象環境
- Raspbierry pi 4 Model B
- Raspbian buster 10.0 2019-09-26 ※
- bash 5.0.3(1)-release
- Python 2.7.16
- Python 3.7.3
$ uname -a Linux raspberrypi 4.19.75-v7l+ #1270 SMP Tue Sep 24 18:51:41 BST 2019 armv7l GNU/Linux