Как передать двоичный файл в виде стандартного ввода в скрипт Python, контейнеризованный Docker, с помощью argparse?


Обновление на основе ответа Энтони Соттайла


Я повторно реализовал его решение, чтобы упростить проблему. Давайте уберем Docker и Django из уравнения. Цель состоит в том, чтобы использовать Pandas для чтения Excel обоими из следующих методов:

  1. python example.py - < /path/to/file.xlsx
  2. cat /path/to/file.xlsx | python example.py -

где example.py воспроизводится ниже:

import argparse
import contextlib
from typing import IO
import sys
import pandas as pd


@contextlib.contextmanager
def file_ctx(filename: str) -> IO[bytes]:
    if filename == '-':
        yield sys.stdin.buffer
    else:
        with open(filename, 'rb') as f:
            yield f


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('FILE')
    args = parser.parse_args()

    with file_ctx(args.FILE) as input_file:
        print(input_file.read())
        df = pd.read_excel(input_file)
        print(df)


if __name__ == "__main__":
    main()

Проблема в том, что Pandas (см. трассировку ниже) не принимает 2. Однако он отлично работает с 1.

Принимая во внимание, что простая печать текстового представления файла excel работает как в 1., так и в 2.


Если вы хотите легко воспроизвести среду Docker:


Первая сборка образа Docker с именем pandas:

docker build --pull -t pandas - <<EOF
FROM python:latest
RUN pip install pandas xlrd
EOF

Затем используйте образ pandas Docker для запуска: docker run --rm -i -v /path/to/example.py:/example.py pandas python example.py - < /path/to/file.xlsx

Обратите внимание, как он правильно может распечатать текстовое представление файла excel, но pandas не может его прочитать.

Более краткая трассировка, похожая на приведенную ниже:

Traceback (most recent call last):
  File "example.py", line 29, in <module>
    main()
  File "example.py", line 24, in main
    df = pd.read_excel(input_file)
  File "/usr/local/lib/python3.8/site-packages/pandas/util/_decorators.py", line 208, in wrapper
    return func(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/pandas/io/excel/_base.py", line 310, in read_excel
    io = ExcelFile(io, engine=engine)
  File "/usr/local/lib/python3.8/site-packages/pandas/io/excel/_base.py", line 819, in __init__
    self._reader = self._engines[engine](self._io)
  File "/usr/local/lib/python3.8/site-packages/pandas/io/excel/_xlrd.py", line 21, in __init__
    super().__init__(filepath_or_buffer)
  File "/usr/local/lib/python3.8/site-packages/pandas/io/excel/_base.py", line 356, in __init__
    filepath_or_buffer.seek(0)
io.UnsupportedOperation: File or stream is not seekable.

Чтобы показать код, работающий при монтировании файла excel (т.е. не передаваемый стандартным вводом):

docker run --rm -i -v /path/to/example.py:/example.py -v /path/to/file.xlsx:/file.xlsx pandas python example.py file.xlsx


Исходное описание проблемы (для дополнительного контекста)


Возьмем сценарий, в котором в хост-системе у вас есть файл по адресу /tmp/test.txt, и вы хотите использовать для него head, но в контейнере Docker (echo 'Hello World!' > /tmp/test.txt, чтобы воспроизвести пример данных, который у меня есть):

Вы можете запустить:

docker run -i busybox head -1 - < /tmp/test.txt для вывода первой строки на экран:

OR

cat /tmp/test.txt | docker run -i busybox head -1 -

и вывод:

Hello World!

Даже с двоичным форматом, таким как .xlsx, вместо обычного текста, вышеописанное можно сделать, и вы получите какой-то странный вывод, похожий на:

�Oxl/_rels/workbook.xml.rels���j�0
                                  ��}

Дело в том, что head работает как с бинарными, так и с текстовыми форматами даже через абстракцию Docker.

Но в моем собственном CLI на основе argparse (фактически пользовательская команда управления Django, который, как мне кажется, использует argparse), я получаю следующую ошибку при попытке использовать read_excel панды в контексте Docker.

Ошибка, которая печатается, выглядит следующим образом:

Traceback (most recent call last):
  File "./manage.py", line 15, in <module>
    execute_from_command_line(sys.argv)
  File "/opt/conda/lib/python3.7/site-packages/django/core/management/__init__.py", line 381, in execute_from_command_line
    utility.execute()
  File "/opt/conda/lib/python3.7/site-packages/django/core/management/__init__.py", line 375, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/opt/conda/lib/python3.7/site-packages/django/core/management/base.py", line 323, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/opt/conda/lib/python3.7/site-packages/django/core/management/base.py", line 364, in execute
    output = self.handle(*args, **options)
  File "/home/jovyan/sequence_databaseApp/management/commands/seq_db.py", line 54, in handle
    df_snapshot = pd.read_excel(options['FILE'].buffer, sheet_name='Snapshot', header=0, dtype=dtype)
  File "/opt/conda/lib/python3.7/site-packages/pandas/util/_decorators.py", line 208, in wrapper
    return func(*args, **kwargs)
  File "/opt/conda/lib/python3.7/site-packages/pandas/io/excel/_base.py", line 310, in read_excel
    io = ExcelFile(io, engine=engine)
  File "/opt/conda/lib/python3.7/site-packages/pandas/io/excel/_base.py", line 819, in __init__
    self._reader = self._engines[engine](self._io)
  File "/opt/conda/lib/python3.7/site-packages/pandas/io/excel/_xlrd.py", line 21, in __init__
    super().__init__(filepath_or_buffer)
  File "/opt/conda/lib/python3.7/site-packages/pandas/io/excel/_base.py", line 356, in __init__
    filepath_or_buffer.seek(0)
