pyinstaller로 exe 파일을 만들었는데, failed to execute script 에러가 나네요..

조회수 13605회

curve fitting 프로그램 하나를 만들었는데, 처음 만들 때는 버튼 9개(1차,2차,3차,4차 다항식, exp, log, sin, cos, tan)를 만들어서 유저가 직접 함수 형태를 고르는 것으로 했습니다. 그리고 pyinstaller로 exe 파일을 만들었습니다. 그러다가 자동으로 골라주면 좋겠다 싶어서 버튼을 하나로 줄이고, sklearn을 추가로 import 했습니다. 그런 다음 다시 pyinstaller로 exe 파일을 만들었죠. sklearn을 import 하기 전에는 exe 파일 py파일 둘 다 잘 돌아갔는데, sklearn을 import 하니 exe 파일은 에러를 뱉으면서 작동을 안 하네요... 코드는 이게 전부입니다. 그리고 pyinstaller는 개발자 버전으로 쓰고 있구요.. 디버깅을 해보려고 debug 옵션 넣고, console 켜지는걸로 했는데, 프로그램이 꺼지자 마자 디버깅 내용이 있는 cmd 창도 같이 꺼져 버려서 디버깅도 못하고 있는 상황이네요.. 이걸 어떻게 해결할 수 있을까요?

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5 import uic

from numpy import array as arr
from numpy import append as nappend
from numpy import exp,log, sin, cos, tan
from scipy.optimize import curve_fit as cuf
from sklearn.metrics import r2_score as r2s

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import matplotlib.pyplot as plt

#UI file load
form_class = uic.loadUiType("fit.ui")[0]


def func1(x, p1, p2):
    return p1*x + p2

def func2(x, p1, p2, p3):
    return p1*x**2 + p2*x + p3

def func3(x, p1,p2, p3, p4):
    return p1*x**3 + p2*x**2 + p3*x + p4

def func4(x,p1,p2, p3, p4, p5):
    return p1*x**4 + p2*x**3 + p3*x**2 + p4*x + p5

def func5(x, p1, p2, p3, p4):
    return p1*exp(p2*x + p3) + p4

def func5_2(x,p1,p2,p3,p4):
    return p1*exp(-p2*x + p3) + p4

def func6(x, p1, p2, p3, p4):
    return p1*log(p2*x + p3) + p4

def func7(x,p1,p2,p3,p4):
    return p1*sin(p2*x + p3) + p4

def func8(x,p1,p2,p3,p4):
    return p1*cos(p2*x + p3) + p4

def func9(x,p1,p2,p3,p4):
    return p1*tan(p2*x + p3) + p4

def lis(data):
    xd = []
    yd = []
    for d in data:
        x, y = d.split()
        xd.append(float(x))
        yd.append(float(y))
    return arr(xd), arr(yd)

def ev(f,xd,yd):
    popt, pcov = cuf(f,xd,yd)
    r2 = r2s(yd,f(xd,*popt))
    return r2

def cf(f,xd,yd,form):
    i = 0
    popt, pcov = cuf(f,xd,yd)

    po = popt.tolist()
    ar = ["a","b","c","d","e"]
    txt = ""

    for p in po:
        if txt == "":
            txt = ar[i] + " = " + str(round(p,2))
            i += 1
        else:
            txt = txt + ", " + ar[i] + " = " + str(round(p,2))
            i += 1

    plt.scatter(xd, yd, color = "black", marker='.')
    plt.plot(xd, f(xd, *popt), color='red', linewidth=2)
    plt.title("form: " + form + "\n" + txt)
    plt.grid(True)
    plt.show()


