使用固定宽度行编写/解析文本文件

14

我是Python的新手,想用它来编写一些我们供应商要求的复杂EDI功能。

基本上,他们需要一个80个字符宽度的固定宽度文本文件,其中某些字段块具有数据,而其他字段块则为空。 我有文档,所以我知道每个“块”的长度。 收到的响应易于解析,因为它已经具有数据,我可以使用Python的“切片”来提取所需内容,但我不能对切片进行赋值 - 我尝试过了,因为它听起来是一个好的解决方案,但由于Python字符串是不可变的,所以它不起作用 :)

就像我说的,我真的是Python的新手,但我对学习它感到兴奋:)我该怎么做? 理想情况下,我想能够说10-20范围等于“Foo”,并将其作为具有7个附加空格字符的“Foo”字符串(假设该字段的长度为10)的一部分,包含在更大的80个字符字段中,但我不确定如何实现我的想法。


你正在处理X12 EDI消息吗?布局并不是真正固定的。你是否在处理其他格式?如果是,那就不是真正的[EDI]了吧?它只是一个固定的文件布局。 - S.Lott
我真的不知道。他们在所有文档中都称其为“EDI”。我只知道我必须发送一条记录给他们(他们称之为“H0”记录),然后他们会发送一个文件给我解析。 - Wayne Molina
X12的ISA头是固定宽度的(即第一行),因为分隔符直到行末才被声明。 - charlesbridge
8个回答

22

您不需要为切片分配,只需使用%格式化构建字符串即可。

以下是三个数据项的固定格式示例:

>>> fmt="%4s%10s%10s"
>>> fmt % (1,"ONE",2)
'   1       ONE         2'
>>> 

同样的事情,使用数据提供的字段宽度:

>>> fmt2 = "%*s%*s%*s"
>>> fmt2 % (4,1, 10,"ONE", 10,2)
'   1       ONE         2'
>>> 

将数据和字段宽度分离,并使用zip()str.join()技巧:

>>> widths=(4,10,10)
>>> items=(1,"ONE",2)
>>> "".join("%*s" % i for i in zip(widths, items))
'   1       ONE         2'
>>> 

10

希望我理解你的意思:希望能够通过简单的变量方便地识别行中的每个部分,但输出它时要填充到正确的宽度?

下面的代码片段可能会给你想要的东西。

class FixWidthFieldLine(object):

    fields = (('foo', 10),
              ('bar', 30),
              ('ooga', 30),
              ('booga', 10))

    def __init__(self):
        self.foo = ''
        self.bar = ''
        self.ooga = ''
        self.booga = ''

    def __str__(self):
        return ''.join([getattr(self, field_name).ljust(width) 
                        for field_name, width in self.fields])

f = FixWidthFieldLine()
f.foo = 'hi'
f.bar = 'joe'
f.ooga = 'howya'
f.booga = 'doin?'

print f
这将产生:
hi        joe                           howya                         doing     

它通过存储类级变量fields来记录每个字段在输出中应该出现的顺序,以及该字段应该具有的列数。在 __init__ 中还有相应命名的实例变量,最初设置为空字符串。

__str__方法将这些值作为字符串输出。它使用一个列表推导式遍历类级别的fields属性,通过名称查找每个字段的实例值,并根据列左对齐其输出。然后将字段的结果列表由空字符串连接在一起。

请注意,这不会解析输入,但是您可以轻松地重写构造函数以获取字符串并按照fields中的字段和字段宽度解析列。它还不检查实例值是否超过了其分配的宽度。


7

您可以使用对齐函数来左对齐、右对齐和居中一个给定宽度的字符串。

'hi'.ljust(10) -> 'hi        '

2

我知道这个帖子有些年头了,但是我们使用一个名为django-copybook的库。它与 Django 没有任何关系(再也没有了)。我们用它来在 Python 和定长 COBOL 文件之间进行转换。你可以创建一个类来定义你的定长记录布局,并轻松地在类型化的 Python 对象和定长文件之间移动:

