小能豆

解析固定宽度的 txt 文件时处理非 ASCII 字符

py

我有一系列巨大的表格数据文件(几 GB)。它们是 txt 格式,每列都有固定的宽度。这个宽度由标题下方的多个破折号表示。到目前为止一切顺利,我有一个脚本可以逐行读取这些文件并将其输出为 XML。

一个挑战是,大多数内容(但不是全部)都采用 UTF-8 编码。在处理过程中尝试解码内容会在某个地方引发错误。因此,我的脚本只读取和处理字节字符串。这会导致输出中的可读性问题,但这是可以容忍的,不是我关心的问题。

我的问题:宽度是根据解码后的内容计算的。UTF-8 中用几个字节表示的非 ASCII 字符不予考虑。

示例:字符串 ´Zürich, Albisgütli´ 的长度为 18,位于固定宽度为 19 的列中。但是,在其 UTF8 表示中,字符串为 ´Z\xc3\xbcrich, Albisg\xc3\xbctli´,长度为 20 个字符,因此将影响其余数据行的解析。

迄今为止的解决方案尝试:

  • 首先尝试解码数据以确保长度正确,但如上所述,一些数据条目实际上不是 UTF8,我希望避免整个编码过程。
  • 识别所有可能出现的非 ASCII 字符,以便我可以调整解析。这是一个问题,因为数据量巨大,而且我不确定能否列出可能出现的非 ASCII 字符的详尽列表。此外,我还不知道在这些情况下如何有效地纠正解析。

一个问题是我正在使用复制的代码进行解析,所以我不知道如何改变它的行为来以不同的方式计算非Ascii字符。

感谢任何可以指出的可能方法!

现在的代码:

def convert(infile, outfile):
    secondline = infile.readline() # the dashes are in this line
    maxlen = len(secondline)
    fieldwidths = get_widths(secondline) # counts the dashes to get the widths

    # code taken from: https://stackoverflow.com/a/4915359/9021715
    fmtstring = ' '.join('{}{}'.format(abs(fw), 'x' if fw < 0 else 's')
                        for fw in fieldwidths)
    fieldstruct = struct.Struct(fmtstring)
    parse = fieldstruct.unpack_from

    c = 0

    outfile.write(b"<?xml version='1.0' encoding='UTF-8'?>\n")

    namespace = f'xmlns="http://www.bar.admin.ch/xmlns/siard/2/table.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.bar.admin.ch/xmlns/siard/2/table.xsd {table_w_num}.xsd" version="2.1"'.encode()

    outfile.write(b'<table ' + namespace + b'>\n')
    for line in infile:
        diff = maxlen - len(line)
        padded_line = bytearray()
        padded_line += line
        for _ in range(diff):
            padded_line += b' '

        data = [elem.strip() for elem in parse(padded_line)]
        data = parse(padded_line)

        if b"Albis" in line:
            print(line)
            print(data)
        row = b''
        for elem, n in zip(data, range(1, len(data)+1)):
            # Timestamp-Fix
            elem = re.sub(b"(\d{4}\-\d{2}\-\d{2}) (\d{2}:\d{2}:\d{2}(\.\d+)?)\S*?", b"\g<1>T\g<2>Z", elem)
            if elem == b'' or elem == b'NULL':
                pass
            else:
                row = b'%s<c%s>%s</c%s>' % (row, str(n).encode(), xml_escape(elem), str(n).encode())
        row = b"<row>%s</row>" % (row)
        outfile.write(b''.join([row, b'\n']))

        c += 1
        if c % infostep == 0:
            timestamp = int(time.time() - start_time)
            print(f"Quarter done, time needed: {str(timestamp)} seconds")

    outfile.write(b'</table>')

编辑:

现在试图摆脱我手写的、可能失败的代码,但第二段的问题出现了。几千行之后我突然发现了这一点:

b'Z\xfcrich'

这在 ANSI/Windows-1252 中解码得很好。ftfy首先对整个文件运行库不知何故没有发现这一点。我犹豫着是否要编写一堆try except循环的混乱代码来尝试解码这些行。我甚至不知道整行是否突然变成 ANSI 格式,还是只是单个字段。


阅读 15

收藏
2024-12-29

共1个答案

小能豆

相同的答案显示了如何在第三个代码片段中处理 UTF8:

from itertools import accumulate, zip_longest
def make_parser(fieldwidths):
    cuts = tuple(cut for cut in accumulate(abs(fw) for fw in fieldwidths))
    pads = tuple(fw < 0 for fw in fieldwidths) # bool flags for padding fields
    flds = tuple(zip_longest(pads, (0,)+cuts, cuts))[:-1]  # ignore final one
    slcs = ', '.join('line[{}:{}]'.format(i, j) for pad, i, j in flds if not pad)
    parse = eval('lambda line: ({})\n'.format(slcs))  # Create and compile source code.
    # Optional informational function attributes.
    parse.size = sum(abs(fw) for fw in fieldwidths)
    parse.fmtstring = ' '.join('{}{}'.format(abs(fw), 'x' if fw < 0 else 's')
                                                for fw in fieldwidths)
    return parse

