Ruby — распаковать массив со смешанными типами

Я пытаюсь использовать unpack для декодирования двоичного файла. Бинарный файл имеет следующую структуру:

ABCDEF\tFFFABCDEF\tFFFF....

куда

ABCDEF -> String of fixed length
\t -> tab character
FFF -> 3 Floats
.... -> repeat thousands of times

Я знаю, как это сделать, когда все типы одинаковы или содержат только числа и массивы фиксированной длины, но в этой ситуации я борюсь. Например, если бы у меня был список поплавков, я бы сделал

s.unpack('F*')

Или если бы у меня были целые числа и числа с плавающей запятой, например

[1, 3.4, 5.2, 4, 2.3, 7.8]

я бы сделал

s.unpack('CF2CF2')

Но в этом случае я немного теряюсь. Я надеялся использовать строку формата, такую ​​как `(CF2)*' с квадратными скобками, но это не сработало.

Мне нужно использовать Ruby 2.0.0-p247, если это имеет значение

Пример

ary = ["ABCDEF\t", 3.4, 5.6, 9.1, "FEDCBA\t", 2.5, 8.9, 3.1]
s = ary.pack('P7fffP7fff')

тогда

s.scan(/.{19}/)
["\xA8lf\xF9\xD4\x7F\x00\x00\x9A\x99Y@33\xB3@\x9A\x99\x11", "A\x80lf\xF9\xD4\x7F\x00\x00\x00\x00 @ff\x0EAff"]

Ну наконец то

s.scan(/.{19}/).map{ |item| item.unpack('P7fff') }
Error: #<ArgumentError: no associated pointer>
<main>:in `unpack'
<main>:in `block in <main>'
<main>:in `map'
<main>:in `<main>'

Проблема в P7, попробуйте заменить p только строчными буквами (№ 7). Есть некоторые отличия при упаковке/распаковке. При чтении файла вы используете P7, потому что он не заканчивается нулем, но при повторной упаковке он есть. Я просто использовал пример без ошибок, упаковав P7fffP7fff и распаковав pfffpfff.

ForeverZer0 12.06.2019 08:16

я получаю ту же ошибку

Rojj 12.06.2019 08:18

В вашем примере используется массив, в котором каждый элемент уже разделен, поэтому вы будете использовать нижний p. При чтении файла это будет строка байтов без разделения на элементы массива, поэтому вы должны указать фиксированную длину с вариантом в верхнем регистре P7.

ForeverZer0 12.06.2019 08:25

OK. Я попробую сегодня вечером, когда вернусь домой и получу доступ к файлу.

Rojj 12.06.2019 08:56

@ForeverZer0: Проблема и в p, и в P.

Eric Duminil 12.06.2019 10:47
Пошаговое руководство по созданию собственного Slackbot: От установки до развертывания
Пошаговое руководство по созданию собственного Slackbot: От установки до развертывания
Шаг 1: Создание приложения Slack Чтобы создать Slackbot, вам необходимо создать приложение Slack. Войдите в свою учетную запись Slack и перейдите на...
2
5
567
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

При работе со смешанными форматами, которые повторяются и имеют известный фиксированный размер, часто проще сначала разбить строку,

Быстрый пример:

binary.scan(/.{LENGTH_OF_DATA}/).map { |item| item.unpack(FORMAT) }

Учитывая приведенный выше пример, возьмите длину строки, включая символ табуляции (в байтах), плюс размер 3 поплавков. Если ваши строки буквально 'ABCDEF\t', вы должны использовать размер 19 (7 для строки, 12 для 3 поплавков).

Ваш конечный продукт будет выглядеть так:

str.scan(/.{19}/).map { |item| item.unpack('P7fff') }

Например:

irb(main):001:0> ary = ["ABCDEF\t", 3.4, 5.6, 9.1, "FEDCBA\t", 2.5, 8.9, 3.1]
=> ["ABCDEF\t", 3.4, 5.6, 9.1, "FEDCBA\t", 2.5, 8.9, 3.1]

irb(main):002:0> s = ary.pack('pfffpfff')
=> "\xE8Pd\xE4eU\x00\x00\x9A\x99Y@33\xB3@\x9A\x99\x11A\x98Pd\xE4eU\x00\x00\x00\x00 @ff\x0EAffF@"

irb(main):003:0> s.unpack('pfffpfff')
=> ["ABCDEF\t", 3.4000000953674316, 5.599999904632568, 9.100000381469727, "FEDCBA\t", 2.5, 8.899999618530273, 3.0999999046325684]

Небольшие различия в точности неизбежны, но не беспокойтесь об этом, так как они возникают из-за разницы между 32-битным числом с плавающей запятой и 64-битным значением типа double (то, что Ruby использует внутри), и разница в точности будет меньше значимой для 32-битное число с плавающей запятой.

Красиво, но у меня проблема. Я прочитал строку из файла с помощью File.binread'. This gives me a String`, а String не имеет метода each_slice. Я пытался преобразовать его в bytes или chars, но это дает мне массивы, а распаковка не работает с массивами. Работает ли each_slice со строкой для Ruby 2.0.0?

Rojj 12.06.2019 07:56

