Python中快速将数字数据转换为定宽格式文件

3 投票
5 回答
1382 浏览
提问于 2025-04-15 16:43

在Python中,如何最快地把只包含数字数据的记录转换成固定宽度的字符串格式,并写入文件呢?比如说,假设有一个很大的列表叫做record,里面有一些对象,这些对象有属性idxywt,我们经常需要把这些数据写到外部文件里。可以用下面的代码来完成这个写入操作:

with open(serial_fname(), "w") as f: 
    for r in records:
        f.write("%07d %11.5e %11.5e %7.5f\n" % (r.id, r.x, r.y, r.wt))

不过,我发现我的代码在生成外部文件上花费了太多时间,导致没有足够的时间去做其他应该做的事情。

对原问题的补充:

我在写一个服务器软件时遇到了这个问题,这个软件需要从多个“生产者”系统获取信息,跟踪一个全局的记录集,并把任何变化实时或近实时地传递给“消费者”系统,数据是经过预处理的。很多消费者系统是Matlab应用程序。

以下是我目前收到的一些建议(谢谢大家),并附上了一些评论:

  • 只写入变化,而不是整个数据集:我其实已经在这么做了,但生成的变化集依然很大。
  • 使用二进制(或其他更高效的)文件格式:我受到Matlab能够高效读取的格式的限制,而且这个格式还需要是跨平台的。
  • 使用数据库:我实际上是在尝试绕过当前的数据库解决方案,因为它被认为太慢且麻烦,尤其是在Matlab那边。
  • 将任务分配给不同的进程:目前,写入文件的代码是在自己的线程中运行。不过由于全局解释器锁(GIL)的原因,它仍然在同一个核心上运行。我想我可以把它移到一个完全独立的进程中。

5 个回答

0

你可以尝试把所有的输出字符串都放在内存里,比如用一个很长的字符串。然后再把这个长字符串写入文件。

更快的方法是:你可以考虑使用二进制文件来记录信息,而不是用文本文件。不过这样的话,你需要再写一个工具来查看这些二进制文件。

2

我看不出你的代码片段有什么可以优化的地方。所以,我觉得我们需要换个思路来解决你的问题。

你的问题似乎是处理大量数据时,格式化数据为字符串并写入文件的速度很慢。你提到“flush”,这意味着你需要定期保存数据。

你是定期保存所有数据,还是只保存变化的数据呢?如果你处理的是一个非常大的数据集,只修改一些数据却写入所有数据……这可能是我们可以解决你问题的一个切入点。

如果你有一个大数据集,并且想要不时更新它……那么你可能需要考虑使用数据库。一个用C语言编写的真正数据库,速度很快,可以处理大量的数据更新,并保持所有记录的一致性。然后你可以定期运行一个“报告”,从中提取记录并生成固定宽度的文本文件。

换句话说,我建议你把问题分成两个部分:在计算或接收更多数据时,逐步更新数据集,以及将整个数据集导出为固定宽度的文本格式,以便后续处理。

值得注意的是,你实际上可以在不停止更新数据库的Python进程的情况下,从数据库生成文本文件。虽然你得到的快照可能不完整,但如果记录是独立的,那应该没问题。

如果你后续处理也是用Python的话,可以考虑将数据永久保留在数据库中。没必要通过固定宽度的文本文件来来回回传输数据。我假设你使用固定宽度文本文件是因为这样提取数据比较方便。

如果你选择使用数据库,建议试试PostgreSQL。它是免费的,而且是真正的数据库。使用Python与数据库时,应该使用ORM(对象关系映射)。其中一个最好的选择是SqlAlchemy。

还有一点需要考虑:如果你是为了将来在其他应用程序中解析和使用数据而将数据保存为固定宽度文本文件格式,而那个应用程序既能读取JSON又能读取固定宽度格式,或许你可以使用一个写JSON的C模块。这样可能不会更快,但也有可能;你可以进行基准测试看看。

除此之外,我的另一个想法是把你的程序分成“工作者”和“更新者”两个部分,工作者生成更新的记录,而更新者负责将记录保存到磁盘。可以让工作者将更新的记录以文本格式输出到标准输出,更新者从标准输入读取并更新数据记录。更新者可以使用字典来存储文本记录;当新记录到达时,它可以简单地更新字典。像这样:

for line in sys.stdin:
    id = line[:7]  # fixed width: id is 7 wide
    records[id] = line # will insert or update as needed

你实际上可以让更新者保持两个字典,在一个字典更新的同时将另一个字典写入磁盘。

将程序分为工作者和更新者是确保工作者不会花太多时间在更新上的好方法,也是平衡多个CPU核心工作负载的好办法。

目前我没有其他想法了。

3

我在尝试检查一下 numpy.savetxt 是否能加快速度,所以写了一个模拟测试:

import sys
import numpy as np

fmt = '%7.0f %11.5e %11.5e %7.5f'
records = 10000

np.random.seed(1234)
aray = np.random.rand(records, 4)

def writ(f, aray=aray, fmt=fmt):
  fw = f.write
  for row in aray:
    fw(fmt % tuple(row))

def prin(f, aray=aray, fmt=fmt):
  for row in aray:
    print>>f, fmt % tuple(row)

def stxt(f, aray=aray, fmt=fmt):
  np.savetxt(f, aray, fmt)

nul = open('/dev/null', 'w')
def tonul(func, nul=nul):
  func(nul)

def main():
  print 'looping:'
  loop(sys.stdout, aray)
  print 'savetxt:'
  savetxt(sys.stdout, aray)

我发现结果(在我的2.4 GHz Core Duo Macbook Pro上,使用的是Mac OS X 10.5.8,Python 2.5.4来自python.org的DMG,numpy 1.4 rc1是从源代码编译的)有点出乎我的意料,但结果是可以重复的,所以我觉得这些结果可能会引起大家的兴趣:

$ py25 -mtimeit -s'import ft' 'ft.tonul(ft.writ)'
10 loops, best of 3: 101 msec per loop
$ py25 -mtimeit -s'import ft' 'ft.tonul(ft.prin)'
10 loops, best of 3: 98.3 msec per loop
$ py25 -mtimeit -s'import ft' 'ft.tonul(ft.stxt)'
10 loops, best of 3: 104 msec per loop

所以,savetxt似乎比调用 write 的循环慢了几个百分点……但是老式的 print(也是在循环中)似乎比 write 快了几个百分点(我猜是因为它避免了一些调用的开销)。我意识到大约2.5%的差异并不是特别重要,但这个结果和我直觉上的预期正好相反,所以我觉得有必要分享一下。(顺便说一下,使用一个真实的文件而不是 /dev/null 只会增加6到7毫秒的时间,所以对结果影响不大,无论是哪种情况)。

撰写回答