Вложение структуры в структуру struct

Вот к примеру такая стуктура в псевдосишном виде

struct {
  uint32 b,
  struct {
      int8 x,
      int8 y
    } xy,
  uint16 a[3]
}

В питоне такая штука подходит для статичной структуры

>>> struct.unpack('<Ibb3H', b'123411121212')
(875770417, 49, 49, 12849, 12849, 12849)

Хочется чтоб оно выдавало в идеале такое

>>> struct.unpack('<I(bb)(H)[3]', b'123411121212')

(875770417, (49, 49), [12849, 12849, 12849])

в struct такого нет, в numpy не нахожу...

Для реального примера формат структуры

FORMATS = [
'I',    'H',    'I',    'B',    'B',    'B',    'B',    'B',
'I',    'i',    'i',    'i',    'f',    'H',    'f',    'f',
'H',    'H',    'H',    'H',    'H',    'H',    'H',    'H',
'H',    'H',    'H',    'H',    'B',    'B',    'B',    'B',
'I',    'I',    'H',    'H',    'I',    'H',    'H',    'H',
'H',    'H',    'H',    'H',    'b',    'b',    'b',    'b',
'b',    'b',    'b',    'b',    'H',    'f',    'H',    'b',
'f',    'H',    'H',    'H',    'H',    'H',    'B',    'B',
'B',    'H',    'I',    'h',    'B',    '8c',   'BB',   'B',
'QQ',   'i',    'H',    'f',    'IHHHbIHHHbIHHHbI', 'b',    'b',    'b',
'b',    'b',    'b',    'Hb',   'Hb',   'Hb',   'Hb',   'Hb',
'Hb',   'Hb',   'Hb',   'Hb',   'Hb',   'BBbBBb',   'BBbBBbBBbBBb', 'BBbBBbBBbBBbBBbBBbBBbBBb',
'BBbBBbBBbBBbBBbBBbBBbBBbBBbBBbBBbBBbBBbBBbBBbBBb', 'B',    'B',    'H',    'B',    'I',    'I',    'B',
'I',    'H',    'hhh',  'H',    'hhh',  'BB',   'BB',   'BB',
'BB',   'BB',   'BB',   'BB',   'BB',   'B',    'H',    'H',
'H',    'B',    'B',    'B',    'B',    'B',    'I',    'I',
'I',    'I',    'I',    'I',    'H',    'H',    'H',    'H',
'H',    'H',    'B',    'B',    'bb',   'bbb',  'h',    'B',
'B',    'BBB',  'H',    'H',    'H',    'H',    'H',    'H',
'H',    'H',    'H',    'H',    'H',    'H',    'H',    'H',
'H',    'H',    'h',    'h',    'h',    'h',    'B',    'B',
'B',    'B',    'H',    'I',    'I',    'I',    'h',    'I',
'h',    'h',    'I',    'h',    'h',    'BB',   'h',    'h',
'h',    'h',    'h',    'h',    'h',    'h',    'H',    'H',
'I',    'I',    'HH',   'HH',   'HHH',  'BH',   'B',    'H',
'H',    'B',    'I',    'BI',   'I',    'I',    '1s',   '1s',
'1s',   '1s',   '1s',   '1s',   '1s',   '1s',   '1s',   '1s',
'1s',   '1s',   '1s',   '1s',   '1s',   '1s',   '2s',   '2s',
'2s',   '2s',   '2s',   '2s',   '2s',   '2s',   '2s',   '2s',
'2s',   '2s',   '2s',   '2s',   '2s',   '4s',   '4s',   '4s',
'4s',   '4s',   '4s',   '4s',   '4s',   '4s',   '4s',   '4s',
'4s',   '4s',   '4s',   '4s',   '8s',   '8s',   '8s' 
]

В пакете передаются обычно штук 10 параметров из этих, остальное пропускается.

Пример данных:

маска fbee30082e080000000e00000000000000000000000000000000000000000000

структура 010000000000113bb1680020630c113bb16830c45501f0a8fb01000000000000000000000000000000000000000000320064008003000000090012000400dd05000000060009000400dd08000000040007000800dd113bb16814281b

содержимое которое сейчас получаю

