Методы подключения дополнительных файлов в программах Python

Раздел: Разработка на Python -> Упаковка приложений

Работа с ресурсными файлами в Python

При разработке приложений часто требуется включать изображения, конфигурационные файлы, звуки или данные. После упаковки программы (например, с помощью PyInstaller или cx_Freeze) обычные относительные пути перестают работать. Существует несколько подходов для организации доступа к таким ресурсам.

Как организовать доступ к файлам внутри установленного пакета?

Начиная с Python 3.7 рекомендуется использовать модуль importlib.resources. Этот способ является стандартным и не требует установки дополнительных библиотек. Он позволяет получать доступ к файлам, входящим в состав пакета, даже после упаковки приложения.

# структура проекта:
# myapp/
#   __init__.py
#   resources/
#       data.txt
#       image.png
#   main.py

# main.py
from importlib import resources

def read_data():
    with resources.open_text('myapp.resources', 'data.txt') as f:
        return f.read()

def get_image_path():
    return resources.path('myapp.resources', 'image.png')

print(read_data())
print(get_image_path())

Python program to exe (преобразование python в exe)

Возможные проблемы: если ресурсный каталог не является частью пакета (отсутствует __init__.py), importlib.resources не сможет его найти. Решение - создать пустой файл __init__.py внутри папки с ресурсами. Также при использовании PyInstaller необходимо указать эту папку как ресурсную (через --additional-hooks-dir или --add-data), иначе файлы не будут включены в сборку.

Каким образом использовать устаревший модуль pkg_resources?

Ранее широко применялся модуль pkg_resources из библиотеки setuptools. Он также умеет находить файлы внутри пакета, но работает медленнее и не поддерживается в будущих версиях Python.

from pkg_resources import resource_string, resource_filename
data = resource_string(__name__, 'resources/data.txt')
print(data.decode('utf-8'))

Resource file python (работа с ресурсными файлами)

Типичные ошибки: имя пакета (__name__) должно соответствовать реальной структуре. Если файл не найден, возникает исключение. В упакованном приложении необходимо указывать пути относительно sys._MEIPASS при работе с PyInstaller, так как pkg_resources не адаптируется к временной папке автоматически.

Как добавить ресурсы при сборке через PyInstaller?

PyInstaller не копирует файлы, не связанные с импортируемыми модулями. Для включения дополнительных файлов используется ключ --add-data.

pyinstaller --add-data "src/resources:resources" main.py

Python exe windows (создание exe из python на windows)

В коде после сборки путь к ресурсам лежит через sys._MEIPASS. При использовании importlib.resources проблем не возникает, если ресурсы оформлены как пакет. Иначе нужно самостоятельно вычислять путь:

import sys, os
def resource_path(relative):
    base = getattr(sys, '_MEIPASS', os.path.abspath('.'))
    return os.path.join(base, relative)

Exe file python (создание exe файла в python)

Проблема: если не перенаправить пути, после сборки программа будет искать файлы в папке с exe, но их там нет. Также важно, чтобы папки с ресурсами содержали __init__.py, если они оформлены как Python-пакеты.

Когда целесообразно встраивать ресурсы прямо в код?

Для небольших файлов (иконки, конфиги) можно закодировать их в base64 и сохранить как строку в исходном коде. Это упрощает распространение - один файл без дополнительных ресурсов.

import base64
# закодированный файл (можно получить из командной строки: base64 image.png > encoded.txt)
encoded = "iVBORw0KGgoAAAANSUhEUg..."
with open('temp_image.png', 'wb') as f:
    f.write(base64.b64decode(encoded))

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

Как задать ресурсы в setup.py для дистрибутива?

При сборке пакета с помощью setuptools можно включить статические файлы через package_data или data_files. Это гарантирует, что при установке через pip файлы окажутся в нужном месте.

from setuptools import setup, find_packages
setup(
    name='myapp',
    packages=find_packages(),
    package_data={
        'myapp.resources': ['*.txt', '*.png'],
    },
    include_package_data=True,
)

Ошибка: если не указать include_package_data=True, файлы могут не копироваться. Также требуется явно указывать расширения в package_data.

Расширенные примеры работы с ресурсными файлами

Пример 1: Иерархическая структура ресурсов с importlib.resources

Предположим, у пакета есть вложенные папки с файлами разных типов.

Пример
# myapp/
#   __init__.py
#   resources/
#       __init__.py
#       icons/
#           __init__.py
#           save.png
#           load.png
#       docs/
#           help.pdf
#   main.py

from importlib.resources import files

# Получение всех файлов внутри подпапки icons
icon_files = files('myapp.resources.icons').iterdir()
for f in icon_files:
    print(f.name)

# Открытие файла из подпапки
data = files('myapp.resources.docs').joinpath('help.pdf').read_bytes()
print(len(data))
save.png
load.png
123456

Пример 2: Динамическая загрузка ресурсов из sys._MEIPASS при использовании PyInstaller

Код адаптируется к среде исполнения: при запуске из интерпретатора используются относительные пути, после сборки - временная папка PyInstaller.

Пример
import sys, os, json

def get_config():
    if getattr(sys, 'frozen', False):
        base = sys._MEIPASS
    else:
        base = os.path.dirname(__file__)
    path = os.path.join(base, 'resources', 'config.json')
    with open(path) as f:
        return json.load(f)

config = get_config()
print(config['version'])
'2.1.0'

Пример 3: Использование вместе с PyInstaller хуками для автоматического включения ресурсов

Можно создать хуки, которые PyInstaller будет вызывать для поиска скрытых импортов. Но для обычных файлов проще использовать --add-data или встроенные возможности importlib.resources, которые PyInstaller частично понимает. Тем не менее, для сложных проектов пишут хуки.

Пример
# hook-myapp.py
from PyInstaller.utils.hooks import collect_data_files

datas = collect_data_files('myapp.resources')
# возвращает список кортежей (исходный путь, целевая папка)

Пример 4: Встраивание бинарного файла в код через base64 и его использование без сохранения на диск

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

Пример
import base64, tkinter as tk
from io import BytesIO

# заранее закодированная иконка (часть данных)
encoded_icon = "R0lGODlhEAAQAMQAAORHHOVSKu..."
icon_bytes = base64.b64decode(encoded_icon)

root = tk.Tk()
photo = tk.PhotoImage(data=icon_bytes)  # Tkinter поддерживает данные base64
label = tk.Label(root, image=photo)
label.pack()
root.mainloop()
(окно с иконкой)

Пример 5: Использование zipimporter для упаковки ресурсов в архив

Можно создать ZIP-архив со всеми ресурсами и добавить его в Python path. Тогда importlib.resources сможет извлекать файлы из архива.

Пример
import sys, zipimport
# добавляем архив с ресурсами в sys.path
sys.path.insert(0, 'resources.zip')

# теперь можно импортировать пакет из архива
from importlib.resources import open_text
text = open_text('packed_resources', 'data.txt').read()
print(text)
Содержимое data.txt

Работа с ресурсными файлами - comments

En
Resource file python (python)