对包含问题文本的字符串使用它会产生预期的结果。请注意,原始字符串有 18 个字符,而不是 19 个字符:

>>> parser = make_parser([18,3,4])
>>> line="Zürich, Albisgütli3456789"
>>> parser(line)
('Zürich, Albisgütli', '345', '6789')

为了证明这确实适用于 UTF8 文本文件:

with open('testutf8.txt',encoding="utf-8",mode='w') as f:
    for i in range(3):
        f.write(line)
        f.write('\n')

with open('testutf8.txt',encoding="utf-8",mode='r') as f1:
    for line in f1.readlines():
        print(parser(line))
-------
('Zürich, Albisgütli', '345', '6789')
('Zürich, Albisgütli', '345', '6789')
('Zürich, Albisgütli', '345', '6789')

问题的代码应该分成几个单独的函数,一个用于读取数据,另一个用于生成 XML 输出。不过,这两个操作已经可以通过各种模块实现。有多个模块可以读取固定宽度的文件,有多个 XML 解析器和序列化库,还有一些模块(如 Pandas)可以读取多种数据格式、处理数据并将其导出为 XML。

使用 Pandas

Pandas 是分析和数据科学领域最受欢迎的模块之一,甚至可以说是最受欢迎的模块之一。它也是数据处理的绝佳工具。它是专为分析而创建的 Python 发行版的一部分,例如 Anaconda。

例如,使用 Pandas,此代码只需 2 个函数调用即可替换:

import pandas as pd
namespaces={ 
    "xmlns" : "http://www.bar.admin.ch/xmlns/siard/2/table.xsd" ,
    "xsi" : "http://www.w3.org/2001/XMLSchema-instance" ,
    ...
}    

df=pd.read_fwf('data.csv')
df.to_xml('data.xml', root_name='table', namespaces=namespaces)

与问题代码中的显式字符串操作相比,这将更快,并且占用更少的内存。字符串操作每次都会创建新的临时字符串,这会消耗 CPU 和 RAM。

默认情况下,read_fwf将尝试根据前 100 行推断列宽。您可以使用参数增加行数infer_nrows,或使用参数指定 (from,to) 元组的列表colspecs,例如colspecs=[(1,3),(3,5),(10,14)]

to_xml提供了几个参数来控制 XML 输出,例如命名空间、根和行使用的元素名称、将哪些列输出为属性以及将哪些列输出为子元素等。它甚至可以写入压缩文件

可以通过参数指定属性名称attr_cols,例如:

df.to_xml(attr_cols=[
          'index', 'shape', 'degrees', 'sides'
          ]) 

您还可以重命名 Dataframe 列,更改其类型,例如将字符串字段解析为日期或数字:

df['Timestamp'] = pd.to_datetime(df['Col3'],infer_datetime_format=True)
df=df.rename(columns={'Col1':'Bananas',...})

使用标准库 xml 模块

即使你不能用 Pandas 在几行代码中解决整个问题,你也可以使用 Python 的xml处理模块之一。它们是 Python 标准库的一部分,这意味着它们在所有发行版中都可用

ElementTree 模块的构建 XML 文档示例展示了如何以编程方式创建 XML 文档:

>>> a = ET.Element('a')
>>> b = ET.SubElement(a, 'b')
>>> c = ET.SubElement(a, 'c')
>>> d = ET.SubElement(c, 'd')
>>> ET.dump(a)
<a><b /><c><d /></c></a>

公司政策

这不是一个合乎逻辑的论点。对于安全经验有限、Python 经验有限、或者 C#、Go、JavaScript 经验有限的非技术经理来说,这听起来可能如此。安全性是甚至“标准”库/模块/程序集也通过包管理器分发和更新的主要原因之一。没有人愿意(或负担得起)再等待 2 年的安全和错误修复。

Python 标准库包含最初作为第三方包的模块。ElementTree 就是这种情况。在许多情况下,文档建议使用外部包来处理更复杂的情况。

此外,Pandas 几乎肯定已经在公司中使用。它是分析和数据科学领域最常见的库之一,甚至可能是最常见的库。事实上,像 Anaconda 这样的发行版已经包含了它。

制定该政策的人是否明白,他们是在告诉你继续使用已披露的安全漏洞并有意识地部署不安全的代码?因为这就是你不升级软件包的结果。每个 Python 发行版都附带大量需要在一段时间后更新的软件包。

最后,您必须使用手写代码重新编写和支持已在各种软件包中编写、测试和审查了数年的代码。这些时间本不应该用于为公司创造有价值的、可收费的工作。

经验法则是,一家公司需要赚到您总工资的 2-3 倍才能证明这项工作的合理性。如果算上错误修复和因这些错误而导致的停机时间,那么在产品的整个生命周期内,手写文本读取和 XML 生成的成本是多少?

2024-12-29