{
 1: (1,),
 2: (0,),
 3: (1756445457,),
 4: (0,),
 5: (32,),
 7: (99,),
 8: (12,),
 9: (1756445457,),
 10: (22398000,),
 11: (33270000,),
 13: (0.0,),
 14: (0,),
 15: (0.0,),
 19: (0,),
 20: (0,),
 29: (0,),
 35: (0,),
 37: (0,),
 38: (50,),
 39: (100,),
 45: (-128,),
 77: (3, 9, 18, 4, -35, 5, 6, 9, 4, -35, 8, 4, 7, 8, -35, 1756445457),
 78: (20,),
 79: (40,)
}

хочу получать:

{
 1:  1 ,
 2:  0 ,
 3:  1756445457 ,
....
 39:  100 ,
 45:  -128 ,
 77:  ([(3, 9, 18, 4, -35), (5, 6, 9, 4, -35), (8, 4, 7, 8, -35)], 1756445457),
 78:  20 ,
 79:  40 
}

Но вопрос именно как делать структуру в структуре.


Ответы (2 шт):

Автор решения: Vitalizzare ушел в монастырь

Как использовать numpy для работы со вложенными структурами

Исходная структура в Си:

typedef struct {
    unsigned int b;
    struct {
        signed char x;
        signed char y;
    } xy;
    unsigned short a[3];
} MyStruct;

Эта же структура в NumPy:

MyStruct = np.dtype([
    ('b', '<I'), 
    ('xy', [
        ('x', 'b'),
        ('y', 'b')
    ]),
    ('a', '<3H')
])

data = np.void(b'123411121212').view(dtype=MyStruct, type=np.recarray)

print(f'{data      = !s}', 
      f'{data.b    = !s}', 
      f'{data.xy.x = !s}', 
      f'{data.xy.y = !s}', 
      f'{data.a    = !s}', 
      sep='\n')

Результат:

data      = (875770417, (49, 49), [12849, 12849, 12849])
data.b    = 875770417
data.xy.x = 49
data.xy.y = 49
data.a    = [12849 12849 12849]

Здесь:

  • numpy.void - приемник для последовательности байт как скалярной величины;
  • numpy.ndarray.view - интерпретация содержимого через новый тип данных;
  • numpy.dtype - создание нового типа данных;
  • numpy.recarray - апгрейд numpy.ndarray, в котором доступ к полям структуры возможен по их имени через точку вместо квадратных скобок: data.field_name vs. data['field_name'].

Если какое-то поле нужно описать как массив, то его размерность задаётся третьим параметром кортежа. Пример:

# Поле по имени 'my_field' содержит массив 15×15×3 целых чисел без знака
np.dtype([('my_field', '<I', (15, 15, 3)), ...])

Доступ к ячейкам массива тот же, что и к ячейкам numpy.ndarray - через индекс после обращения к полю: data['my_field'][0, :, 1]. Но совмещать в одном обращении имя поля и индексы ячеек нельзя - обращение data['my_field', 0, :, 1] выбросит ошибку IndexError.

Относительно простые структуры с вложениями однотипных массивов можно записать в одну строку. Для полей автоматически создаются имена вида 'f0', 'f1', 'f2', .... Пример:

# field 0: unsigned integer, 4 bytes
# field 1: 2×3 array of signed chars, 6 bytes
# field 2: 3 unsigned shorts, 6 bytes

my_type = np.dtype('<I, (2, 3)b, <3H')

raw_data = np.void(b'\xFF\x00\x00\x00\x01\x02\x03\x04\x05\x06\x0A\x00\x0B\x00\x0C\x00')
data = raw_data.view(dtype = my_type, type=np.recarray)

print('Содержимое:',
      f'{data    = !s}', 
      f'{data.f0 = !s}', 
      f'data.f1 =\n{data.f1}', 
      f'{data.f2 = !s}', 
      '\nПримеры доступа к ячейкам:',
      f'{data[1][0, 0]    = !s}', 
      f'{data.f1[0, 0]    = !s}',     # numpy.recarray
      f"{data['f1'][:, 1] = !s}",
      f"{data['f1'][0, 0] = !s}",
      f'{data[2][0]       = !s}', 
      f'{data.f2[0]       = !s}',     # numpy.recarray
      f"{data['f2'][0]    = !s}",
      sep='\n')
