Build-Prozess (pyinstaller) verstehen und kontrollieren

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
dan1
User
Beiträge: 2
Registriert: Mittwoch 4. November 2020, 11:44

Hallo in die Runde,

ich bin neu hier im Forum, daher eine kurze Vorstellung:
ich habe bereits einige Jahre Erfahrung in der Programmierung mit C++ / Java, andere Programmiersprachen kenne ich eher in der Theorie vom Studium.
Mit Python habe ich erst vor einigen Wochen angefangen und war beeindruckt, wie schnell man vorzeigbare Resultate erzielen kann.

Jedoch hatte ich bisher noch nie das Bedürfnis, ein ausführbares Programm aus Python-Code zu erzeugen... bis Anfang der Woche.
Ich muss sagen, ich war doch sehr irritiert, wie stiefmütterlich (anscheinend?) das Thema "Build-Prozess" seitens Python behandelt wird.

Konkret ist mein "Problem", dass die vom pyinstaller erzeugten Executables (unter Windows10) unverschämt "groß" sind.

Ich habe folgendes Beispiel-Programm, welches sämtliche *.XML-Dateien aus einem Verzeichnis einliest, und ggf. unerwünschte BOM-Werte am Anfang (UTF8-BOM-Codierung) entfernt und die jeweilige Datei ohne BOM zurückschreibt.

Beispiel-Code:

Code: Alles auswählen

import os
import codecs

cur_dir = os.path.dirname(os.path.realpath(__file__))

xmls = []
for f in os.listdir(cur_dir):
    if f.endswith(".xml"):
        xmls.append(os.path.join(cur_dir,f))

print(len(xmls), "XML files found.")

# Remove BOMs
countBOM = 0
for xml in xmls:
    try:
        with open(xml, 'r+b') as x:
            x.seek(0,2) # seek to end
            if x.tell() < 4:
                print(xml, " too small.")
            else:
                x.seek(0,0) # seek to beginning
                hasBOM = False
                while x.read(3) == codecs.BOM_UTF8:
                    hasBOM = True
                if hasBOM:
                    countBOM = countBOM + 1
                    x.seek(-3, 1)
                    cleanContent = x.read()
                    x.seek(0,0)
                    x.write(cleanContent)
                    x.truncate()
                    print(xml, " BOM(s) removed.")
    except EnvironmentError as ee:
        print(xml, " env error: ", ee)

print("Success.", countBOM, "files cleaned.")
Wenn ich nun diesen Code dem pyinstaller übergebe mittels

Code: Alles auswählen

pyinstaller --onefile removeBOMs.py
dann erhalte ich eine exe-Datei einer Größe von 9521KB.

Ich habe schon gelesen, dass ich manuell Module ausschließen kann:

Code: Alles auswählen

pyinstaller --exclude-module tkinter --onefile removeBOMs.py
ergibt eine exe-Datei von 6845KB.

Code: Alles auswählen

pyinstaller --exclude-module tkinter --exclude-module xml --exclude-module datetime --exclude-module _bz2 --exclude-module _hashlib --onefile removeBOMs.py
ergibt eine exe-Datei von 6541KB.

Ich glaube, ihr versteht, worauf ich hinaus will.

Meine Fragen sind:
1. Gibt es eine Möglichkeit, automatisiert nicht benötigte Module im Build-Prozess auszuschließen, um damit die Größe der Executable zu minimieren?
2. Ist dieser Prozess irgendwo dokumentiert? (bzw. mache ich etwas grundsätzlich falsch?) Denn hier z.B. https://pyinstaller.readthedocs.io/en/s ... -mode.html finde ich den Abschnitt: Analysis: Finding the Files Your Program Needs was allerdings in meinem Fall nicht wie gewünscht zu funktionieren scheint.
3. Könnt ihr mir ggf. eine Alternative zu "pyinstaller" empfehlen?

Ziel wäre es, aus dem obigen Beispielprogramm eine Executable der Größe von maximal 1MB zu erhalten. Dies ist zwar für die Anwendung nicht "kriegsentscheidend", hätte ich aber von einer modernen und vielseitigen Programmiersprache wie Python irgendwie erwartet :)

Danke für alle Infos und einen schönen Tag zusammen!
Dan
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das “vielseitig” steht im Widerspruch zu deinem Wunsch. Denn schon die Standardbibliothek benötigt diesen Platz. Du kannst daran rumschnitzen, aber das lässt sich nicht automatisieren. Denn anders als zb bei C++ oder Java kann auch zur Laufzeit entschieden werden, ob ein bestimmtes Modul angezogen wird. Und entsprechend wird dieser Versuch auch nicht unternommen.

