Объединение файлов данных с PyInstaller (--onefile)


110

Я пытаюсь создать однофайловый EXE с PyInstaller, который должен включать изображение и значок. Я не могу, хоть убей, заставить его работать --onefile.

Если я это сделаю, --onedirвсе будет работать очень хорошо. Когда я использую --onefile, он не может найти дополнительные файлы, на которые есть ссылки (при запуске скомпилированного EXE). Он находит DLL и все остальное в порядке, но не два изображения.

Я просмотрел временный каталог, созданный при запуске EXE ( \Temp\_MEI95642\например), и файлы действительно там. Когда я помещаю EXE в этот временный каталог, он их находит. Очень непонятно.

Это то, что я добавил в .specфайл

a.datas += [('images/icon.ico', 'D:\\[workspace]\\App\\src\\images\\icon.ico',  'DATA'),
('images/loaderani.gif','D:\\[workspace]\\App\\src\\images\\loaderani.gif','DATA')]     

Я должен добавить, что я также пытался не помещать их во вложенные папки, но это не имело значения.

Изменить: помечен новый ответ как правильный из-за обновления PyInstaller.


11
Спасибо большое! строка здесь ( a.datas += ...) действительно помогла мне только что. в документации по pyinstaller говорится об использовании, COLLECTно при использовании не удается поместить файлы в двоичный файл--onefile
Игорь Серебряный

@IgorSerebryany: Прикомандированный! У меня была точно такая же проблема.
Hubro

Мой .exe вылетает, когда я нажимаю на строку меню, если я использовал
iacopo

1
Учтите, что временная папка исчезает после завершения выполнения, поэтому, чтобы проверить, что внутри, поместите listdir sys._MEIPASS в главную
hithwen

Есть ли также способ использования синтаксиса Tree (), который я, кажется, видел повсюду?
fatuhoku

Ответы:


153

Более новые версии PyInstaller больше не устанавливают envпеременную, поэтому отличный ответ Шиша не сработает. Теперь путь устанавливается как sys._MEIPASS:

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    try:
        # PyInstaller creates a temp folder and stores path in _MEIPASS
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

10
Я использую pyinstaller 2 с python 2.7, и у меня нет _MEIPASS2envs, но sys._MEIPASSработает хорошо, поэтому +1. Я предлагаю:path = getattr(sys, '_MEIPASS', os.getcwd())
kbec

2
+1. Спасибо за это. Пытаясь какое-то время использовать переменную среды _MEIPASS2, очевидно, я написал свой код, когда она все еще была переменной среды, потому что я помню, как она работала. Внезапно он остановился, когда я недавно перекомпилировал.
Favil Orbedios

9
Я бы порекомендовал поймать AttributeErrorвместо более общего Exception. Я столкнулся с проблемами, когда мне не хватало импорта sys, а путь по умолчанию был установлен os.path.abspath.
Аарон,

Возможно, вы сможете помочь решить мою проблему .
Филлип

1
Использование текущего рабочего каталога в случае, когда _MEIPASS не задано, неправильно. Смотрите мой ответ .
Джонатон Рейнхарт

52

pyinstaller распаковывает ваши данные во временную папку и сохраняет этот путь к каталогу в _MEIPASS2переменной среды. Чтобы получить _MEIPASS2каталог в упакованном режиме и использовать локальный каталог в распакованном (разработке) режиме, я использую это:

def resource_path(relative):
    return os.path.join(
        os.environ.get(
            "_MEIPASS2",
            os.path.abspath(".")
        ),
        relative
    )

Вывод:

# in development
>>> resource_path("app_icon.ico")
"/home/shish/src/my_app/app_icon.ico"

# in production
>>> resource_path("app_icon.ico")
"/tmp/_MEI34121/app_icon.ico"

10
Как, когда и где это использовать?
gseattle