Содержимое:
data    = (255, [[1, 2, 3], [4, 5, 6]], [10, 11, 12])
data.f0 = 255
data.f1 =
[[1 2 3]
 [4 5 6]]
data.f2 = [10 11 12]

Примеры доступа к ячейкам:
data[1][0, 0]    = 1
data.f1[0, 0]    = 1
data['f1'][:, 1] = [2 5]
data['f1'][0, 0] = 1
data[2][0]       = 10
data.f2[0]       = 10
data['f2'][0]    = 10

Документация

→ Ссылка
Автор решения: Vitalizzare ушел в монастырь

Пример работы с динамически изменяемыми структурами

Задан фиксированный список из 255 форматов и маска из 32 байт (используются первые 255 бит). По маске выбираются используемые форматы. Отобранные форматы применяются в той последовательности, как они встречаются в списке, ко входной последовательности байт.

На выходе нужно получить словарь, ключи которого - это положения форматов в списке (индекс + 1), а значения - извлеченные по ним данные.

Для решения задачи используем NumPy. Первым делом, перепишем сложные форматы в понятном для NumPy виде. Например, "bbb" может быть "b,b,b" (три отдельных байтовых поля), "3b" (одно поле с тремя байтовыми ячейками), "b,2b", "2b,b". Также нужно учесть, что символы форматов в struct, используемые в условии задачи, не во всём совпадают с numpy. Например:

  • малую 's' из struct понадобится перевести в большую 'S' из numpy;
  • длина целого 'l', 'L' равна 4 байтам в struct и 8 в numpy, их нужно заменить на 'i', 'I' - целое число из 4 байт;
  • символы выравнивания и порядка '@!' и 'x' (pad byte) неизвестны в NumPy;
  • символ порядка байтов (endianness) в struct указывается один раз в начале формата, а в numpy - для каждого поля, где это имеет смысл.

Для текущего примера актуально только первое замечание, остальные можем проигнорировать.

Вложенные структуры придётся развернуть детальнее. В примере из вопроса возьмем структуру FORMATS[76], равную 'IHHHbIHHHbIHHHbI'. По ней ожидается результат вида:

([(3, 9, 18, 4, -35), (5, 6, 9, 4, -35), (8, 4, 7, 8, -35)], 1756445457)

Чтобы получить такой результат с помощью NumPy, структуру нужно переписать так:

[('', 'I,H,H,H,b', (3,)), ('', 'I')]

Здесь имена полей не важны, поэтому оставляем их в виде пустой строки - им автоматически подберутся имена 'f0', 'f1', 'f2', ....

Минимальное преобразование форматов из примера может выглядеть так:

# Исходные форматы структур данных
formats = ['I', 'H', 'I', 'B', 'B', 'B', 'B', 'B', 'I', 'i', 'i', 'i', 'f', 'H', 'f', 'f', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'B', 'B', 'B', 'B', 'I', 'I', 'H', 'H', 'I', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'H', 'f', 'H', 'b', 'f', 'H', 'H', 'H', 'H', 'H', 'B', 'B', 'B', 'H', 'I', 'h', 'B', '8c', 'BB', 'B', 'QQ', 'i', 'H', 'f', 'IHHHbIHHHbIHHHbI', 'b', 'b', 'b', 'b', 'b', 'b', 'Hb', 'Hb', 'Hb', 'Hb', 'Hb', 'Hb', 'Hb', 'Hb', 'Hb', 'Hb', 'BBbBBb', 'BBbBBbBBbBBb', 'BBbBBbBBbBBbBBbBBbBBbBBb', 'BBbBBbBBbBBbBBbBBbBBbBBbBBbBBbBBbBBbBBbBBbBBbBBb', 'B', 'B', 'H', 'B', 'I', 'I', 'B', 'I', 'H', 'hhh', 'H', 'hhh', 'BB', 'BB', 'BB', 'BB', 'BB', 'BB', 'BB', 'BB', 'B', 'H', 'H', 'H', 'B', 'B', 'B', 'B', 'B', 'I', 'I', 'I', 'I', 'I', 'I', 'H', 'H', 'H', 'H', 'H', 'H', 'B', 'B', 'bb', 'bbb', 'h', 'B', 'B', 'BBB', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'H', 'h', 'h', 'h', 'h', 'B', 'B', 'B', 'B', 'H', 'I', 'I', 'I', 'h', 'I', 'h', 'h', 'I', 'h', 'h', 'BB', 'h', 'h', 'h', 'h', 'h', 'h', 'h', 'h', 'H', 'H', 'I', 'I', 'HH', 'HH', 'HHH', 'BH', 'B', 'H', 'H', 'B', 'I', 'BI', 'I', 'I', '1s', '1s', '1s', '1s', '1s', '1s', '1s', '1s', '1s', '1s', '1s', '1s', '1s', '1s', '1s', '1s', '2s', '2s', '2s', '2s', '2s', '2s', '2s', '2s', '2s', '2s', '2s', '2s', '2s', '2s', '2s', '4s', '4s', '4s', '4s', '4s', '4s', '4s', '4s', '4s', '4s', '4s', '4s', '4s', '4s', '4s', '8s', '8s', '8s']

