【问题标题】:Moving to an arbitrary position in a file in Python在 Python 中移动到文件中的任意位置
【发布时间】:2011-02-28 11:13:25
【问题描述】:

假设我经常需要处理包含未知但大量行的文件。每行包含闭区间 [0, R] 中的一组整数(空格、逗号、分号或一些非数字字符作为分隔符),其中 R 可以任意大。每行的整数个数可以是可变的。很多时候,我在每行上得到相同数量的整数,但偶尔我的行中的数字集不相等。

假设我想转到文件中的第 N 行并检索该行上的第 K 个数字(并假设输入 N 和 K 是有效的——也就是说,我不担心输入错误)。我如何在适用于 Windows 的 Python 3.1.2 中高效地执行此操作?

我不想逐行遍历文件。

我尝试使用 mmap,但在 SO 上四处逛逛时,我了解到这可能不是 32 位构建的最佳解决方案,因为有 4GB 的限制。事实上,我真的不知道如何简单地将 N 行从我当前的位置移开。如果我至少可以“跳转”到第 N 行,那么我可以使用 .split() 并以这种方式获取第 K 个整数。

这里的细微差别是我不需要从文件中抓取一行。我需要抓住几行:它们不一定都彼此靠近,我得到它们的顺序很重要,而且顺序并不总是基于某些确定性的函数。

有什么想法吗?我希望这是足够的信息。

谢谢!

【问题讨论】:

  • 如果有解决方案来解决文件中每个整数的数量相同的情况,我会很高兴。

标签: python file python-3.x


【解决方案1】:

如果文件可能会发生很大变化,另一种解决方案是完全访问适当的数据库。数据库引擎将为您创建和维护索引,因此您可以进行非常快速的搜索/查询。

不过,这可能有点矫枉过正。

