Зв’язування файлів даних з PyInstaller (--onefile)


107

Я намагаюся створити однофайловий 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
Враховуйте, що папка temp зникає, коли виконання завершується, тому, щоб перевірити, що знаходиться всередині неї, поставте listdir з sys._MEIPASS у головну
hithwen

Чи існує також спосіб використання синтаксису Tree (), який, здається, я бачив навколо цього місця?
fatuhoku

Відповіді:


152

Новіші версії 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

8
Я б рекомендував ловити AttributeErrorзамість більш загального Exception. Я зіткнувся з проблемами, в яких мені не вистачало імпорту sys, і шлях, який мовчки дефолтував os.path.abspath.
Аарон

Ви можете допомогти вирішити мою проблему .
Філліп

1
Використання поточного робочого каталогу у випадку, коли _MEIPASS не встановлено, є неправильним. Дивіться мою відповідь .
Джонатан Райнхарт

51

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

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 'відповідь для поточної версії 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 раніше, це не вийшло.
Ахмед АбдельХалек

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

Привіт, ваше рішення працює добре, коли resource_path()він знаходиться в основному сценарії. Чи є спосіб змусити його працювати, коли він записується замість модуля? На сьогоднішній день він намагатиметься отримати ресурси в папці, де знаходиться модуль, а не основний.
Гімуте

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

@barny Дякую за підказку! Я спробую щось спробувати.
Гімуте

14

Можливо, я пропустив крок або зробив щось не так, але наведені вище методи не з'єднували файли даних з 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 (у моєму випадку файл = Converter-GUI).

Висновок: У папці dist ще є більше ніж один файл.

Примітка: я використовую Python 3.5.

EDIT: Нарешті, це працює за методом Джонатана Рейнхарта.

  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. крок: Збережіть і закрийте файл шляху. Перейдіть у свою папку, яка містить файл spec та 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

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

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 не встановлено, є неправильним. Дивіться мою відповідь .
Джонатан Райнхарт

1

Найпоширеніша скарга / питання, яке я бачив у Wrt PyInstaller, - це "мій код не може знайти файл даних, який я точно включив у комплект, де це?", І непросто зрозуміти, що / де ваш код шукає, оскільки витягнутий код знаходиться у тимчасовому місці та видаляється, коли він закривається. Додайте цей біт коду, щоб побачити, що міститься у вашому файлі та де він знаходиться, використовуючи @Jonathon Reinhart'sresource_path()

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

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 шукає файли у каталозі виконуваного файлу, а не в каталозі temp.

Щоб виправити це, будь-яка відповідь на це питання працює. Особисто я знайшов суміш їх усіх, щоб вони працювали: умовно змінити каталог, перш за все, у головному файлі вхідної точки, а все інше працює так, як є:

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


1
Вибачте. Але зміна каталогів ніколи не є правильною відповіддю. Якщо користувач надає шлях до програми, він очікує, що він буде відносно поточного каталогу. Якщо це змінити, це буде неправильно.
Джонатан Райнхарт

0

Якщо ви все ще намагаєтесь розмістити файли відносно вашого виконуваного файлу замість тимчасового каталогу, вам потрібно скопіювати його самостійно. Ось як я закінчила це робити.

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

Ви додаєте крок у файл специфікації, який копіює файлову систему на змінну DISTPATH.

Сподіваюся, що це допомагає.


0

Я займався цим питанням давно (ну, дуже довго). Я шукав майже кожне джерело, але в моїй голові все не вийшло.

Нарешті, я думаю, що я з’ясував точні кроки, які слід виконати, хотів поділитися.

Зауважте, що моя відповідь використовує інформацію щодо відповідей інших на це питання.

Як створити окремий виконуваний проект python.

Припустимо, у нас є папка Project_ і дерево файлів таке:

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

0

Користуючись чудовою відповіддю Макса і цієї публікації про додавання додаткових файлів даних, таких як зображення чи звук, і власне дослідження / тестування, я зрозумів, що я вважаю, що це найпростіший спосіб додати такі файли.

Якщо ви хочете побачити живий приклад, моє сховище знаходиться тут на GitHub.

Примітка: це для компіляції за допомогою команди --onefileабо -Fз pyinstaller.

Моє оточення таке.


Вирішення проблеми в 2 етапи

Щоб вирішити цю проблему, нам потрібно спеціально сказати Pyinstaller, що у нас є додаткові файли, які потрібно "поєднати" з додатком.

Нам також потрібно використовувати "відносний" шлях , тому програма може працювати належним чином, коли вона працює як сценарій Python або Frozen 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додаткові аргументи, ви можете знайти тут додаткові аргументи . Через це мій специфікаційний файл може виглядати трохи інакше, ніж ваш, але я публікую його для довідки після пояснення важливих бітів.

add_files - це, по суті, список, що містить Tuple, в моєму випадку я хочу лише додати єдине зображення, але ви можете додати кілька 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"

0

Ще одне рішення - зробити гачок виконання, який буде копіювати (або переміщувати) ваші дані (файли / папки) до каталогу, в якому зберігається виконуваний файл. Гак - це простий файл 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), тому виконуваний файл завжди матиме доступ до неї. Таким чином не потрібно змінювати код вашого проекту.

Друге рішення

Ви можете скористатися механізмом підключення режиму виконання та змінити поточний каталог, який, на думку деяких розробників, не є хорошою практикою, але він працює чудово.

Код гака можна знайти нижче:

import sys
import os

path = getattr(sys, '_MEIPASS', os.getcwd())   
os.chdir(path)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.