簡體   English   中英

ruamel.yaml 中人性化元組和 np.array 支持的隱式解析器和強大的表示器

[英]Implicit resolvers and robust representers for human-friendly tuple and np.array support in ruamel.yaml

我有一個項目,希望用戶手動編寫一個 yaml 文件。 這個 yaml 文件可能有一些條目格式為元組或 numpy arrays。我們在 python 內部區分元組和列表,為用戶提供方便的界面,例如 (1, 2, 3) 不同於 [1, 2, 3 ].

為方便起見,我希望用戶能夠使用括號直接輸入元組,例如name: (1,2,3) 我還希望用戶能夠通過輸入類似other_name: np.array([1,2,3])的內容來提供 numpy arrays。 我知道這不會保留 numpy arrays 的精確數字准確性,但我們確定這是改善用戶體驗的公平折衷。

我正在使用 ruamel.yaml,主要是因為它保留了注釋。

我設法做了一些有用的事情,但我覺得它不“正確”,尤其是解決部分。 基本上沒有隱式解析器,我使用的是臟評估。 我確實設法在 ruamel.yaml 在線,SO 上找到了一些關於隱式解析器的信息,並通過源代碼翻找,但我無法真正理解它。

這是一個最小的工作示例,其中的注釋指出了我認為實施不穩健或不干凈的地方。

import sys
import numpy as np
import ruamel.yaml


def _tupleRepresenter(dumper, data):
    # TODO: Make this more robust
    return dumper.represent_scalar(u'tag:yaml.org,2002:str', str(data))


def _numpyRepresenter(dumper, data):
    # TODO: Make this more robust
    as_string = 'np.array(' + np.array2string(data, max_line_width=np.inf, precision=16, prefix='np.array(', separator=', ', suffix=')') + ')'
    return dumper.represent_scalar(u'tag:yaml.org,2002:str', as_string)


def load_yaml(file):
    # TODO: Resolve tuples and arrays properly when loading
    yaml = ruamel.yaml.YAML()
    yaml.Representer.add_representer(tuple, _tupleRepresenter)
    yaml.Representer.add_representer(np.ndarray, _numpyRepresenter)
    return yaml.load(file)


def dump_yaml(data, file):
    yaml = ruamel.yaml.YAML()
    yaml.Representer.add_representer(tuple, _tupleRepresenter)
    yaml.Representer.add_representer(np.ndarray, _numpyRepresenter)
    return yaml.dump(data, file)


yaml_file = """
test_tuple: (1, 2, 3)
test_array: np.array([4,5,6])
"""

data = load_yaml(yaml_file)

data['test_tuple'] = eval(data['test_tuple']) # This feels dirty
data['test_array'] = eval(data['test_array']) # This feels dirty

dump_yaml(data, sys.stdout)
# test_tuple: (1, 2, 3)
# test_array: np.array([4, 5, 6])

我歡迎使用適當的隱式解析器改進此實現的任何幫助,使用健壯的代表,並且通常更像預期的那樣使用 ruamel.yaml。


更新

在評論的幫助下,我設法做了一些幾乎完全有效的事情。 讓我們忽略我現在需要編寫一個適當的非評估解析器。

剩下的唯一問題是新標簽現在導出為字符串,因此在重新加載時無法正確解釋它們。 它們變成了字符串,並且它們不會在多次往返中存活下來。

我怎樣才能避免這種情況?

這是一個最小的工作示例:

import sys
import numpy as np
import ruamel.yaml

# TODO: Replace evals by actual parsing
# TODO: Represent custom types without the string quotes

_tuple_re = "^(?:\((?:.|\n|\r)*,(?:.|\n|\r)*\){1}(?: |\n|\r)*$)"
_array_re = "^(?:(np\.|)array\(\[(?:.|\n|\r)*,(?:.|\n|\r)*\]\){1}(?: |\n|\r)*$)"
_complex_re = "^(?:(?:\d+(?:(?:\.\d+)?(?:e[+\-]\d+)?)?)?(?: *[+\-] *))?(?:\d+(?:(?:\.\d+)?(?:e[+\-]\d+)?)?)?[jJ]$"


def _tuple_constructor(self, node):
    return eval(self.construct_scalar(node))


def _array_constructor(self, node):
    value = node.value
    if not value.startswith('np.'):
        value = 'np.' + value
    return eval(value)


def _complex_constructor(self, node):
    return eval(node.value)


def _tuple_representer(dumper, data):
    return dumper.represent_scalar(u'tag:yaml.org,2002:str', str(data))


def _array_representer(dumper, data):
    as_string = 'np.array(' + np.array2string(data, max_line_width=np.inf, precision=16, prefix='np.array(', separator=', ', suffix=')') + ')'
    as_string = as_string.replace(' ', '').replace(',', ', ')
    return dumper.represent_scalar(u'tag:yaml.org,2002:str', as_string)


def _complex_representer(dumper, data):
    repr = str(data).replace('(', '').replace(')', '')
    return dumper.represent_scalar(u'tag:yaml.org,2002:str', repr)