# Заменить проблемные символы
formats = [f.replace('s', 'S') for f in formats]

# Разделить поля запятой, чтобы форматы были применимы в numpy
# (ситуативное решение за неимением примеров ожидаемого результата)
formats = [','.join(re.findall(r'[0-9]*[a-zA-Z]', f)) for f in formats]

# Переделать формат №77 по заданному примеру
formats[76] = [('', 'I,H,H,H,b', (3,)), ('', 'I')]

# Сохранить позицию формата как имя будущего поля
# (используем при создании конечного словаря)
formats = [(str(i), f) for i, f in enumerate(formats, start=1)]

Следующим шагом формируем по заданной маске структуру входных данных:

hex_mask = 'fbee30082e080000000e00000000000000000000000000000000000000000000'
mask = int(hex_mask, base=16)

selected_formats_indices = [i for i in range(len(formats)) if (mask & 2**(255-i))]
selected_formats = [formats[i] for i in selected_formats_indices]
data_type = np.dtype(selected_formats)

Принимаем входные данные и проверяем применимость к ним созданной структуры:

# Последний байт данных закомментирован как служебный (см. комментарий к ответу)

hex_data = '010000000000113bb1680020630c113bb16830c45501f0a8fb01000000000000000000000000000000000000000000320064008003000000090012000400dd05000000060009000400dd08000000040007000800dd113bb1681428'   # '1b'
raw_data = np.void(bytes.fromhex(hex_data))

assert data_type.itemsize == raw_data.itemsize, f"Can't apply type of size {data_type.itemsize} to data of size {data.itemsize}"

Может понадобиться пройтись вглубь данных, чтобы свести элементы вложенных структур к пайтоновским типам:

def to_python(npdata):
    if isinstance(npdata, (np.ndarray, np.generic)):
        data = npdata.tolist()
        # если тип данных не `void`, то вложенных структур нет
        return data if npdata.dtype.char != 'V' else to_python(data)
    if isinstance(npdata, (list, tuple)):
        # `tolist` возвращает `tuple` на скалярных величинах
        # сложной структуры и `list` на массивах; проверяем их 
        # на наличие более глубоких вложенных структур
        return type(npdata)(map(to_python, npdata))
    return npdata

Конечное преобразование:

data = raw_data.view(data_type)
keys = list(map(int, data_type.names))    # номера форматов в именах полей
values = to_python(data)                  # приводим данные к пайтоновским типам
answer = dict(zip(keys, values))

print(answer)
{1: 1,
 2: 0,
 3: 1756445457,
 4: 0,
 5: 32,
 7: 99,
 8: 12,
 9: 1756445457,
 10: 22398000,
 11: 33270000,
 13: 0.0,
 14: 0,
 15: 0.0,
 19: 0,
 20: 0,
 29: 0,
 35: 0,
 37: 0,
 38: 50,
 39: 100,
 45: -128,
 77: ([(3, 9, 18, 4, -35), (5, 6, 9, 4, -35), (8, 4, 7, 8, -35)], 1756445457),
 78: 20,
 79: 40}
→ Ссылка