存储整数列表的最有效方法

4
我最近在做一个项目,其中一个目标是使用尽可能少的内存来存储一系列文件,使用Python 3。除了一个整数列表外,几乎所有文件占用的空间都很小,该列表大约有333,000个整数,并且整数大小不超过8000。
我目前正在使用pickle来存储此列表,它占用大约7mb的空间,但我感觉必须有更省内存的方法来完成这个任务。
我尝试将其存储为文本文件和csv,但这两种方法都使用了超过10mb的空间。

你可能想了解Pandas和HDF5格式(+ blosc压缩)。 - MaxU - stand with Ukraine
你的最大整数需要多少字节? - MaxU - stand with Ukraine
3
“integers up to about 8000 in size” 的意思是“大小不超过约8000的整数”。 - njzk2
你的意思是 所有整数中的最大值 <= 8000 吗? - MaxU - stand with Ukraine
在使用 Python 标准库支持的 zip、gz、lzma 或 bzip2 进行压缩后,pickle/text/csv 文件会变得多大? - Stefan Pochmann
3个回答

4
你可以使用一个 stdlib 的解决方案,即来自array的数组。文档中如下所述:

该模块定义了一种对象类型,可以紧凑地表示基本值的数组:字符、整数、浮点数。数组是序列类型,其行为非常类似于列表,只是存储在其中的对象类型受到限制。

通常情况下,这会减少大型列表的一些内存占用。例如,对于一个包含1000万个元素的列表,使用数组可以节省约 11mb 的内存空间。
import pickle    
from array import array

l = [i for i in range(10000000)]
a = array('i', l)

# tofile can also be used.
with open('arrfile', 'wb') as f:  
    pickle.dump(a, f)

with open('lstfile', 'wb') as f:
    pickle.dump(l, f)