class MyWindow(QMainWindow, form_class):
    # initialize
    def __init__(self):
        super().__init__()
        self.setupUi(self)

        self.setWindowTitle("Fitting")
        self.txtbox.setStyleSheet("color:black")

        self.trial.clicked.connect(self.tr)
        self.p1.clicked.connect(self.btn1)
        self.load.clicked.connect(self.btn_load)

    def file(self,txt):
        if txt == "":
            QMessageBox.about(self,"Warning","you should select file first")
        else:
            try:
                dat = []
                f = open(txt,"r")

                while True:
                    line = f.readline()
                    if not line:
                        break
                    dat.append(line)

                nxd, nyd = lis(dat)
                f.close()
            except FileNotFoundError:
                QMessageBox.about(self,"Warning","file can't be found or invalid data form.")
                return 0,0
            except OSError:
                try:
                    dat = txt.split("\n")
                    nxd, nyd = lis(dat)
                except OSError:
                    QMessageBox.about(self,"Warning","please put correct data form.")
                    return 0,0
                except ValueError:
                    QMessageBox.about(self,"Warning","please put correct data form.")
                    return 0,0


        return nxd, nyd

    def tr(self):
        try:
            txt = self.txtbox.toPlainText()
            nxd, nyd = self.file(txt)
            if nxd == 0 and nyd == 0:
                pass
        except ValueError:
            plt.scatter(nxd, nyd, color = "black", marker='.')
            plt.plot(nxd, nyd, color='red', linewidth=2)
            plt.grid(True)
            plt.title("Data Figure")
            plt.show()

    def btn1(self):
        try:
            txt = self.txtbox.toPlainText()
            nxd, nyd = self.file(txt)
            if nxd == 0 and nyd == 0:
                pass
        except ValueError:
            r = [0,0,0,0,0,0,0,0,0,0]
            f = [func1,func2,func3,func4,func5,func5_2,func6,func7,func8,func9]
            form = ["y = a*x + b","y = a*x^2 + b*x + c","y = a*x^3 + b*x^2 + c*x + d","y = a*x^4 + b*x^3 + c*x^2 + d*x + e", "y = a*exp(b*x + c) + d","y = a*exp(-b*x + c) + d","y = a*log(b*x + c) + d","y = a*sin(b*x + c) + d","y = a*cos(b*x + c) + d","y = a*tan(b*x + c) + d"]
            for i in range(0,10):
                try:
                    r[i] = ev(f[i],nxd,nyd)
                except RuntimeError:
                    continue
            max = 0
            j = 0
            for i in range(0,10):
                if r[i] > max:
                    max = r[i]
                    j = i

            print(r)
            cf(f[j],nxd,nyd,form[j])

    def btn_load(self):
        fname = QFileDialog.getOpenFileName(self, "Open File", "" ,"TextFile (*.txt)")
        self.txtbox.setText(fname[0])

if __name__ == "__main__":
    app = QApplication(sys.argv)
    myWindow = MyWindow()
    myWindow.show()
    app.exec_()

아래는 fit.ui 파일의 코드 입니다.

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>708</width>
    <height>537</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <widget class="QPushButton" name="p1">
    <property name="geometry">
     <rect>
      <x>270</x>
      <y>310</y>
      <width>111</width>
      <height>51</height>
     </rect>
    </property>
    <property name="text">
     <string>Fitting</string>
    </property>
   </widget>
   <widget class="QTextEdit" name="txtbox">
    <property name="geometry">
     <rect>
      <x>70</x>
      <y>70</y>
      <width>431</width>
      <height>141</height>
     </rect>
    </property>
    <property name="html">
     <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Gulim'; font-size:9pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; color:#9f9f9f;&quot;&gt;값으로 넣을 때는&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; color:#9f9f9f;&quot;&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; color:#9f9f9f;&quot;&gt;과 같이 x,y 구분은 공백(1 2)이나 탭(1   2)으로&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; color:#9f9f9f;&quot;&gt;데이터 구분은 한 줄 밑으로 내려서 해주십시오&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
    </property>
   </widget>
   <widget class="QPushButton" name="load">
    <property name="geometry">
     <rect>
      <x>530</x>
      <y>70</y>
      <width>111</width>
      <height>51</height>
     </rect>
    </property>
    <property name="text">
     <string>파일 불러오기</string>
    </property>
   </widget>
   <widget class="QPushButton" name="trial">
    <property name="geometry">
     <rect>
      <x>530</x>
      <y>150</y>
      <width>111</width>
      <height>51</height>
     </rect>
    </property>
    <property name="text">
     <string>그려보기</string>
    </property>
   </widget>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <resources/>
 <connections/>
