介绍

Python ftputil 模块在上传时报错:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Traceback (most recent call last):
  File "C:\Build\deploy.py", line 165, in <module>
    main()
  File "C:\Build\lib\ftp.py", line 22, in upload
    upload_dir(ftp_host, directory, target, old_cache, new_cache, ignore_list)
  File "C:\Build\lib\ftp.py", line 99, in upload_dir
    ftp_host.upload(local_f, remote_f)
  File "C:\Build\vendor\ftputil\host.py", line 493, in upload
    conditional=False, callback=callback)
  File "C:\Build\vendor\ftputil\file_transfer.py", line 186, in copy_file
    target_fobj = target_file.fobj()
  File "C:\Build\vendor\ftputil\file_transfer.py", line 96, in fobj
    return self._host.open(self.name, self.mode)
  File "C:\Build\vendor\ftputil\host.py", line 245, in open
    rest=rest)
  File "C:\Build\vendor\ftputil\file.py", line 99, in _open
    self._conn = self._session.transfercmd(command, rest)
  File "C:\Build\vendor\ftputil\file.py", line 99, in _open
    self._conn = self._session.transfercmd(command, rest)
  File "C:\Program Files\Python37\lib\ftplib.py", line 399, in transfercmd
    return self.ntransfercmd(cmd, rest)[0]
  File "C:\Program Files\Python37\lib\ftplib.py", line 365, in ntransfercmd
    resp = self.sendcmd(cmd)
  File "C:\Program Files\Python37\lib\ftplib.py", line 272, in sendcmd
    self.putcmd(cmd)
  File "C:\Program Files\Python37\lib\ftplib.py", line 199, in putcmd
    self.putline(line)
  File "C:\Program Files\Python37\lib\ftplib.py", line 194, in putline
    self.sock.sendall(line.encode(self.encoding))
UnicodeEncodeError: 'latin-1' codec can't encode characters in position 14-15: ordinal not in range(256)

环境

  • Python 3.7
  • Windows 7

思路

搜索

如果只是单纯地搜索最后的 UnicodeEncodeError 大部分只会搜到文件编码出现问题,需要在 ftplib 中指定正确文件编码。

但实际上项目中使用的是 ftputil,ftputil 是一个对 Python 标准库 ftplib 的封装,ftputil 并没有暴露所有接口,因此无法直接修改 ftplib 属性。

在尝试修改 ftputil 的文件编码后发现依然无效,仔细搜索后发现有问题提示这与文件路径编码有关,仔细查看报错发现问题出在文件名中存在中文。

重现

使用 Python 做一个小实验,确定中文会不会引起问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ python3
>>> s = 'english中文'
>>> print(s)
english中文
>>> s.encode('latin-1')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'latin-1' codec can't encode characters in position 7-8: ordinal not in range(256)
>>> s.encode('utf-8')
b'english\xe4\xb8\xad\xe6\x96\x87'

思考

ftputil 作者在这篇贴子里介绍了一下问题来源,ftputil 底层使用 ftplib,然而最底层 socket 传输的只是字节,ftplib 也不知道编码是什么,只能认为是 latin-1

贴子中的例子正好是从服务器接收到的文件名乱码问题,可以将字符串先以 latin1编码,再以 utf8 解码就可以将内容还原。

其实这与客户端上传文件类似,Python 使用 Unicode 表示字符串,在传递时需要以具体的编码传递。

解决方案

正确的解决方案是在调用 upload 方法前将文件路径字符串参数以 utf-8 编码:

1
ftp_host.upload(local_f.encode('utf-8'), remote_f.encode('utf-8'))

使用其他 FTP 软件连接 FTP 服务器,发现上传文件路径中的中文可以正常显示。