尺寸:
!du -sh ./*
39M     arrfile
48M     lstfile

这可能是OP问题的一个不错解决方案。然而,重要的是要知道,该数组将使用平台本地的C-int类型存储值,而不是Python的任意精度整数。 - 5gon12eder
这个程序在 array('i', [10**7999]) 处崩溃。 - Stefan Pochmann
当然可以@StefanPochmann,超过array('i', [2**31-1])的任何内容都不允许使用'i' :-)。如果OP明确表示size实际上是8000位数(我非常怀疑),我会注意到大整数受底层C类型限制的大小。 - Dimitris Fasarakis Hilliard
就像我在其他地方说的那样:将超过10MB的文本文件除以333000。每个数字超过30字节。他们得有多愚蠢,才会在文本文件中每个四位数占用超过30字节?看起来很确定他们的意思是高达8000位(或者也许是比特)。 - Stefan Pochmann

3

这里是一个小示例,使用了Pandas模块:

import numpy as np
import pandas as pd
import feather

# let's generate an array of 1M int64 elements...
df = pd.DataFrame({'num_col':np.random.randint(0, 10**9, 10**6)}, dtype=np.int64)
df.info()

%timeit -n 1 -r 1 df.to_pickle('d:/temp/a.pickle')

%timeit -n 1 -r 1 df.to_hdf('d:/temp/a.h5', 'df_key', complib='blosc', complevel=5)
%timeit -n 1 -r 1 df.to_hdf('d:/temp/a_blosc.h5', 'df_key', complib='blosc', complevel=5)
%timeit -n 1 -r 1 df.to_hdf('d:/temp/a_zlib.h5', 'df_key', complib='zlib', complevel=5)
%timeit -n 1 -r 1 df.to_hdf('d:/temp/a_bzip2.h5', 'df_key', complib='bzip2', complevel=5)
%timeit -n 1 -r 1 df.to_hdf('d:/temp/a_lzo.h5', 'df_key', complib='lzo', complevel=5)

%timeit -n 1 -r 1 feather.write_dataframe(df, 'd:/temp/a.feather')

DataFrame信息:

In [56]: df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1000000 entries, 0 to 999999
Data columns (total 1 columns):
num_col    1000000 non-null int64
dtypes: int64(1)
memory usage: 7.6 MB

结果(速度):

In [49]: %timeit -n 1 -r 1 df.to_pickle('d:/temp/a.pickle')
1 loop, best of 1: 16.2 ms per loop

In [50]: %timeit -n 1 -r 1 df.to_hdf('d:/temp/a.h5', 'df_key', complib='blosc', complevel=5)
1 loop, best of 1: 39.7 ms per loop

In [51]: %timeit -n 1 -r 1 df.to_hdf('d:/temp/a_blosc.h5', 'df_key', complib='blosc', complevel=5)
1 loop, best of 1: 40.6 ms per loop

In [52]: %timeit -n 1 -r 1 df.to_hdf('d:/temp/a_zlib.h5', 'df_key', complib='zlib', complevel=5)
1 loop, best of 1: 213 ms per loop

In [53]: %timeit -n 1 -r 1 df.to_hdf('d:/temp/a_bzip2.h5', 'df_key', complib='bzip2', complevel=5)
1 loop, best of 1: 1.09 s per loop

In [54]: %timeit -n 1 -r 1 df.to_hdf('d:/temp/a_lzo.h5', 'df_key', complib='lzo', complevel=5)
1 loop, best of 1: 32.1 ms per loop

In [55]: %timeit -n 1 -r 1 feather.write_dataframe(df, 'd:/temp/a.feather')
1 loop, best of 1: 3.49 ms per loop

结果(大小):

{ temp }  » ls -lh a*                                                                                         /d/temp
-rw-r--r-- 1 Max None 7.7M Sep 20 23:15 a.feather
-rw-r--r-- 1 Max None 4.1M Sep 20 23:15 a.h5
-rw-r--r-- 1 Max None 7.7M Sep 20 23:15 a.pickle
-rw-r--r-- 1 Max None 4.1M Sep 20 23:15 a_blosc.h5
-rw-r--r-- 1 Max None 4.0M Sep 20 23:15 a_bzip2.h5
-rw-r--r-- 1 Max None 4.1M Sep 20 23:15 a_lzo.h5
-rw-r--r-- 1 Max None 3.9M Sep 20 23:15 a_zlib.h5

结论:如果您需要同时考虑速度和合理的大小,请注意使用HDF5(+ blosclzo压缩),如果您只关心速度,请使用Feather格式 - 它比Pickle快4倍!


1

我喜欢Jim的建议使用array模块。如果您的数字值足够小,可以适应机器的本地int类型,则这是一个很好的解决方案。(我更喜欢使用array.tofile方法序列化数组,而不是使用pickle。)如果一个int是32位,则每个数字使用4个字节。

不过,我想问一下您如何创建文本文件。如果我创建一个包含333,000个整数的文件,范围在[0, 8,000]之间,每行一个数字,

import random

with open('numbers.txt', 'w') as ostr:
    for i in range(333000):
        r = random.randint(0, 8000)
        print(r, file=ostr)

这个程序只占用了1.6 MiB的空间,相比于二进制表示法使用的1.3 MiB来说并不算太糟糕。而且,如果你有一天遇到超出本地int类型范围的值,文本文件也可以愉快地处理它,而不会发生溢出。

此外,如果我使用gzip对文件进行压缩,文件大小会缩小到686 KiB。这比对二进制数据进行gzip压缩要好!当使用bzip2时,文件大小仅为562 KiB。Python标准库支持gzipbz2,因此您可能想再次尝试纯文本格式加压缩。


@StefanPochmann 这就是我理解的问题;8000也有4位数字。 - 5gon12eder
加油。将他们的文本文件超过10MB除以333000。那是每个数字超过30字节。他们得有多愚蠢,才会在文本文件中每个四位数占用超过30个字节? - Stefan Pochmann
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Stefan Pochmann
嗯,奇怪,我一直得到708-709 KiB。顺便说一下,当我使用array('h', ...)时,得到589-590 KiB。 - Stefan Pochmann
@StefanPochmann 嗯,你使用的是 'h'(即 short),而不是像 Jim 和我一样使用 'i'(即 int)。 - 5gon12eder
显示剩余4条评论

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