使用Python和ftplib.FTP从z/os下载文本文件
我正在尝试用Python和ftplib自动下载一些来自z/os PDS的文本文件。
因为主机上的文件是EBCDIC编码的,所以我不能直接使用FTP.retrbinary()。
使用FTP.retrlines()时,如果把open(file,w).writelines作为回调函数,它当然不会提供行结束符。
所以,作为开始,我写了这段代码,感觉“看起来还不错”,但因为我对Python还不太熟悉,有人能建议我更好的方法吗?显然,为了保持问题简单,这不是最终的、功能齐全的版本。
非常感谢。
#!python.exe
from ftplib import FTP
class xfile (file):
def writelineswitheol(self, sequence):
for s in sequence:
self.write(s+"\r\n")
sess = FTP("zos.server.to.be", "myid", "mypassword")
sess.sendcmd("site sbd=(IBM-1047,ISO8859-1)")
sess.cwd("'FOO.BAR.PDS'")
a = sess.nlst("RTB*")
for i in a:
sess.retrlines("RETR "+i, xfile(i, 'w').writelineswitheol)
sess.quit()
更新:我使用的是Python 3.0,平台是Windows XP下的MingW。
z/os PDS有固定的记录结构,而不是依赖行结束符作为记录分隔符。不过,z/os FTP服务器在文本模式下传输时,会提供记录结束符,而retrlines()会把这些去掉。
结束更新:
这是我修改后的解决方案,将作为后续开发的基础(例如,去掉内置密码):
import ftplib
import os
from sys import exc_info
sess = ftplib.FTP("undisclosed.server.com", "userid", "password")
sess.sendcmd("site sbd=(IBM-1047,ISO8859-1)")
for dir in ["ASM", "ASML", "ASMM", "C", "CPP", "DLLA", "DLLC", "DLMC", "GEN", "HDR", "MAC"]:
sess.cwd("'ZLTALM.PREP.%s'" % dir)
try:
filelist = sess.nlst()
except ftplib.error_perm as x:
if (x.args[0][:3] != '550'):
raise
else:
try:
os.mkdir(dir)
except:
continue
for hostfile in filelist:
lines = []
sess.retrlines("RETR "+hostfile, lines.append)
pcfile = open("%s/%s"% (dir,hostfile), 'w')
for line in lines:
pcfile.write(line+"\n")
pcfile.close()
print ("Done: " + dir)
sess.quit()
感谢John和Vinay的帮助。
5 个回答
你的writelineswitheol方法在写入文件时加了'\r\n'而不是'\n',这样会导致在任何平台上都会出现不必要的'\r'。只需要加上'\n',就能得到合适的换行符。
处理错误的时候,不能只做个表面功夫。你应该把打开文件的代码放在try/except里,这样可以保留对输出文件的引用;写入文件的代码也要放在try/except里;当retrlines()返回时,使用callback_obj.close()方法来明确地关闭文件(同样放在try/except里)。这样你就能得到明确的错误信息,比如“无法打开|写入|关闭文件X,因为Y”,而且你也不需要担心文件什么时候会被隐式关闭,避免文件句柄用完的问题。
在Python 3.x中,ftplib.FTP.retrlines()会给你返回字符串对象,这实际上是Unicode字符串。在写入之前,你需要对它们进行编码,除非默认编码是latin1,这在Windows上比较少见。你应该准备一些测试文件,包含(1)所有可能的256个字节(2)在预期的EBCDIC编码页中有效的所有字节。
[一些“清理”建议]
考虑把你的Python从3.0(一个“概念验证”版本)升级到3.1。
为了更好地理解你的代码,使用“i”作为标识符时,只在作为序列索引时使用,除非你是从三十多年前的FORTRAN中养成了这个习惯:-)
到目前为止发现的两个问题(在每个字符后面加换行符,换行符错误)在你第一次测试时就应该能发现。
你可以把文件当作二进制文件下载(使用 retrbinary
),然后用 codecs
模块把它从 EBCDIC 编码转换成你想要的输出编码。你需要知道 z/OS 系统上使用的具体 EBCDIC 代码页(比如 cp500)。如果文件不大,你甚至可以做一些简单的转换,比如把它转换成 UTF-8:
file = open(ebcdic_filename, "rb")
data = file.read()
converted = data.decode("cp500").encode("utf8")
file = open(utf8_filename, "wb")
file.write(converted)
file.close()
更新:如果你需要使用 retrlines
来获取行,并且这些行的编码是正确的,那么你的方法就不适用了,因为回调函数会对每一行调用一次。在这个回调中,sequence
就是当前行,而你的循环会把这一行中的每个字符单独写入输出,每个字符都会单独占一行。所以你可能想用 self.write(sequence + "\r\n")
来替代这个 for
循环。不过,单单为了添加这个实用方法而去继承 file
类,感觉也不是特别合适 - 可能应该放在你 bells-and-whistles
版本的另一个类里。
我刚看到这个问题,因为我在尝试弄明白如何从 z/OS 递归下载数据集。我已经用一个简单的 Python 脚本下载主机上的 ebcdic 文件好多年了。这个脚本实际上就是这么做的:
def writeline(line):
file.write(line + "\n")
file = open(filename, "w")
ftp.retrlines("retr " + filename, writeline)