从 python 中表示为字符串的 Hive Struct 数据类型中提取列

Extract columns from Hive Struct data type represented as string in python

在我的 python 程序中,我需要编写一个函数,将 hive 数据类型作为输入,并 return 判断数据类型是否有效。

hive支持的原始数据类型如下:

supported_data_types: Set = {'void', 'boolean', 'tinyint', 'smallint', 'int', 'bigint', 'float', 'double',
                                  'decimal', 'string', 'varchar', 'timestamp', 'date', 'binary'}

hive 支持的复杂数据类型有:

arrays: array<data_type>
maps: map<primitive_type, data_type>
structs: struct<col_name : data_type [comment col_comment], ...>
union: union<data_type, data_type, ...>

在我的 python 程序中,我有一个变量存储原始或复杂的配置单元数据类型。如果变量的数据类型有效,我需要编写一个可以 return True 的函数,否则我必须 return false.

原始数据类型很容易验证,我只需要写:

def validate_datatype(_type: str):
    _type = _type.strip()

    if _type in HIVE_SUPPORTED_DATA_TYPES:
        return True

我还设法验证了数组和映射数据类型到任何嵌套级别。我观察到这样一个事实,即对于数组,任何有效类型都具有语法:array<data_type>。所以,如果我有一个类型 array<data_type>,我递归地验证 data_type。在地图的情况下,它的键总是原始数据类型,因此,也可以递归地验证地图数据类型。我使用以下函数递归验证数据类型。

def validate_datatype(_type: str) -> bool:
    _type = _type.strip()

    if _type in HIVE_SUPPORTED_DATA_TYPES:
        return True

    # Array type has syntax : array<data_type>, where data_type is any valid data_type
    if _type.startswith('array<'):
        assert _type.endswith(">"), "could not find matching '>' for data type 'array<' "
        return validate_datatype(_type[6:-1])

    # Map type has syntax : map<primitive_type, data_type>, where primitive type can only be one of
    # primitive types present in HIVE_SUPPORTED_DATA_TYPES. data_type can be any(primitive or complex)
    # valid hive data type
    if _type.startswith('map<'):
        assert _type.endswith(">"), "could not find matching '>' for data type 'map<' "
        primitive_type, data_type = _type[4:-1].split(',', 1)
        if primitive_type.strip() in HIVE_SUPPORTED_DATA_TYPES and validate_datatype(data_type):
            return True

这样的递归函数不可能(至少在我看来)来验证 struct 数据类型,它具有以下语法:struct<col1_name : data_type, col2_name : data_type, ... >(假设我不必担心 [COMMENT col_comment] 现在部分。我没有收到任何带有列评论的输入。)

为了验证结构数据类型,我首先需要编写一些其他函数来从结构数据类型中提取列。设此抽象函数为 extract_columns,语法如下。

@abstractmethod
def extract_columns(dt: str) -> List[Tuple[str, str]]:
    pass

我应该得到以下输入的结果:

dt = "struct<col_1: int, col_2 : struct<nested_col_1 : int, nested_col_2 : str>>"

extract_columns(dt)
>> [('col_1','int'), ('col_2', 'struct<nested_col_1 : int, nested_col_2 : str>')]

如果我能写出这样的函数,我就能递归地验证 data_types。由于 struct 可以包含许多内部 struct 和其他复杂数据类型,我不能在 : 处使用拆分(也可能拆分嵌套结构,我不想这样做),或者,(也可能在嵌套映射和嵌套结构类型处拆分,同样,我不想这样做)。

所以,在我看来,re 可能能够提取这样的列列表,但是,我无法确定我们对此的需求。有人能帮我一下吗?非常感谢。

这里的任务是找到所有出现的逗号和冒号,这些逗号和冒号的左侧 < 数量与 > 数量相同。虽然正则表达式确实有一些奇特的特性,比如在搜索时能够存储变量,但我认为这个问题超出了它们的范围。一般来说,正则表达式只对简单的模式匹配有用,比如识别 URL。很容易得意忘形并想在任何地方应用正则表达式,但这通常会导致代码无法阅读 nightmare strings.

这里最简单的解决方案是简单的迭代。要实现 extract_columns 函数,您需要一个 split 函数来忽略嵌套在 <>:

中的分隔符
def first_index(target: str, string: str) -> int:
    # Finds the index of the first top-level occurrence of `target` in `string`
    depth = 0
    for i in range(len(string)):
        if string[i] == '>':
            depth -= 1
        if string[i] == target and depth == 0:
            return i
        if string[i] == '<':
            depth += 1
    # No matches were found
    return len(string)
def top_level_split(string: str, separator: str) -> [str]:
    # Splits `string` on every occurrence of `separator` that is not nested in a "<>"
    split = []
    while string != '':
        index = first_index(target=separator, string=string)
        split.append(string[:index])
        string = string[index + 1 :]
    return split

完整的实现如下:

def is_valid_datatype(string: str) -> bool:
    
    if is_valid_primitive(string):
        return True
    
    # Find the opening carrot
    left = first_index(target='<', string=string)
    # Find the closing carrot
    right = first_index(target='>', string=string)
    if left > right or right == len(string):
        return False
    # Make sure there isn't anything to the right of `right`
    if string[right + 1 : ].strip() != '':
        return False
    
    # The name of the data type, e.g. "array"
    type_name = string[ : left].strip()
    # The substring between the carrots
    contents = string[left + 1 : right]
    
    if type_name == 'array':
        return is_valid_datatype(contents)
    if type_name == 'map':
        # We don't need `top_level_split` here because the first type is always primitive
        split = contents.split(',', 1)
        return len(split) == 2 and is_valid_primitive(split[0]) and is_valid_datatype(split[1])
    if type_name == 'struct':
        # Get each column by splitting on top-level commas
        for column in top_level_split(string=contents, separator=','):
            # We don't need `top_level_split` here because the first type is a column name
            split = column.split(':', 1)
            if not (len(split) == 2 and is_valid_colname(split[0]) and is_valid_datatype(split[1])):
                return False
        # All columns were valid!
        return True
    if type_name == 'union':
        for entry in top_level_split(string=contents, separator=','):
            if not is_valid_datatype(entry):
                return False
        # All entries were valid!
        return True

    # The type name is not recognized
    return False

我会留给你实施 is_valid_colname 并允许在 struct 秒内发表评论。

请注意,此算法是最坏情况 O(N^2),其中 N 是输入字符串的长度。 is_valid_datatype 被调用 O(N) 次,每次调用 top_level_split 也是 O(N).

如果太慢,可以达到线性时间。每个数据类型都可以被认为是一个 tree ,其节点代表嵌套数据类型。您可以通过保留所有数据类型的堆栈来深度优先搜索输入字符串,这些数据类型是您当前正在解析的数据类型的祖先。例如,每次看到 array< 时,您都会将 array 压入堆栈。每当你看到 >,你就会从堆栈中弹出。这可以在一次扫描字符串中完成。不过,比上面的代码复杂得多。

此外,一般建议:尝试 return False 而不是使用 assert。这样您就不必担心输入无效字符串时代码崩溃。

祝项目顺利!