当从shell或通过CGI运行时,python scrip读取不同的unicode字符串



在我的Ubuntu服务器上,我有一个包含以下两个文件的目录:

testDir# ls -als
insgesamt 12
4 drwxr-xr-x 2 root root 4096 Mai 29 15:12 .
4 drwxr-xr-x 6 root root 4096 Mai 28 18:38 ..
0 -rw-r--r-- 1 root root    0 Mai 28 19:17 Ö.txt
4 -rw-r--r-- 1 root root    9 Mai 28 19:16 Ö.txt

文件名看起来是一样的,但事实并非如此。大小为0的文件在点前有1个字符(Unicode代码点214=Ö),另一个文件(大小为9)有两个字符(代码点79=O,后面跟着776=¨,这是一个组合字符,可以修改前面的字符)。为了显示unicode代码点,我写了一个小脚本:

#!/usr/bin/env python3
import os
def printFileList(fileList):
for file in fileList:
string = ""
for char in file:
string += str(ord(char)) + " "
string += "<br>"
print(string)
print("Content-Type: text/htmln")
printFileList(os.listdir("testDir"))
printFileList(["Ö.txt", "Ö.txt"])

正如你所看到的,我首先从操作系统中读取文件名,并显示文件名字符的代码点。然后我做同样的事情,但使用的字符串是在程序代码中硬编码的。

当我从shell运行这个程序时,我得到的结果是:

testDir# ./test.py
Content-Type: text/html
79 776 46 116 120 116 <br>
214 46 116 120 116 <br>
79 776 46 116 120 116 <br>
214 46 116 120 116 <br>

但这个脚本(更准确地说:这个脚本的更高级版本)是作为CGI脚本从Web服务器运行的。我的Web服务器是Apache2,当我从浏览器调用这个脚本时,我得到的结果是:

79 56524 56456 46 116 120 116 
56515 56470 46 116 120 116 
79 776 46 116 120 116 
214 46 116 120 116

字符串Content-Type: text/html是http协议的一部分,不会显示,而<br>显示为换行符,因此这些部分在浏览器中不可见是有充分理由的。但看看数字!

应该是776的是第一行中的56524 56456,而在第二行中214变成了56515 56470。但这种情况只发生在从操作系统读取的文件名上。硬编码字符串是正确的。

我的问题:

1) 是什么导致了这种奇怪的行为
2)为了显示正确的代码点(776214),需要更改什么?


附录

我在我的程序中添加了以下几行:

import sys
print(sys.getfilesystemencoding())

这条线的输出是:

  • 从shell运行时:

    utf-8 
    

    这是正确的。

  • 当作为CGI脚本从apache运行时:

    ascii  
    

    这是错误的。

所以,我的新问题是:

我如何告诉我的脚本,它应该始终使用utf-8作为文件系统编码?

我在回答我自己的问题。

我仍然没有回答我的第一个问题("是什么导致了这种奇怪的行为?">),所以这仍然是开放的,我真的很好奇。

但我找到了一种变通方法,可以在不真正解决原始问题的情况下获得正确的结果。

以下是我的测试程序的一个版本,当从shell和Apache运行CGI脚本时,它产生了相同的正确输出:

#!/usr/bin/env python3
import os
def printFileList(fileList):
for file in fileList:
file = file.decode("utf-8")
string = ""
for char in file:
string += str(ord(char)) + " "
string += "<br>"
print(string)
print("Content-Type: text/htmln")
printFileList(os.listdir("testDir".encode("utf-8")))
printFileList(["Ö.txt".encode("utf-8"), "Ö.txt".encode("utf-8")])

这就是它工作的原因:

如果os.listdir的输入是unicode字符串或文件描述符,则它会生成一个unicode字符串列表作为输出。但是,如果输入一个字节序列,输出也将是一个字节顺序列表。这里有很好的记录:https://docs.python.org/3/library/os.html#os.listdir

但这两种模式之间还有另一个没有记录的区别:

  • 如果输入是一个字节序列,python不关心文件系统的编码。它总是以字节序列的形式读取文件名,并将这些序列附加到将作为输出的列表中
  • 但是,如果输入是其他内容(unicode字符串或文件描述符),那么它也会在第一步读取字节,但随后会使用调用sys.getfilesystemencoding()时显示的编码来解码此字节序列。如果字节序列包含不符合此编码的内容,则此"垃圾"将由代理字符替换
    如果sys.getfilesystemencoding()产生正确的输出,则此操作效果良好(更准确地说:如果python确实正确地猜测了文件系统编码,这会很好。sys.getfilesystemencoding()不会做出这个猜测,它只显示这个猜测的结果。)但出于某种原因,我仍然很好奇,如果脚本是由Apache作为CGI脚本运行的,这个猜测是错误的。在这里描述的设置中,真正的文件系统编码是utf-8,但python认为如果它是从Apache启动的,那么它就是ascii。因此,它产生了不正确的输出

解决方案是在不执行任何编码和转换的模式下使用os.listdir。这意味着:字节输入,字节输出。

要做到这一点,你必须更换

os.listdir("testDir")

通过

os.listdir("testDir".encode("utf-8"))

现在os.listdir将以字节模式工作,其输出也将是字节序列列表。要将它们用作unicode字符串,您只需要用以下行解码字节序列:

file = file.decode("utf-8")

(我的小程序("Ö.txt".encode("utf-8"))最后一行的编码是必要的,因为我的函数printFileList现在不再能够处理unicode字符串的列表,而只能处理字节序列的列表。)


但要小心:这不是问题的解决方案。这只是一个变通办法。如果您按照这里描述的方式实现它,那么只有当实际的文件系统编码真的是utf-8时,它才会工作。

我认为python中试图猜测文件系统编码的例程有一个错误。当python从Apache启动时,它不能正常工作,并做出了错误的猜测。真正的解决方案是修复这个错误。

另一种可能性是,Apache2的某些错误设置使Python被认为可以在基于ascii的文件系统上工作。也许你只需要找到这个设置并更正它,但我不知道a)是否真的有这样的Apache设置,b)如果是,哪个参数需要设置为哪个值。

最新更新