USAGE:
class Person(Record):
    first_name = fields.StringField(length=20)
    last_name = fields.StringField(length=30)
    siblings = fields.IntegerField(length=2)
    birth_date = fields.DateField(length=10, format="%Y-%m-%d")

>>> fixedwidth_record = 'Joe                 Smith                         031982-09-11'
>>> person = Person.from_record(fixedwidth_record)
>>> person.first_name
'Joe'
>>> person.last_name
'Smith'
>>> person.siblings
3
>>> person.birth_date
datetime.date(1982, 9, 11)

它还可以处理类似于Cobol的OCCURS功能的情况,例如当特定部分重复X次时。

1

我使用了Jarret Hardie的示例并稍作修改。这允许选择文本对齐方式(左对齐、右对齐或居中对齐)。

class FixedWidthFieldLine(object):
    def __init__(self, fields, justify = 'L'):
        """ Returns line from list containing tuples of field values and lengths. Accepts
            justification parameter.
            FixedWidthFieldLine(fields[, justify])

            fields = [(value, fieldLenght)[, ...]]
        """
        self.fields = fields

        if (justify in ('L','C','R')):
            self.justify = justify
        else:
            self.justify = 'L'

    def __str__(self):
        if(self.justify == 'L'):
            return ''.join([field[0].ljust(field[1]) for field in self.fields])
        elif(self.justify == 'R'):
            return ''.join([field[0].rjust(field[1]) for field in self.fields])
        elif(self.justify == 'C'):
            return ''.join([field[0].center(field[1]) for field in self.fields])

fieldTest = [('Alex', 10),
         ('Programmer', 20),
         ('Salem, OR', 15)]

f = FixedWidthFieldLine(fieldTest)
print f
f = FixedWidthFieldLine(fieldTest,'R')
print f

返回:
Alex      Programmer          Salem, OR      
      Alex          Programmer      Salem, OR

0

我有点难以理解你的问题,但我猜测你正在接收一个文件或类似文件的对象,读取它,并用一些业务逻辑结果替换其中的某些值。这是正确的吗?

克服字符串不可变性最简单的方法是编写一个新字符串:

# Won't work:
test_string[3:6] = "foo"

# Will work:
test_string = test_string[:3] + "foo" + test_string[6:]

话虽如此,听起来你很在意对这个字符串做些什么,但我不确定具体是什么。你是要将它写回输出文件,尝试原地编辑文件还是其他什么操作?我提出这个问题是因为创建一个新字符串(恰好与旧字符串同名)的行为应强调在转换后执行显式写操作的必要性。


0
你可以将字符串转换为列表并进行切片操作。
>>> text = list("some text")
>>> text[0:4] = list("fine")
>>> text
['f', 'i', 'n', 'e', ' ', 't', 'e', 'x', 't']
>>> text[0:4] = list("all")
>>> text
['a', 'l', 'l', ' ', 't', 'e', 'x', 't']
>>> import string
>>> string.join(text, "")
'all text'

有趣。你不需要将其转换为列表来提取。那很傻。但是构建一个列表,然后折叠成字符串...如果您预先分配足够的空间,它会给您一些类似于“可变字符串”的东西。 - S.Lott
如果您不太关心性能,实际上您不需要预先分配任何东西。如果将更大的范围分配给切片的范围,则列表类型会自动分配更多的空间。 - Skurmedel
此外,列表转换是为了清晰起见。当然,如果他从一开始就直接将数据读入列表可能会更好,但这不是我想展示的内容。 - Skurmedel

0

编写函数来“修改”字符串非常容易。

def change(string, start, end, what):
    length = end - start
    if len(what)<length: what = what + " "*(length-len(what))
    return string[0:start]+what[0:length]+string[end:]

使用方法:

test_string = 'This is test string'

print test_string[5:7]  
# is
test_string = change(test_string, 5, 7, 'IS')
# This IS test string
test_string = change(test_string, 8, 12, 'X')
# This IS X    string
test_string = change(test_string, 8, 12, 'XXXXXXXXXXXX')
# This IS XXXX string

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接