4
Вы должны использовать этот сценарий в файле .py, который вы пытаетесь скомпилировать с помощью PyInstaller. Не помещайте этот фрагмент кода в файл .spec, это не сработает. Получите доступ к своим файлам, подставив путь, который вы обычно вводите resource_path("file_to_be_accessed.mp3"). Будьте осторожны, используйте max 'answer для текущей версии PyInstaller.
Exeleration-G

1
Этот ответ относится к использованию --one-fileопции?
fatuhoku

Возможно, вы сможете помочь решить мою проблему .
Филлип

Использование текущего рабочего каталога в случае, когда _MEIPASS не задано, неправильно. Смотрите мой ответ .
Джонатон Рейнхарт

30

Все остальные ответы используют текущий рабочий каталог в случае, если приложение не установлено PyInstalled (т.е. sys._MEIPASSне установлено). Это неправильно, так как это не позволяет вам запустить приложение из каталога, отличного от того, в котором находится ваш скрипт.

Лучшее решение:

import sys
import os

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, relative_path)

Спасибо, Джон, ваша функция помогла мне сегодня решить проблему. У меня просто вопрос. Почему некоторые пишут _MEIPASS2 вместо _MEIPASS? Когда я пытался добавить 2 раньше, это не сработало.
Ахмед АбдельХалек

1
Я думаю os.env['_MEIPASS2'](с использованием среды ОС) был старый метод получения каталога. AFAIK sys._MEIPASS- единственный правильный метод сейчас.
Джонатон Рейнхарт

Здравствуйте, ваше решение отлично работает, когда оно resource_path()находится в основном скрипте. Есть ли способ заставить его работать, если вместо этого он написан в модуле? На данный момент он будет пытаться получить ресурсы из папки, в которой находится модуль, а не из основной.
Guimoute

1
@Guimoute похоже, что код в этом ответе работает так, как задумано: он локализует ресурсы в модуле, который их предоставляет, потому что он использует __file__. Если вы хотите, чтобы модуль имел возможность доступа к ресурсам за пределами его собственной области действия, вам необходимо реализовать, чтобы код импорта предоставлял путь к ним для использования вашим модулем, например, с помощью модуля, предоставляющего вызов "set_resource_location ()", который вызовы импортера - не думайте, что это отличается от того, как вы должны работать, когда не используете pyinstaller.
barny

@barny Спасибо за подсказку! Я попробую что-нибудь в этом роде.
Guimoute

15

Возможно, я пропустил шаг или сделал что-то не так, но приведенные выше методы не объединяли файлы данных с PyInstaller в один exe-файл. Позвольте мне рассказать о том, что я сделал.

  1. Шаг: запишите один из вышеуказанных методов в свой файл py с импортом модулей sys и os. Я пробовал их обоих. Последний:

    def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
        base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
        return os.path.join(base_path, relative_path)
  2. Шаг: Запишите pyi-makepec file.py в консоль, чтобы создать файл file.spec.

  3. Шаг: Откройте file.spec с помощью Notepad ++, чтобы добавить файлы данных, как показано ниже:

    a = Analysis(['C:\\Users\\TCK\\Desktop\\Projeler\\Converter-GUI.py'],
                 pathex=['C:\\Users\\TCK\\Desktop\\Projeler'],
                 binaries=[],
                 datas=[],
                 hiddenimports=[],
                 hookspath=[],
                 runtime_hooks=[],
                 excludes=[],
                 win_no_prefer_redirects=False,
                 win_private_assemblies=False,
                 cipher=block_cipher)
    #Add the file like the below example
    a.datas += [('Converter-GUI.ico', 'C:\\Users\\TCK\\Desktop\\Projeler\\Converter-GUI.ico', 'DATA')]
    pyz = PYZ(a.pure, a.zipped_data,
         cipher=block_cipher)
    exe = EXE(pyz,
              a.scripts,
              exclude_binaries=True,
              name='Converter-GUI',
              debug=False,
              strip=False,
              upx=True,
              #Turn the console option False if you don't want to see the console while executing the program.
              console=False,
              #Add an icon to the program.
              icon='C:\\Users\\TCK\\Desktop\\Projeler\\Converter-GUI.ico')
    
    coll = COLLECT(exe,
                   a.binaries,
                   a.zipfiles,
                   a.datas,
                   strip=False,
                   upx=True,
                   name='Converter-GUI')
  4. Шаг: Я выполнил вышеуказанные шаги, а затем сохранил файл спецификации. Наконец открыл консоль и напишите pyinstaller file.spec (в моем случае file = Converter-GUI).