io.UnsupportedOperation: File or stream is not seekable.

Конкретно,

docker run -i <IMAGE> ./manage.py my_cli import - < /path/to/file.xlsx не работает,

но ./manage.py my_cli import - < /path/to/file.xlsx работает!

Каким-то образом в контексте Docker есть разница.

Однако я также отмечаю, даже исключая Docker из уравнения:

cat /path/to/file.xlsx | ./manage.py my_cli import - не работает

хотя:

./manage.py my_cli import - < /path/to/file.xlsx действительно работает (как упоминалось ранее)

Наконец, код, который я использую (вы должны сохранить его как my_cli.py в разделе management/commands, чтобы он работал в проекте Django):

import argparse


import sys


from django.core.management.base import BaseCommand


class Command(BaseCommand):
    help = 'my_cli help'

    def add_arguments(self, parser):
        subparsers = parser.add_subparsers(
            title='commands', dest='command', help='command help')
        subparsers.required = True
        parser_import = subparsers.add_parser('import', help='import help')
        parser_import.add_argument('FILE', type=argparse.FileType('r'), default=sys.stdin)

    def handle(self, *args, **options):
        import pandas as pd
        df = pd.read_excel(options['FILE'].buffer, header=0)
        print(df)

person Dean Kayton    schedule 24.12.2019    source источник


Ответы (2)


Похоже, вы читаете файл в текстовом режиме (FileType('r')/sys.stdin)

В соответствии с этой проблемой bpo argparse не поддерживает открытие двоичных файлов напрямую.

Я бы предложил самостоятельно обрабатывать тип файла с помощью кода, подобного этому (я не знаком с способом django/pandas, поэтому я упростил его до простого python)

import argparse
import contextlib
import io
from typing import IO


@contextlib.contextmanager
def file_ctx(filename: str) -> IO[bytes]:
    if filename == '-':
        yield io.BytesIO(sys.stdin.buffer.read())
    else:
        with open(filename, 'rb') as f:
            yield f


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument('FILE')
    args = parser.parse_args()

    with file_ctx(args.FILE) as input_file:
        # do whatever you need with that input file
person Anthony Sottile    schedule 24.12.2019
comment
См. «Обновление вопроса» на основе вашего ответа. Проблема, с которой я столкнулся, связана с пандами (спасибо, что сузили ее). Я повторно реализовал ваш код, чтобы показать, что файл может быть успешно прочитан через 1. Docker, 2. cat & pipe, 3. ‹. Однако Pandas выдает проблему на 1 и 2, но работает на 3. - person Dean Kayton; 25.12.2019
comment
Я думаю, что решил это. Смотрите ответ ниже. Если вы хотите, вы можете отредактировать свой ответ, чтобы учесть то, что я нашел, и я приму ваш ответ как принятый. Потому что на самом деле вы сделали 99% работы. - person Dean Kayton; 25.12.2019
comment
Ах да, это должно быть доступно для поиска из-за того, как работает xlrd - person Anthony Sottile; 25.12.2019
comment
Принял ваш ответ, потому что он ответил на тему вопроса: Как передать двоичный файл в виде стандартного ввода в скрипт Python, контейнеризованный Docker, с помощью argparse? Настройка, с которой я столкнулся, была незначительной, связанной с библиотекой, даже не упомянутой в вопросе. - person Dean Kayton; 25.12.2019

В значительной степени основан на Anthony Sottile Answer, но с небольшим изменением, которое полностью решает проблему:

import argparse
import contextlib
import io
from typing import IO
import sys

import pandas as pd


@contextlib.contextmanager
def file_ctx(filename: str) -> IO[bytes]:
    if filename == '-':
        yield io.BytesIO(sys.stdin.buffer.read())
    else:
        with open(filename, 'rb') as f:
            yield f


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('FILE')
    args = parser.parse_args()

    with file_ctx(args.FILE) as input_file:
        print(input_file.read())
        df = pd.read_excel(input_file)
        print(df)


if __name__ == "__main__":
    main()

Я понял эту идею после прочтения этого ответа на Pandas 0.25.0 и xlsx из потока содержимого ответа


Как это выглядит с точки зрения контекста исходного вопроса на основе Django:

import contextlib
import io
import sys
from typing import IO

import pandas as pd

from django.core.management.base import BaseCommand


@contextlib.contextmanager
def file_ctx(filename: str) -> IO[bytes]:
    if filename == '-':
        yield io.BytesIO(sys.stdin.buffer.read())
    else:
        with open(filename, 'rb') as f:
            yield f


class Command(BaseCommand):
    help = 'my_cli help'

    def add_arguments(self, parser):
        subparsers = parser.add_subparsers(
            title='commands', dest='command', help='command help')
        subparsers.required = True
        parser_import = subparsers.add_parser('import', help='import help')
        parser_import.add_argument('FILE')

    def handle(self, *args, **options):
        with file_ctx(options['FILE']) as input_file:
            df = pd.read_excel(input_file)
            print(df)
person Dean Kayton    schedule 25.12.2019