Приношу свои извинения, я исправил ответ, чтобы использовать String#scan вместо each_slice. В качестве альтернативы вы можете использовать str.chars.each_slice, но scan — более чистый подход, IMO.

ForeverZer0 12.06.2019 08:04

Я получаю no associated pointer. Я добавил пример, поэтому мы смотрим на одно и то же.

Rojj 12.06.2019 08:14
p / P здесь неправильная директива. Используйте a, как в a6x (x, чтобы игнорировать символ табуляции), поскольку P7 означает семь указателей на строки Ruby.
cremno 12.06.2019 09:10

Строчная p означает, что, а прописная P указывает на размер структуры. Легко продемонстрировано с помощью ['stack', 'overflow'].pack('pp').unpack('P5P8'). Он не пытается распаковать 13 указателей.

ForeverZer0 12.06.2019 09:24

Проблема в том, что pack('pfff') не создает 19-байтовую двоичную строку.

Eric Duminil 12.06.2019 10:25

Будет добавлен нулевой байт, но вопрос ОП касается чтения из файла, весь нижний регистр p - это просто пояснение к его примеру. «a» будет лучшим вариантом, если он записывает его обратно, чтобы избежать включения нулевого терминатора.

ForeverZer0 12.06.2019 11:16

@ForeverZer0: Все, что я хочу сказать, это то, что str.scan(/.{19}/).map { |item| item.unpack('P7fff') } неправильно и не будет работать с данными OP. Я был бы рад вернуть отрицательный голос, если вы измените шаблон. Есть ли какая-то конкретная причина, по которой вы проголосовали за мой ответ?

Eric Duminil 12.06.2019 11:38

Наконец, может быть не очень хорошей идеей читать большой файл в памяти и создавать большой массив с помощью scan, если можно обрабатывать данные последовательно.

Eric Duminil 12.06.2019 11:42

Согласен, хлебать весь файл - плохая практика, можно потом отредактировать, мобильный банкомат. Я действительно не минусовал тебя. Я только когда-либо отрицаю кого-либо, если это объективно неверно, никогда только из-за стиля и / или эффективности и т. д.

ForeverZer0 12.06.2019 12:24
Ответ принят как подходящий

Вы можете прочитать файл небольшими порциями по 19 байт и использовать 'A7fff' для упаковки и распаковки. Не используйте указатели для структурирования ('p' и 'P'), так как для кодирования вашей информации им требуется более 19 байт. Вы также можете использовать 'A6xfff', чтобы игнорировать 7-й байт и получить строку из 6 символов.

Вот пример, похожий на документацию IO.read:

data = [["ABCDEF\t", 3.4, 5.6, 9.1], 
        ["FEDCBA\t", 2.5, 8.9, 3.1]]
binary_file = 'data.bin'
chunk_size = 19
pattern = 'A7fff'

File.open(binary_file, 'wb') do |o|
  data.each do |row|
    o.write row.pack(pattern)
  end
end

raise "Something went wrong. Please check data, pattern and chunk_size." unless File.size(binary_file) == data.length * chunk_size

File.open(binary_file, 'rb') do |f|
  while record = f.read(chunk_size)
    puts '%s %g %g %g' % record.unpack(pattern)
  end
end
# =>
#    ABCDEF   3.4 5.6 9.1
#    FEDCBA   2.5 8.9 3.1

Вы можете использовать кратное 19, чтобы ускорить процесс, если ваш файл большой.

A7fff справился. Даже без записи в файл я могу распаковать его с помощью s.scan(/.{19}/).map{ |item| item.unpack('A7fff') }
Rojj 12.06.2019 19:00

@Rojj: Конечно, вам не нужно ничего писать, если у вас уже есть данные. Это было просто для того, чтобы иметь общие двоичные данные для отладки и тестирования. scan тоже работает, но для этого нужно иметь весь файл в памяти, что может не подойти, если вы работаете с большими файлами.

Eric Duminil 12.06.2019 19:13

@Rojj: Если вам не нужен последний символ строки, вы также можете использовать 'A6xfff', как в ["ABCDEF\t", 3.4, 5.6, 9.1].pack('A7fff').unpack('A6xfff')

Eric Duminil 12.06.2019 19:18

О, это здорово! Спасибо

Rojj 12.06.2019 19:19

Другие вопросы по теме

Похожие вопросы

Проверьте, есть ли в родительской записи какие-либо дочерние записи с start_date в будущем, затем остановите обновление рельсов
Как я могу скрыть модальную форму начальной загрузки после успешной отправки данных формы в рельсы
Существует ли метод Ruby для вывода объекта на консоль в удобном для чтения виде?
Как я могу убедиться, что пользователи вводят число, а не строку?
Как читать несколько файлов XML, а затем выводить в несколько файлов CSV с одинаковыми именами файлов XML
Как предотвратить преобразование имен атрибутов в нижний или нижний регистр для компонента Vue с помощью Jeykll
Вызов функций PostGIS в Ruby
Rails генерирует выбор как с параметрами, так и с группами параметров
Имена полей FieldPath не могут содержать '.' при попытке использовать АГРЕГАТ
Создать пользовательскую кнопку link_to и получить неправильную ошибку маршрутизации ruby