Вывод: в папке dist по-прежнему больше одного файла.

Примечание: я использую Python 3.5.

РЕДАКТИРОВАТЬ: Наконец, он работает с методом Джонатана Рейнхарта.

  1. Шаг: Добавьте приведенные ниже коды в свой файл Python с импортом sys и os.

    def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
        base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
        return os.path.join(base_path, relative_path)
  2. Шаг: вызовите указанную выше функцию, добавив путь к вашему файлу:

    image_path = resource_path("Converter-GUI.ico")
  3. Шаг: напишите указанную выше переменную, которая вызывает функцию туда, где вашим кодам нужен путь. В моем случае это:

        self.window.iconbitmap(image_path)
  4. Шаг: откройте консоль в том же каталоге, что и ваш файл python, напишите коды, как показано ниже:

        pyinstaller --onefile your_file.py
  5. Шаг: Откройте файл .spec файла python, добавьте массив a.datas и добавьте значок в класс exe, который был указан выше перед редактированием на 3-м шаге.
  6. Шаг: Сохраните и выйдите из файла пути. Перейдите в свою папку, которая включает файл спецификации и py. Снова откройте окно консоли и введите следующую команду:

        pyinstaller your_file.spec

После шага 6. ваш единственный файл готов к использованию.


Спасибо @dilde! Не то, чтобы a.datasмассив из шага 5 принимает кортежи строк, см. Pythonhosted.org/PyInstaller/spec-files.html#adding-data-files для справки.
Ян Кэмпбелл

Извините, я не могу понять часть вашего сообщения из-за моего слабого английского. Эта часть - «Не то, что a.datas ....». Вы хотели написать «Обратите внимание, что a.datas ...» Что-то не так с тем, что я написал о добавлении строк в массив a.datas?
dildeolupbiten 05

Ой! Следует отметить, что ... извините за опечатку. Ваши шаги отлично сработали для меня :), я не смог найти эту информацию ни в их документации, ни где-либо еще.
Ян Кэмпбелл

8

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

if getattr(sys, 'frozen', False):
    os.chdir(sys._MEIPASS)

Просто добавьте эти две строки в начало вашего кода, остальные можете оставить как есть.


2
Нет. Хорошие приложения должны редко менять рабочий каталог. Есть способы получше.
Джонатон Рейнхарт

3

Небольшое изменение принятого ответа.

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative_path)

    return os.path.join(os.path.abspath("."), relative_path)

Использование текущего рабочего каталога в случае, когда _MEIPASS не задано, неправильно. Смотрите мой ответ .
Джонатон Рейнхарт

2

Используя отличный ответ от Макса и этот пост о добавлении дополнительных файлов данных, таких как изображения или звук, а также мое собственное исследование / тестирование, я выяснил, что, по моему мнению, является самым простым способом добавления таких файлов.

Если вы хотите увидеть живой пример, мой репозиторий находится здесь, на GitHub.

Примечание: это для компиляции с использованием команды --onefileили -Fс pyinstaller.

Моя среда выглядит следующим образом.


Решение проблемы в 2 шага

Чтобы решить эту проблему, нам нужно специально сообщить Pyinstaller, что у нас есть дополнительные файлы, которые необходимо «связать» с приложением.

Нам также необходимо использовать «относительный» путь , чтобы приложение могло работать правильно, когда оно выполняется как скрипт Python или замороженный EXE.

При этом нам нужна функция, которая позволяет нам иметь относительные пути. Используя функцию, которую опубликовал Макс, мы можем легко решить относительный путь.

def img_resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    try:
        # PyInstaller creates a temp folder and stores path in _MEIPASS
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

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

