Python应用程序编译为Py2Exe时抛出UnknownTimezoneError异常

15 投票
2 回答
8855 浏览
提问于 2025-04-17 12:25

我在分发一个使用 pytz 的应用程序时遇到了问题。我正在用 Py2Exe 将我的 Python 源代码打包成一个可执行文件。

为了简单说明我遇到的问题,我有一个文件叫做:

import pytz

tz_au = pytz.timezone("Australia/Sydney")
print tz_au

还有一个叫做 setup.py 的文件:

from distutils.core import setup
import py2exe

setup(console=['pytz_test.py'], options={"py2exe" : { 'packages': ['pytz'], } })

然后我运行 setup.py:

python setup.py py2exe

这会生成可执行文件。运行生成的 pytz_test.exe 时,我得到了:

Traceback (most recent call last):
  File "pytz_test.py", line 3, in <module>
    tz_au = pytz.timezone("Australia/Sydney")
  File "pytz\__init__.pyc", line 185, in timezone
pytz.exceptions.UnknownTimeZoneError: 'Australia/Sydney'

我猜这可能是因为时区信息没有和可执行文件一起打包,但我不太确定该怎么做才能解决这个问题。

编辑: 一个简单的解决办法是把 pytz 模块在 Python 的 site-packages 目录下的 zoneinfo 文件夹添加到 library.zip 中。

为了自动完成这个操作,我参考了一个项目 Google Transit Data Feed 的解决方案,链接在这里: http://code.google.com/p/googletransitdatafeed/source/browse/trunk/python/setup.py

我修改后的 setup.py 现在看起来是这样的:

from distutils.core import setup
import glob
import py2exe

options = {
    "py2exe" : { 
        "compressed": 1, 
        "optimize": 2,
        'packages': ['pytz'], 
     } 
}

setup(console=['pytz_test.py'], options=options)

import pytz
import os 
import zipfile
zipfile_path = os.path.join("dist/" 'library.zip')
z = zipfile.ZipFile(zipfile_path, 'a')
zoneinfo_dir = os.path.join(os.path.dirname(pytz.__file__), 'zoneinfo')
disk_basedir = os.path.dirname(os.path.dirname(pytz.__file__))
for absdir, directories, filenames in os.walk(zoneinfo_dir):
    assert absdir.startswith(disk_basedir), (absdir, disk_basedir)
    zip_dir = absdir[len(disk_basedir):]
    for f in filenames:
      z.write(os.path.join(absdir, f), os.path.join(zip_dir, f))

z.close()

2 个回答

3

手动压缩zoneinfo文件(正如Jason S所描述的那样)确实帮我在一台电脑上成功构建了这个包。不过,当我在另一台电脑上构建时,错误又回来了!找出原因花了我一段时间,所以我觉得有必要分享一下。

提出的解决方案在新的pytz版本上不管用(至少在2014.7版本上是这样)!深入研究后发现,pytz把zoneinfo文件的格式从pyc改成了一种二进制格式。对我来说,这个变化似乎“破坏”了将pytz打包成zip的选项,因为Python内置的zipimport机制无法加载二进制文件。实际上,这个问题应该由pytz来解决,但目前我找到了一种替代方案:

  • 直接把整个pytz目录复制到你的dist目录里
  • 在你的程序中,把主可执行文件的路径添加到Python的搜索路径中

实际上,这意味着在你的setup.py中,把pytz的压缩部分替换为:

import pytz, os, shutil
srcDir = os.path.dirname( pytz.__file__ )
dstDir = os.path.join( 'dist', 'pytz' )
shutil.copytree( srcDir, dstDir, ignore = shutil.ignore_patterns('*.py') )

并把pytz从“packages”选项中移到“excludes”中:

options = {
    "py2exe" : { 
        "compressed": 1, 
        "optimize": 2,
        "packages": [],
        "excludes": ['pytz']
     } 
}

在你程序的主入口处(确保在导入pytz之前执行),你需要添加类似以下的内容:

import os, sys
basePath = os.path.dirname( os.path.abspath( sys.argv[0] ) )
sys.path.insert( 0, basePath )
3

一个简单的解决办法是把 pytz 模块中的 zoneinfo 文件夹添加到 library.zip 里。

为了自动完成这个操作,我参考了项目 Google Transit Data Feed 的做法,具体可以查看这个链接: http://code.google.com/p/googletransitdatafeed/source/browse/trunk/python/setup.py

我修改后的 setup.py 文件现在是这样的:

from distutils.core import setup
import glob
import py2exe

options = {
    "py2exe" : { 
        "compressed": 1, 
        "optimize": 2,
        'packages': ['pytz'], 
     } 
}

setup(console=['pytz_test.py'], options=options)

import pytz
import os 
import zipfile
zipfile_path = os.path.join("dist/" 'library.zip')
z = zipfile.ZipFile(zipfile_path, 'a')
zoneinfo_dir = os.path.join(os.path.dirname(pytz.__file__), 'zoneinfo')
disk_basedir = os.path.dirname(os.path.dirname(pytz.__file__))
for absdir, directories, filenames in os.walk(zoneinfo_dir):
    assert absdir.startswith(disk_basedir), (absdir, disk_basedir)
    zip_dir = absdir[len(disk_basedir):]
    for f in filenames:
      z.write(os.path.join(absdir, f), os.path.join(zip_dir, f))

z.close()

(由提问者回答)

撰写回答