使用Python和ftplib.FTP从z/os下载文本文件

6 投票
5 回答
13491 浏览
提问于 2025-04-15 13:10

我正在尝试用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 个回答

1

你的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编码页中有效的所有字节。

[一些“清理”建议]

  1. 考虑把你的Python从3.0(一个“概念验证”版本)升级到3.1。

  2. 为了更好地理解你的代码,使用“i”作为标识符时,只在作为序列索引时使用,除非你是从三十多年前的FORTRAN中养成了这个习惯:-)

  3. 到目前为止发现的两个问题(在每个字符后面加换行符,换行符错误)在你第一次测试时就应该能发现。

3

你可以把文件当作二进制文件下载(使用 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 版本的另一个类里。

6

我刚看到这个问题,因为我在尝试弄明白如何从 z/OS 递归下载数据集。我已经用一个简单的 Python 脚本下载主机上的 ebcdic 文件好多年了。这个脚本实际上就是这么做的:

def writeline(line):
    file.write(line + "\n")

file = open(filename, "w")
ftp.retrlines("retr " + filename, writeline)

撰写回答