Ich würde mir im Zeitalter von Terabyteplatten den Stress nicht geben.

Und als kleine Einordnung: dein angeführtes C++ hat auch eine Laufzeitumgebung, vcredist. Der komprimierte Download ist schon 15Mb groß. Du merkst das nur nicht, weil das meistens schon auf dem System vorhanden ist. Diese privilegierte Position hat Python nicht. Entsprechend muss man den Interpreter eben mitliefern.
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ergänzend: Die Java Runtime ist auch deutlich >1 MiB.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
narpfel
User
Beiträge: 645
Registriert: Freitag 20. Oktober 2017, 16:10

Moin,

PyInstaller ist kein Build-Prozess, sondern ein Paketierungs-Prozess. Der „Build-Prozess“ läuft automatisch im Hintergrund, wenn ein Modul importiert wird.

PyInstaller packt im Prinzip nur dein Programm zusammen mit seinen Abhängigkeiten (inklusive der Python-Standardbibliothek) und dem Interpreter zusammen in eine Zip-Datei. Allein `pythonXY.dll` ist weit größer als 1 MB. Du wirst also nie unter 1 MB kommen können. Vergleiche das mit Java, da wirst du ein Programm auch nie unter 1 MB bekommen, wenn du die JVM dazu zählst. Und in C++ auch nicht, wenn du zum Beispiel die Standardbibliothek statisch linkst.

Das (finde ich) einfachere Vorgehen ist, erst Python auf dem Zielsystem zu installieren und dann das gewünschte Programm mit `pip` (am besten in eine Virtualenv) zu installieren. Dann ist dein Programm nur eine Zip-Datei aus dem Quellcode.
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Zum Code: os.listdir ist veraltet, genauso wie os.path. Statt dessen benutzt man pathlib.Path
Variablennamen schreibt man komplett klein. Das mit den ganzen seeks sieht sehr kryptisch aus, aber wenn Du so optimiert arbeiten willst, bitte.
`x` ist ein sehr schlechter Name für ein Fileobjekt.
Die Prüfung auf die Dateigröße ist überflüssig, da es ja automatisch beim Lesen dann kein BOM gelesen werden kann. Eine Datei die weniger als 4 Bytes groß ist, kann kein gültiges XML sein.
Da Du eh die gesamte Datei in den Speicher liest, ist das mit dem seek nicht wirklich effizienter. Da würde ich einmal die Datei zum Lesen und dann zum Schreiben öffnen.

Code: Alles auswählen

import codecs
from pathlib import Path

def read_file_content_without_bom(file):
    """ this function reads the file content skipping BOM characters.
    if the file does not have a BOM, None is returned
    """
    data = file.read(len(codecs.BOM_UTF8))
    if data != codecs.BOM_UTF8:
        return None
    while True:
        data = file.read(len(codecs.BOM_UTF8))
        if data != codecs.BOM_UTF8:
            break
    return data + file.read()
    
def main():
    xml_filenames = list(Path(__file__).parent.glob("*.xml"))
    print(f"{len(xml_filenames)} XML files found.")

    # Remove BOMs
    number_of_bom_files = 0
    for filename in xml_filenames:
        try:
            with filename.open('rb') as file:
                data = read_file_content_without_bom(file)
            if data is not None:
                filename.write_bytes(data)
                number_of_bom_files += 1
                print(f"{filename}: BOM(s) removed.")
        except EnvironmentError as ee:
            print(f"{filename}: Error {ee}")
    print(f"Success. {number_of_bom_files} files cleaned.")

if __name__ == '__main__':
    main()
dan1
User
Beiträge: 2
Registriert: Mittwoch 4. November 2020, 11:44

Danke für die vielen Antworten und auch für den Vorschlag zur Code-Optimierung :)
So viel Hilfe in so kurzer Zeit, echt top!

Tatsächlich hatte ich den "Denkfehler", dass die komplette Python-Runtime in die Executable reingepackt wird, welche bei anderen Sprachen in der Regel schon im System vorhanden ist.

Schön fände ich es trotzdem, wenn es eine Art "minimale Standard-Runtime" gäbe, welche beispielsweise große Module wie das "tkinter" außen vor lässt.
Aber ich kann auch gut mit der jetzigen Lösung leben.

Danke euch nochmal und frohes Schaffen weiter :)
Dan
Antworten