icon_path = img_resource_path("app/img/app_icon.ico")
root.wm_iconbitmap(icon_path)

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

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

Во-первых, у вас уже должен быть файл .spec. В моем случае я смог создать то, что мне нужно, запустив pyinstallerдополнительные аргументы, вы можете найти дополнительные аргументы здесь . Из-за этого мой файл спецификаций может немного отличаться от вашего, но я публикую его для справки после того, как объясню важные моменты.

added_files - это, по сути, список, содержащий кортежи, в моем случае я хочу добавить только ОДНО изображение, но вы можете добавить несколько ico, png или jpg, используя('app/img/*.ico', 'app/img')Вы также можете создать другой кортеж, например, чтобыadded_files = [ (), (), ()]иметь несколько импортов

Первая часть кортежа определяет, какой файл или какой тип файла вы хотите добавить, а также где их найти. Думайте об этом как о CTRL + C

Вторая часть кортежа сообщает Pyinstaller, что нужно сделать путь app / img / и поместить файлы в этот каталог ОТНОСИТЕЛЬНО к тому временному каталогу, который создается при запуске .exe. Думайте об этом как о CTRL + V

Подa = Analysis([main... , я установилdatas=added_files, изначально это было,datas=[]но мы хотим, чтобы список импорта был ... ну, импортирован, поэтому мы передаем наши пользовательские импорты.

Вам не нужно этого делать, если вам не нужен конкретный значок для EXE, в нижней части файла спецификации я говорю Pyinstaller, чтобы установить значок моего приложения для exe с опцией icon='app\\img\\app_icon.ico'.

added_files = [
    ('app/img/app_icon.ico','app/img/')
]
a = Analysis(['main.py'],
             pathex=['D:\\Github Repos\\Processes-Killer\\Process Killer'],
             binaries=[],
             datas=added_files,
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='Process Killer',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          upx_exclude=[],
          runtime_tmpdir=None,
          console=True , uac_admin=True, icon='app\\img\\app_icon.ico')

Компиляция в EXE

Я очень ленив; Я не люблю печатать больше, чем должен. Я создал файл .bat, по которому можно просто щелкнуть. Вам не нужно этого делать, этот код будет отлично работать и без него в командной строке.

Поскольку файл .spec содержит все наши параметры компиляции и аргументы (также известные как параметры), нам просто нужно передать этот файл .spec в Pyinstaller.

pyinstaller.exe "Process Killer.spec"

1

Самая распространенная жалоба / вопрос, которые я видел по поводу PyInstaller, - это «мой код не может найти файл данных, который я определенно включил в пакет, где он?», И нелегко увидеть, что / где ваш код выполняет поиск, потому что извлеченный код находится во временном месте и удаляется при выходе. Добавьте этот фрагмент кода, чтобы увидеть, что включено в ваш onefile и где он находится, используя @Jonathon Reinhart'sresource_path()

for root, dirs, files in os.walk(resource_path("")):
    print(root)
    for file in files:
        print( "  ",file)

1

Давно (ну очень давно) занимаюсь этим вопросом . Я искал почти все источники, но все не складывалось в моей голове.

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

Обратите внимание, что в моем ответе используется информация об ответах других на этот вопрос.

Как создать автономный исполняемый файл проекта Python.

Предположим, у нас есть project_folder и дерево файлов выглядит следующим образом:

project_folder/
    main.py
    xxx.py # modules
    xxx.py # modules
    sound/ # directory containing the sound files
    img/ # directory containing the image files
    venv/ # if using a venv

Прежде всего, предположим, что вы определили свои пути sound/и img/папки в переменныеsound_dir и img_dirследующим образом :

img_dir = os.path.join(os.path.dirname(__file__), "img")
sound_dir = os.path.join(os.path.dirname(__file__), "sound")

Вы должны изменить их следующим образом:

img_dir = resource_path("img")
sound_dir = resource_path("sound")

Куда, resource_path() определяется в верхней части вашего скрипта как:

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, relative_path)

Активируйте виртуальный env при использовании venv,

Установите pyinstaller, если вы еще этого не сделали: pip3 install pyinstaller .

Бегать: pyi-makespec --onefile main.py создать файл спецификации для процесса компиляции и сборки.

Это изменит иерархию файлов на:

project_folder/
    main.py
    xxx.py # modules
    xxx.py # modules
    sound/ # directory containing the sound files
    img/ # directory containing the image files
    venv/ # if using a venv
    main.spec

Открытый (с редактором) main.spec :

Вверху вставьте:

added_files = [

("sound", "sound"),
("img", "img")

]

Затем измените строку datas=[], наdatas=added_files,

Подробнее о выполняемых операциях main.specсм. Здесь.

Бегать pyinstaller --onefile main.spec

И это все, вы можете запустить mainв project_folder/distиз любой точки мира, не имея ничего другого в своей папке. Вы можете распространять только этот mainфайл. Теперь это настоящая автономная версия .


@JonathonReinhart, если вы все еще здесь, я хотел сообщить вам, что я использовал вашу функцию в своем ответе.
muyustan

1

Другое решение - создать обработчик времени выполнения, который будет копировать (или перемещать) ваши данные (файлы / папки) в каталог, в котором хранится исполняемый файл. Хук - это простой файл Python, который может делать почти все, непосредственно перед запуском вашего приложения. Чтобы установить его, вы должны использовать --runtime-hook=my_hook.pyопцию pyinstaller. Итак, если ваши данные представляют собой папку с изображениями , вы должны выполнить команду:

pyinstaller.py --onefile -F --add-data=images;images --runtime-hook=cp_images_hook.py main.py

Cp_images_hook.py может быть примерно таким:

import sys
import os
import shutil

path = getattr(sys, '_MEIPASS', os.getcwd())

full_path = path+"\\images"
try:
    shutil.move(full_path, ".\\images")
except:
    print("Cannot create 'images' folder. Already exists.")

Перед каждой казнью изображения папка перемещается в текущий каталог (из папки _MEIPASS), поэтому исполняемый файл всегда будет иметь к нему доступ. Таким образом, нет необходимости изменять код вашего проекта.

Второе решение

Вы можете воспользоваться механизмом runtime-hook и изменить текущий каталог, что, по мнению некоторых разработчиков, не является хорошей практикой, но работает нормально.

Код перехвата можно найти ниже:

import sys
import os

path = getattr(sys, '_MEIPASS', os.getcwd())   
os.chdir(path)

0

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

Когда я запускаю свое приложение, я получаю сообщение об ошибке Failed to execute script foo(если foo.pyэто основной файл). Чтобы устранить эту проблему, не запускайте PyInstaller с --noconsole(или отредактируйте, main.specчтобы изменить console=False=> console=True). При этом запустите исполняемый файл из командной строки, и вы увидите ошибку.

Первое, что нужно проверить, это правильно ли упаковывает ваши лишние файлы. Вы должны добавить кортежи, например, ('x', 'x')если хотите, чтобы папкаx была включена.

После сбоя не нажимайте ОК. Если вы работаете в Windows, вы можете использовать Поиск во всем . Найдите один из ваших файлов (например sword.png). Вы должны найти временный путь, по которому были распакованы файлы (например C:\Users\ashes999\AppData\Local\Temp\_MEI157682\images\sword.png). Вы можете просмотреть этот каталог и убедиться, что в нем есть все. Если вы не можете найти его таким образом, поищите что-нибудь вроде main.exe.manifest(Windows) или python35.dll(если вы используете Python 3.5).

Если установщик включает все, следующая вероятная проблема - файловый ввод-вывод: ваш код Python ищет файлы в каталоге исполняемого файла, а не во временном каталоге.

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

if hasattr(sys, '_MEIPASS'): os.chdir(sys._MEIPASS)


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

0

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

https://stackoverflow.com/a/59415662/999943

Вы добавляете в файл спецификации шаг, который копирует файловую систему в переменную DISTPATH.

Надеюсь, это поможет.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.