custom_types = {
    '!tuple':   {'re':_tuple_re,   'constructor': _tuple_constructor,   'representer':_tuple_representer,   'type': tuple,      'first':list('(')             },
    '!nparray': {'re':_array_re,   'constructor': _array_constructor,   'representer':_array_representer,   'type': np.ndarray, 'first':list('an')            },
    '!complex': {'re':_complex_re, 'constructor': _complex_constructor, 'representer':_complex_representer, 'type': complex,    'first':list('0123456789+-jJ')},
}


def load_yaml(file):
    yaml = ruamel.yaml.YAML()
    for tag,ct in custom_types.items():
        yaml.Constructor.add_constructor(tag, ct['constructor'])
        yaml.Resolver.add_implicit_resolver(tag, ruamel.yaml.util.RegExp(ct['re']), ct['first'])
        yaml.Representer.add_representer(ct['type'], ct['representer'])
    return yaml.load(file)


def dump_yaml(data, file):
    yaml = ruamel.yaml.YAML()
    for tag,ct in custom_types.items():
        yaml.Constructor.add_constructor(tag, ct['constructor'])
        yaml.Resolver.add_implicit_resolver(tag, ruamel.yaml.util.RegExp(ct['re']), ct['first'])
        yaml.Representer.add_representer(ct['type'], ct['representer'])
    return yaml.dump(data, file)

yaml_file = """
test_tuple: (1, 2, 3)
test_array: array([4.0,5+0j,6.0j])
test_complex: 3 + 2j
"""

data = load_yaml(yaml_file)

dump_yaml(data, sys.stdout)
# test_tuple: '(1, 2, 3)'
# test_array: 'np.array([4.+0.j, 5.+0.j, 0.+6.j])'
# test_complex: '3+2j'

謝謝!

A representer in ruamel.yaml is used to dump a Python type in a specific way as YAML, you cannot normally use it to create a Python type from some part of YAML. 對於后者,您需要一個構造函數。

構造函數可以是顯式的,使用標記(例如!!float ,可以在此處找到這些列表),也可以是隱式的,即識別ruamel.yaml中最初使用正則表達式在標量上完成的輸入。

您的示例似乎需要在映射和字典之外擴展 YAML 的集合類型,如果不重寫大部分ruamel.yaml代碼,我認為您不會成功。 我建議您編寫代碼構造 numpy 數組,從標記的輸入開始,如下所示:

test_tuple: !np.array [1, 2, 3]

即使你不希望你的用戶那樣寫東西。 並且還使用該標簽轉儲 numpy arrays。

