Обновление на основе ответа Энтони Соттайла
Я повторно реализовал его решение, чтобы упростить проблему. Давайте уберем Docker и Django из уравнения. Цель состоит в том, чтобы использовать Pandas для чтения Excel обоими из следующих методов:
python example.py - < /path/to/file.xlsx
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)