Есть ли способ добавить индикатор выполнения (например, tqdm) в функцию PyYAML yaml.load()?

Используя PyYAML с CLoader в качестве синтаксического анализатора YAML, я пытаюсь загрузить файл YAML, разобрать его и записать в отдельный файл.

В целях тестирования я использую очень большой файл YAML, больше, чем 1GB.

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

Вот мой текущий код:

import yaml
import argparse

from tqdm import tqdm
from yaml import CLoader as Loader

def main():

parser = argparse.ArgumentParser(description='Takes in YAML files and uploads straight to Neo4J database')
parser.add_argument('-f', '--files', nargs='+', metavar='', required=True,
                    help='<Required> One or more YAML files to upload')

args = parser.parse_args()

for file_name in args.files:

    with open(file_name, 'r') as stream:
        print("Reading input file...")
        with open('test2.txt', 'w') as wf:
            print("Writing to output file...")

            try:
                for data in tqdm(yaml.load(stream, Loader=Loader)):
                    wf.write(data.get('primaryName') + '\n')
                    wf.write('++++++++++\n')
            except yaml.YAMLError as exc:
                print(exc)

if __name__ == "__main__":
    main()

Теперь происходит то, что индикатор выполнения tqdm отображается для цикла записи данных, но не для процесса yaml.load(), который занимает больше всего времени.

То есть долгое время не показывается индикатор выполнения, пока файл YAML не загрузится полностью.

Я надеюсь найти решение, позволяющее обернуть индикатор выполнения вокруг функции, к которой у меня нет доступа, в данном случае yaml.load().

Я делаю что-то неправильно? Любой совет будет большим и оцененным.


person Jaron C.    schedule 06.09.2018    source источник


Ответы (1)


Нет, невозможно обернуть индикатор выполнения вокруг кода, к которому у вас нет доступа.

Кроме того, вы можете использовать интерфейс на основе iterable для tqdm только при зацикливании. над итерируемым объектом, которого здесь нет. Таким образом, вы должны использовать интерфейс на основе update:

with tqdm(total=100) as pbar:
    for i in range(10):
        pbar.update(10)

Вопрос в том, как заставить PyYAML вызывать это pbar.update?

В идеале вам нужно найти место для подключения процесса загрузки, где вы можете вызвать pbar.update. Если это невозможно, вам придется сделать что-то уродливое (например, разветвить PyYAML и добавить к его API или сделать то же самое во время выполнения, исправив его), или переключиться на другую библиотеку. Но это должно быть возможно.


Очевидным вариантом является создание собственного подкласса PyYAML.Loader. Документы для PyYAML объясняют API для этого класса, поэтому вы можете переопределить любой из методов, чтобы передать некоторый прогресс, а затем super в базовый класс.

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

Одна вещь, которую вы могли бы сделать, это заставить ваш Loader подкласс вызвать tell на его stream, чтобы выяснить, сколько байтов вы уже прочитали.

У меня нет PyYAML на этом компьютере, а документы довольно запутанные, поэтому вам, вероятно, придется немного поэкспериментировать, но должно получиться что-то вроде этого:

class ProgressLoader(yaml.CLoader):
    def __init__(self, stream, callback):
        super().__init__(stream)
        # __ because who knows what names the base class is using?
        self.__stream = stream
        self.__pos = 0
        self.__callback = callback
    def get_token(self):
        result = super().get_token()
        pos = self.__stream.tell()
        self.__callback(pos - self.__pos)
        self.__pos = pos
        return result

Но тогда я не уверен, как заставить PyYAML передать ваш обратный вызов в конструктор ProgressLoader, поэтому вам придется сделать что-то вроде этого:

with open(file_name, 'r') as stream:
    size = os.stat(stream.fileno()).st_size
    with tqdm(total=size) as progress:
        factory = lambda stream: ProgressLoader(stream, progress.update)
        data = yaml.load(stream, Loader=factory)

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

Документация для файловых объектов довольно плотная< /em>, но, по крайней мере, они понятны, а на самом деле работа довольно проста:

class ProgressFileWrapper(io.TextIOBase):
    def __init__(self, file, callback):
        self.file = file
        self.callback = callback
    def read(self, size=-1):
        buf = self.file.read(size)
        if buf:
            self.callback(len(buf))
        return buf
    def readline(self, size=-1):
        buf = self.file.readline(size)
        if buf:
            self.callback(len(buf))
        return buf

В настоящее время:

with open(file_name, 'r') as stream:
    size = os.stat(stream.fileno()).st_size
    with tqdm(total=size) as progress:
        wrapper = ProgressFileWrapper(stream, progress.update)
        data = yaml.load(wrapper, Loader=Loader)

Конечно, это не идеально. Здесь мы предполагаем, что вся работа заключается в чтении файла с диска, а не в его анализе. Это, вероятно, достаточно близко к истине, чтобы нам это сошло с рук, но если это не так, у вас будет один из тех индикаторов выполнения, который приближается почти к 100%, а затем просто бесполезно остается там в течение длительного времени. 1


1. Это не только ужасно раздражает, но и настолько тесно связано с Windows и другими продуктами Microsoft, что они, вероятно, могут подать на вас в суд за кражу их внешнего вида. :)

person abarnert    schedule 06.09.2018
comment
Спасибо за комплексное решение, оно работает! Действительно, моей первой ошибкой было использование итерируемого интерфейса, но потом я не мог понять, когда использовать update(). Ваш класс-обертка действительно гениален! Я думал о обертке, но не мог понять, как это сделать (я новичок в Python). Теперь у меня есть еще один вопрос, чтобы лучше понять процесс, происходящий в оболочке (которая наследуется от «io.TextIOBase»): правильно ли говорить, что вы перегружаете функции «чтения», так что функция обратного вызова (т.е. обновление) вводится как «поток» читается? - person Jaron C.; 07.09.2018
comment
@ДжаронС. да. Но обратите внимание, что мой класс является не только подклассом TextIOBase, но и прокси для self.file, который является еще одним TextIOBase. Поэтому вместо обычного super().read(size) приходится делать self.file.read(size). Это довольно распространенная идиома в Python, но поначалу может быть немного сложно понять. - person abarnert; 07.09.2018