然后,下一步將編寫一個構造函數,該構造函數匹配以左括號和右括號開頭和結尾的標量,或者以np.array([並以])開頭(即使那里有一個[這不會開始位於標量中間的序列。您應該跟蹤使用這兩種格式中的哪一種來構造 NumPy 數組(例如,使用一些具有三態的唯一屬性,用於標記-、括號-或 np.array - 輸入)。您將需要解析匹配的標量,但您不需要使用eval()來執行此操作。對於替代方案,請查看例如!timestamp的處理。雖然您的示例只有一個整數數組,但您可能想看看(也)接受浮點數。

一旦有了這些額外的非標記構造函數,就可以根據屬性調整 NumPy 數組的表示器以使用非標記格式。

上面的一個很好的例子是浮點數的往返處理(保留科學記數法)和上述時間戳。

在 Anthon 在評論中的幫助下,並閱讀了他的 ruamel.yaml 源,我設法回答了我的問題。

我在這里提供了一個最小可行的解決方案以供參考。 如果要在來自您不信任的來源的 yaml 文件上執行此操作,則用實際的解析器替換 eval 以避免漏洞利用可能是一個好主意。

import sys
import numpy as np
import ruamel.yaml

from ruamel.yaml.comments import TaggedScalar

# TODO: Replace evals by actual parsing

_tuple_re = "^(?:\((?:.|\n|\r)*,(?:.|\n|\r)*\){1}(?: |\n|\r)*$)"
_array_re = "^(?:(np\.|)array\(\[(?:.|\n|\r)*,(?:.|\n|\r)*\]\){1}(?: |\n|\r)*$)"


def _tuple_constructor(self, node):
    return eval(self.construct_scalar(node))


def _array_constructor(self, node):
    value = node.value
    if not value.startswith('np.'):
        value = 'np.' + value
    return eval(value)


def _tuple_representer(dumper, data):
    repr = str(data)
    return dumper.represent_tagged_scalar(TaggedScalar(repr, style=None, tag='!tuple'))


def _array_representer(dumper, data):
    repr = 'np.array(' + np.array2string(data, max_line_width=np.inf, precision=16, prefix='np.array(', separator=', ', suffix=')') + ')'
    repr = repr.replace(' ', '').replace(',', ', ')
    return dumper.represent_tagged_scalar(TaggedScalar(repr, style=None, tag='!nparray'))


custom_types = {
    '!tuple':   {'re':_tuple_re,   'constructor': _tuple_constructor,   'representer':_tuple_representer,   'type': tuple,      'first':list('(')             },
    '!nparray': {'re':_array_re,   'constructor': _array_constructor,   'representer':_array_representer,   'type': np.ndarray, 'first':list('an')            },
}


def load_yaml(file):
    yaml = ruamel.yaml.YAML()
    for tag,ct in custom_types.items():
        yaml.Constructor.add_constructor(tag, ct['constructor'])
        yaml.Resolver.add_implicit_resolver(tag, ruamel.yaml.util.RegExp(ct['re']), ct['first'])
        yaml.Representer.add_representer(ct['type'], ct['representer'])
    return yaml.load(file)


def dump_yaml(data, file):
    yaml = ruamel.yaml.YAML()
    for tag,ct in custom_types.items():
        yaml.Constructor.add_constructor(tag, ct['constructor'])
        yaml.Resolver.add_implicit_resolver(tag, ruamel.yaml.util.RegExp(ct['re']), ct['first'])
        yaml.Representer.add_representer(ct['type'], ct['representer'])
    return yaml.dump(data, file)

yaml_file = """
test_tuple: (1, 2, 3)
test_array: array([4.0,5+0j,6.0j])
"""

data = load_yaml(yaml_file)

dump_yaml(data, sys.stdout)
# test_tuple: (1, 2, 3)
# test_array: np.array([4.+0.j, 5.+0.j, 0.+6.j])

我想通過添加一種不使用eval()的方法來擴展提供的解決方案。 它在某些情況下也可能會失敗,並且可能會使用 numpy arrays 的tolist()方法進行轉儲。

避免 eval() 的基本方法是構造函數將調用數組或元組的內部解析為列表。 一旦它處於類似列表的形式,ruamel.yaml 解析器就可以嘗試將項目作為列表加載。 然后,調用np.array()或 `tuple() 可以使 object 成為我們想要的。

此處添加了一個復雜的解析器,它是必需的,以便 yaml 本身可以將 numpy 數組的內部作為列表加載,以便它對此提供支持。 免責聲明:復數的正則表達式可能不必要地復雜。

加載和轉儲功能也發生了一些細微的外觀變化。

import sys
import numpy as np
import ruamel.yaml
import re

from ruamel.yaml.comments import TaggedScalar

_tuple_re = "^(?:\((?:.|\n|\r)*,(?:.|\n|\r)*\){1}(?: |\n|\r)*$)"
_array_re = "^(?:(np\.|)array\(\[(?:.|\n|\r)*,(?:.|\n|\r)*\]\){1}(?: |\n|\r)*$)"
_complex_re= "^(?:(?:\(|)(?:.|\n|\r)*[+-]|)(?:.|\n|\r)*j(?:\)|)$"


yaml = ruamel.yaml.YAML()

def setup_yaml(yaml):
    for tag,ct in custom_types.items():
        yaml.Constructor.add_constructor(tag, ct['constructor'])
        yaml.Resolver.add_implicit_resolver(tag, ruamel.yaml.util.RegExp(ct['re']), ct['first'])
        yaml.Representer.add_representer(ct['type'], ct['representer'])
    

def _tuple_constructor_safe(self,node):
    value = node.value
    value = re.sub("^\(","[",value)
    value = re.sub("\)$","]",value)
    safe_l = yaml.load(value)
    return tuple(safe_l)
    
def _complex_constructor(self,node):
    return complex(node.value)

def _array_constructor_safe(self,node):
    value = node.value
    value = re.sub("^(?:np\.|)array\(","",value)
    value = re.sub("\)$","",value)
    safe_l = yaml.load(value)
    return np.array(safe_l)


def _tuple_representer(dumper, data):
    repr = str(data)
    return dumper.represent_tagged_scalar(TaggedScalar(repr, style=None, tag='!tuple'))

def _complex_representer(dumper,data):
    repr = str(data)
    repr = re.sub("()","",repr)
    return dumper.represent_tagged_scalar(TaggedScalar(repr, style=None, tag='!complex'))

def _array_representer(dumper, data):
    repr = 'np.array(' + np.array2string(data, max_line_width=np.inf, precision=16, prefix='np.array(', separator=', ', suffix=')') + ')'
    repr = repr.replace(' ', '').replace(',', ', ')
    return dumper.represent_tagged_scalar(TaggedScalar(repr, style=None, tag='!nparray'))
  

    


custom_types = {
    '!tuple':   {'re':_tuple_re,   'constructor': _tuple_constructor_safe,   'representer':_tuple_representer,   'type': tuple,      'first':list('(')             },
    '!nparray': {'re':_array_re,   'constructor': _array_constructor_safe,   'representer':_array_representer,   'type': np.ndarray, 'first':list('an')},
    '!complex': {'re':_complex_re,   'constructor': _complex_constructor,   'representer':_complex_representer,   'type': complex, 'first':None }
}

def load_yaml(file):
    return yaml.load(file)


def dump_yaml(data, file):
    return yaml.dump(data, file)


setup_yaml(yaml)

yaml_file = """
test_tuple: (1, 2, 3)
test_array: array([4.0,5+0j,6.0j])
"""

data = load_yaml(yaml_file)

dump_yaml(data, sys.stdout)
# test_tuple: (1, 2, 3)
# test_array: np.array([4.+0.j, 5.+0.j, 0.+6.j])

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM