使用Python 2.X的locale模块格式化数字和货币

4 投票
1 回答
4248 浏览
提问于 2025-04-16 06:27

这个任务是要把数字、货币金额和日期格式化成符合当地习惯的 unicode 字符串。

一开始简单尝试处理数字时让我感到有点希望:

Python 2.7 (r27:82525, Jul  4 2010, 09:01:59) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import locale
>>> locale.setlocale(locale.LC_ALL, '')
'English_Australia.1252'
>>> locale.format("%d", 12345678, grouping=True)
'12,345,678'
>>> locale.format(u"%d", 12345678, grouping=True)
u'12,345,678'
>>>

接下来试试法语:

>>> locale.setlocale(locale.LC_ALL, 'French_France')
'French_France.1252'
>>> locale.format("%d", 12345678, grouping=True)
'12\xa0345\xa0678'
>>> locale.format(u"%d", 12345678, grouping=True)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\python27\lib\locale.py", line 190, in format
    return _format(percent, value, grouping, monetary, *additional)
  File "C:\python27\lib\locale.py", line 211, in _format
    formatted, seps = _group(formatted, monetary=monetary)
  File "C:\python27\lib\locale.py", line 160, in _group
    left_spaces + thousands_sep.join(groups) + right_spaces,
UnicodeDecodeError: 'ascii' codec can't decode byte 0xa0 in position 0: ordinal not in range(128)

这里发生了什么呢?

>>> locale.localeconv() # output edited for brevity
{'thousands_sep': '\xa0', 'mon_thousands_sep': '\xa0', 'currency_symbol': '\x80'}

哇!看起来有点老旧。于是我想到一个解决办法:

>>> locale.format("%d", 12345678, grouping=True).decode(locale.getpreferredencoding())
u'12\xa0345\xa0678'
>>>

更新 1 locale.getpreferredencoding() 这个方法并不靠谱;应该用 locale.getlocale()[1] 来替代:

Python 2.7 (r27:82525, Jul  4 2010, 09:01:59) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import locale
>>> locale.getpreferredencoding(), locale.getlocale()
('cp1252', (None, None))
>>> locale.setlocale(locale.LC_ALL, '')
'English_Australia.1252'
>>> locale.getpreferredencoding(), locale.getlocale()
('cp1252', ('English_Australia', '1252'))
>>> locale.setlocale(locale.LC_ALL, 'russian_russia')
'Russian_Russia.1251'
>>> locale.getpreferredencoding(), locale.getlocale()
('cp1252', ('Russian_Russia', '1251')) #### Whoops! ####
>>>

更新 2 在使用 strftime() 相关的方法和 str.format() 时也会遇到类似的问题。

>>> locale.setlocale(locale.LC_ALL, 'french_france')
'French_France.1252'
>>> format(12345678, 'n')
'12\xa0345\xa0678'
>>> format(12345678, u'n') # type triggers cast to unicode somehow
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xa0 in position 2: ordinal not in range(128)
>>> import datetime;datetime.date(1999,12,31).strftime(u'%B') # type is ignored
'd\xe9cembre'
>>>

在所有情况下,解决办法是调用这些方法时只使用 str 对象,得到一个 str 结果,然后用 locale.getlocale()[1] 得到的编码来解码。

还有其他问题:

(1) 在测试或探索时,Windows 的地区名称不仅和 POSIX 的(比如 "fr_FR")不同,而且还很冗长,文档也不全。这让人很烦恼。例如,印度的数字分组并不是“每三位”... 我找不到合适的地区来探索这个;像 "Hindi" 和 "Hindi_India" 这样的尝试都不成功。

(2) 有些 localeconv() 提供的数据根本就是错的。例如,对于韩语,货币符号被标记为 '\\',也就是一个反斜杠。我知道一些 7 位的老旧字符集并不兼容 ASCII,有时 chr(92) 被用作当地货币符号,所以我期待 '\\'.decode('949') 能产生一个韩元符号,而不是仅仅 u'\\'

我知道有像 babel 这样的模块,但我并不想引入这样一个大的外部依赖。我能否同时获得正确性和便利性?在 locale 模块中有没有我遗漏的东西?

1 个回答

3

你可能没注意到的一个地方是,locale模块实际上是根据你操作系统的供应商(其实是C库的供应商)来定义地区设置的。所以在Windows上,你需要使用Windows的地区名称,想知道有哪些支持的名称,可以查阅你操作系统供应商的文档。简单在网上搜索“windows locale name”就能很快找到这个列表.

locale.format不支持Unicode其实是Python 2.x的一个限制;你可以试试Python 3.1。

补充说明:关于韩元符号的事情,我觉得是这样的:微软把韩元符号分配到了与反斜杠在MS-DOS中相同的代码位置(在日文版本中,日元符号也是如此)。因此,文件分隔符字符就是韩元符号,并且显示成那样。随着他们转向Windows,后来又转向Unicode,他们必须继续支持这个,但也要保持文件分隔符是反斜杠的特性(特别是在Unicode API中)。他们解决了这个冲突,具体来说:

  1. 字符\x5c确实是反斜杠,而不是韩元符号。
  2. 在终端应用程序中,反斜杠被显示为韩元符号;这只是字体的问题。
  3. 货币符号被报告为\x5c:所以货币符号实际上就是反斜杠。

撰写回答