【讨论】:

    【解决方案2】:

    Python 的 seek 指向文件中的 byte 偏移量,而不是 line 偏移量,因为这是现代操作系统及其文件系统的工作方式—— OS/FS 根本不会以任何方式记录或记住“行偏移”,Python(或任何其他语言)也无法神奇地猜测它们。任何声称“转到一行”的操作都不可避免地需要“遍历文件”(在幕后)以在行号和字节偏移之间建立关联。

    如果您对此表示满意,只是希望将其隐藏起来,那么解决方案是标准库模块linecache——但性能不会比您自己编写的代码更好。

    如果您需要多次从同一个大文件中读取,一个大的优化将是在该大文件上运行 一次 一个脚本,该脚本构建并保存到磁盘的行号 - 到 - 字节偏移对应(技术上是“索引”辅助文件);然后,您所有的连续运行(直到大文件更改)都可以非常快速地使用索引文件在大文件中以非常高的性能进行导航。这是您的用例吗...?

    编辑:因为显然这可能适用——这是一般的想法(仔细测试、错误检查或优化;-)。制作索引,使用makeindex.py,如下:

    import array
    import sys
    
    BLOCKSIZE = 1024 * 1024
    
    def reader(f):
      blockstart = 0
      while True:
        block = f.read(BLOCKSIZE)
        if not block: break
        inblock = 0
        while True:
          nextnl = block.find(b'\n', inblock)
          if nextnl < 0:
            blockstart += len(block)
            break
          yield nextnl + blockstart
          inblock = nextnl + 1
    
    def doindex(fn):
      with open(fn, 'rb') as f:
        # result format: x[0] is tot # of lines,
        # x[N] is byte offset of END of line N (1+)
        result = array.array('L', [0])
        result.extend(reader(f))
        result[0] = len(result) - 1
        return result
    
    def main():
      for fn in sys.argv[1:]:
        index = doindex(fn)
        with open(fn + '.indx', 'wb') as p:
          print('File', fn, 'has', index[0], 'lines')
          index.tofile(p)
    
    main()
    

    然后去使用它,比如下面的useindex.py

    import array
    import sys
    
    def readline(n, f, findex):
      f.seek(findex[n] + 1)
      bytes = f.read(findex[n+1] - findex[n])
      return bytes.decode('utf8')
    
    def main():
      fn = sys.argv[1]
      with open(fn + '.indx', 'rb') as f:
        findex = array.array('l')
        findex.fromfile(f, 1)
        findex.fromfile(f, findex[0])
        findex[0] = -1
      with open(fn, 'rb') as f:
        for n in sys.argv[2:]:
          print(n, repr(readline(int(n), f, findex)))
    
    main()
    

    这是一个示例(在我的慢速笔记本电脑上):

    $ time py3 makeindex.py kjv10.txt 
    File kjv10.txt has 100117 lines
    
    real    0m0.235s
    user    0m0.184s
    sys 0m0.035s
    $ time py3 useindex.py kjv10.txt 12345 98765 33448
    12345 '\r\n'
    98765 '2:6 But this thou hast, that thou hatest the deeds of the\r\n'
    33448 'the priest appointed officers over the house of the LORD.\r\n'
    
    real    0m0.049s
    user    0m0.028s
    sys 0m0.020s
    $ 
    

    示例文件是詹姆士国王圣经的纯文本文件:

    $ wc kjv10.txt
    100117  823156 4445260 kjv10.txt
    

    100K 行,4.4 MB,如您所见;这需要大约 1/4 秒来索引和 50 毫秒来读取和打印出三个随机 y 行(毫无疑问,这可以通过更仔细的优化和更好的机器大大加速)。内存中的索引(以及磁盘上的索引)每行被索引的文本文件占用 4 个字节,并且性能应该以完美的线性方式扩展,所以如果你有大约 1 亿行,4.4 GB,我预计大约 4-5建立索引需要几分钟,提取并打印出任意三行需要一分钟(并且为索引占用的 400 MB RAM 甚至不会给小型机器带来不便——即使我的小型慢速笔记本电脑毕竟也有 2GB 空间;-)。

    您还可以看到(为了速度和方便)我将文件视为二进制文件(并假设 utf8 编码——当然也适用于像 ASCII 这样的任何子集,例如 KJ 文本文件是 ASCII)并且不要打扰如果文件具有作为行终止符的内容,则将 \r\n 折叠为单个字符(如果需要,在读取每一行之后这样做非常简单)。

    【讨论】:

    • 您的优化解决方案很好。我喜欢。 +1。我仍然必须遍历索引辅助文件,对吗?但当然,这比必须遍历我的原始文件要好。 linecache 似乎将整个文件加载到 RAM 中,我不会总是那么奢侈。
    • @B Rivera,“索引辅助文件”应该足够小,即使对于几百万行的文本文件也可以保存在 RAM 中。让我草拟一个简单的解决方案来展示我的想法(我将很快编辑我的答案以展示这一点)。
    • 你知道吗,我明白你在说什么。在索引辅助文件上使用 linecache。这当然是合理的。
    • 更好的是,不要对索引使用文本格式;使用具有固定字段大小的二进制格式。然后你也可以使用seek()
    • 64 位偏移量列表足以让索引文件允许 O(1) 查找
    【解决方案3】:

    问题在于,由于您的行不是固定长度的,因此您必须注意行结束标记才能进行查找,这实际上变成了“逐行遍历文件”。因此,任何可行的方法仍然是遍历文件,这只是什么可以最快地遍历它的问题。

    【讨论】:

    • 假设我放宽了对不等长行的要求。长度是指实际字符数还是整数数?我将获得的最结构化的文件是具有相同数量的整数的文件,每行由一些分隔符分隔,但它们不一定具有相同数量的字符。这是否可以做我需要的事情?
    • @BRivera,问题是每行的字节数 - 一行中的这些字节如何在整数或其他任何东西之间分配是无关紧要的。
    • 假设它们是文本文件(并假设文件以固定字节数的字符编码,例如纯 ASCII 编码),“等长”意味着相同数量的字符。之所以会有所不同,是因为如果您知道行的 character 长度是固定的,那么您也知道行的 byte 长度是固定的,因此您可以使用file.seek(linelength*(linenum-1)) 移动到开始给定行的字节。但这只有在 linelength 对所有行都相同时才有效。否则,这种计算是不可能的。
    • +1 很公平。也许我要问的应该转移到另一个问题:我收到的是带有 ascii 编码的文本文件。我将进一步将我的数字限制在 [0, 2**32 - 1] 的范围内。是否有将每个整数视为相同大小的文件格式?或者有没有办法将我的文件转换为这种格式?这样,我可以按照您建议的方式使用 .seek 命令。我知道这会增加我的文件大小。但我可以处理。
    • 将数字表示为文件中每个实际 4(32 位)字节集的二进制文件格式将是固定宽度的(假设每行上的数字数量相同)。 ASCII 格式的数字不是固定宽度的原因是因为 ASCII 表示以 10 为底的数字,而它以二进制(以 2 为底)格式存储。由于表示与存储不同,因此宽度不是固定的。当然,一种选择是强制将其固定在 ASCII 中 - 通过用零向左填充数字,以便每个数字具有相同的位数。
    猜你喜欢
    • 1970-01-01
    • 2023-03-27
    • 2022-12-28
    • 1970-01-01
    • 1970-01-01
    • 2022-07-01
    • 2015-12-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多