</ui>

  • (•́ ✖ •̀)
    알 수 없는 사용자
  • fit.ui 파일이 없어서 해볼수도 없습니다. github에 풀소스를 올려주시던지 오류메세지를 올려주세요 그리고 .spec 파일을 만들어서 빌드하는 것을 추천드립니다. 정영훈 2019.11.30 18:00
  • fit.ui 코드 올렸습니다. 그리고.. spec 파일을 만들어서도 해봤는데 안 되더라구요.. 알 수 없는 사용자 2019.12.1 22:56

1 답변

  • spec 파일까지 올렸으면 더 좋았을 것 같네요. 문제는 spec 파일에 있는 것이니까요.

    테스트 해보니 결국 라이브러리 디펜던시 문제입니다. exe 로 만들어준다는 것이 마법이 아니라 파이썬 interpreter 를 내장해주고 관련된 라이브러리도 한곳에 두는 것 뿐입니다. 단순히 python script.py 대신 .exe 파일로 런쳐 역활만 해주는 겁니다.

    파이썬이 개발할때는 편할 수 있는데 디버깅으로 넘어가면 힘듭니다. 그 이유가 python 이 성능상의 이유등으로 확장모듈을 많이 사용하기 때문입니다.

    python 파일내에서 오류가 발생하면 트러블 슈팅하기가 괜찮지만 확장모듈(.dll, .so, .pyd)에서 오류가 발생하면 c/c++ 코드를 봐야 합니다.

    왜 직접적인 관계 없는 이야기까지 적느냐면 지금 빌드 문제도 확장모듈 때문에 발생하는 문제이기 때문입니다.

    오류를 확인하는 방법은 exe 파일을 cmd 창에서 실행하면 됩니다. 그러면 스크립트 몇라인 import 시 오류가 발생했다고 나타날겁니다.

    scikit learn 은 c로 작성된 확장모듈을 많이 사용합니다. 그런 확장모듈들도 같은 디렉토리에 배포가 되어야 하는데 제대로 카피가 되지 않아 찾을 수 없다는 디펜던시 문제가 발생하는 겁니다.

    일단 빌드한 파일을 올려드립니다. 압축풀고 module1.exe 실행해보면 됩니다. 코드 수정후에는 질문자 환경에서 빌드를 합니다. 그러면 exe 파일이 생성될텐데 그 exe파일을 module1 디렉토리에 복사해서 사용하면 될겁니다.

    https://drive.google.com/open?id=1UEAiobDXXb-mIehJOFYpsd-Mw_0v3tcT

    • 혹시 그런 C 확장 모듈은 오류내용에서 확인하고 추가를 해줘야 하는 것인가요? 알 수 없는 사용자 2019.12.2 10:22
    • 맞습니다. 오류를 보고 적당히 추가하면 됩니다. cmd 에서 exe를 실행해보면 오류 메세지가 보일겁니다. 몇라인에서 오류가 발생했다는 식으로 보이게 되는데 만약 import sklearn 라인에서 오류가 발생했다면 찾아서 넣어주는 형식으로 처리할 수 있습니다. 핵심은 pyinstaller 라는 것이 그냥 하나로 묶어주는 것 이상도 아니라는 겁니다. 정영훈 2019.12.2 10:50

답변을 하려면 로그인이 필요합니다.

프로그래머스 커뮤니티는 개발자들을 위한 Q&A 서비스입니다. 로그인해야 답변을 작성하실 수 있습니다.

(ಠ_ಠ)
(ಠ‿ಠ)