快速高效地将以空格分隔的数字文件读入数组的方法?

3
我需要一种快速高效的方法将以空格分隔的数字文件读入数组中。文件格式如下:
4 6
1 2 3 4 5 6
2 5 4 3 21111 101
3 5 6234 1 2 3
4 2 33434 4 5 6

第一行表示数组的维度 [行数 列数]。接下来的行包含数组数据。

数据也可以像这样没有换行符格式化:

4 6
1 2 3 4 5 6 2 5 4 3 21111 101 3 5 6234 1 2 3 4 2 33434 4 5 6

我可以阅读第一行并使用行和列值初始化一个数组。然后,我需要用数据值填充数组。我的第一个想法是逐行读取文件并使用拆分函数。但是第二种格式让我有些犹豫,因为整个数组数据将一次性加载到内存中。其中一些文件大小达到了100 MB。第二种方法是分块读取文件,然后逐个解析它们。也许有人有更好的方法来处理这个问题?

第二种格式是否会有额外的换行符?例如:{ 8 6 } {1 2 3 4 5 6 2 5 4 3 2 1 3 5 6 1 2 3 4 2 3 4 5 6} {2 3 4 5 6 7 3 4 5 6 7 8 4 5 6 7 8 9 5 6 7 8 9 0} - AllenG
是的,第二种格式中没有额外的换行符。 - John_Sheares
7个回答

2

逐个字符读取文件。如果是空格,则开始一个新数字。如果是数字,则使用它。

对于有多个数字的数字,保持计数器变量:

int counter = 0;
while (fileOpen) {
    char ch = readChar(); // use your imagination to define this method.
    if (isDigit(ch)) {
        counter *= 10;
        counter += asciiToDecimal(ch);
    } else if (isWhitespace(ch)) {
        appendToArray(counter);
        counter = 0;
    } else {
        // Error?
    }
}

澄清编辑:


需要进行一些调整,以处理连续多个空格字符(或换行符)的情况,但除此之外还不错。+1。 - dtb
是的,这并不是一个万能解决方案——仅仅是一个引导OP思考的指南。编辑:此外,“appendToArray()”的调用对于前两个数字需要改为其他内容。 - TreDubZedd

2
一旦数据被加载,您的使用模式是什么?您通常需要接触每个数组元素还是只进行稀疏/随机访问?
如果需要接触大多数数组元素,则将其加载到内存中可能是最好的选择。
如果只需要访问某些元素,则可能希望延迟加载所需的元素到内存中。一种策略是确定文件使用的两个布局之一(有/无换行符),并创建一个算法以根据需要直接从磁盘加载特定元素(查找给定文件偏移量,读取和解析)。为了有效地重新访问相同的元素,可以考虑将已读取的元素保存在由偏移量索引的字典中。在寻找特定值之前,首先检查字典是否存在该元素。
总的原则是除非测试证明需要采用更复杂的方法(避免过早优化),否则应采取简单的方法。

1

这样怎么样:

    static void Main()
    {
        // sample data
        File.WriteAllText("my.data", @"4 6
1 2 3 4 5 6
2 5 4 3 21111 101
3 5 6234 1 2 3
4 2 33434 4 5 6");

        using (Stream s = new BufferedStream(File.OpenRead("my.data")))
        {
            int rows = ReadInt32(s), cols = ReadInt32(s);
            int[,] arr = new int[rows, cols];
            for(int y = 0 ; y < rows ; y++)
                for (int x = 0; x < cols; x++)
                {
                    arr[y, x] = ReadInt32(s);
                }
        }
    }

    private static int ReadInt32(Stream s)
    { // edited to improve handling of multiple spaces etc
        int b;
        // skip any preceeding
        while ((b = s.ReadByte()) >= 0 && (b < '0' || b > '9')) {  }
        if (b < 0) throw new EndOfStreamException();

        int result = b - '0';
        while ((b = s.ReadByte()) >= '0' && b <= '9')
        {
            result = result * 10 + (b - '0');
        }
        return result;
    }

实际上,这并没有非常具体地说明分隔符 - 它几乎会假定任何不是整数的东西都是分隔符,并且它仅支持ASCII(如果您需要其他编码,则可以使用阅读器)。


0
假设我们已经将整个文件读入字符串。
你说前两个是行和列,所以我们肯定需要解析这些数字。
之后,我们可以取出前两个数字,创建我们的数据结构,并相应地填充它。
var fileData = File.ReadAllText(...).Split(' ');
var convertedToNumbers = fileData.Select(entry => int.Parse(entry));
int rows = convertedToNumbers.First();
int columns = convertedToNumbers.Skip(1).First();
// Now we have the number of rows, number of columns, and the data.
int[,] resultData = new int[rows, columns];
// Skipping over rows and columns values.
var indexableData = convertedToNumbers.Skip(2).ToList();
for(int i=0; i<rows; i++)
    for(int j=0; j<columns; j++)
        resultData[i, j] = inedexableData[i*rows + j];

另一种方法是从流中读取前两个,初始化数组,然后每次读取 n 个值,这会很复杂。此外,最好尽可能短地保持文件打开。


我们不能假设一次性将整个文件读入内存。 - luke

0
你需要将文件流式传输到内存中,并边传输边解析。
private IEnumerable<String> StreamAsSpaceDelimited(this StreamReader reader)
{
    StringBuilder builder = new StringBuilder();
    int v;
    while((v = reader.Read()) != -1)
    {
        char c = (char) v;
        if(Char.IsWhiteSpace(c))
        {
            if(builder.Length >0)
            {
                yield return builder.ToString();
                builder.Clear();
            }
        }
        else
        {
            builder.Append(c);
        }
    }
    yield break;
}

这将把文件解析为一组以空格分隔的字符串(惰性地),然后您可以像读取以下内容一样读取它们:

using(StreamReader sr = new StreamReader("filename"))
{
    var nums = sr.StreamAsSpaceDelimited().Select(s => int.Parse(s));
    var enumerator = nums.GetEnumerator();
    enumerator.MoveNext();
    int numRows = enumerator.Current;
    enumerator.MoveNext();
    int numColumns = enumerator.current;
    int r =0, c = 0;
    int[][] destArray = new int[numRows][numColumns];
    while(enumerator.MoveNext())
    {
        destArray[r][c] = enumerator.Current;
        c++;
        if(c == numColumns)
        {
            c = 0;
            r++;
            if(r == numRows)
               break;//we are done
        }
    }

因为我们使用迭代器,所以每次最多只读取几个字符。这是解析大型文件的常用方法(例如 LINQ2CSV 的工作方式)。


0

这里有两种方法

IEnumerable<int[]> GetArrays(string filename, bool skipFirstLine)
{
    using (StreamReader reader = new StreamReader(filename))
    {
        if (skipFirstLine && !reader.EndOfStream)
            reader.ReadLine();

        while (!reader.EndOfStream)
        {
            string temp = reader.ReadLine();
            int[] array = temp.Trim().Split().Select(s => int.Parse(s)).ToArray();
            yield return array;
        }
    }
}

int[][] GetAllArrays(string filename, bool skipFirstLine)
{
    int skipNumber = 0;
    if (skipFirstLine )
        skipNumber = 1;
    int[][] array = File.ReadAllLines(filename).Skip(skipNumber).Select(line => line.Trim().Split().Select(s => int.Parse(s)).ToArray()).ToArray();
    return array;
}

如果你正在处理大文件,第一个选项可能更可取。如果文件很小,则第二个选项可以将整个文件加载到锯齿数组中。

你不能使用ReadLine,因为文件可能包含任意长的行(如多个MB),所以可能会出现内存不足错误。 - luke
啊,我没注意到第二个文件结构问题。 - Anthony Pegram

0

除非你用于解析这些文本文件的机器受到限制,否则几百兆字节大小的文件仍然可以放入内存中。我建议采用第一种读取行并使用分割符的方法。

如果内存成为问题,那么第二种按块读取的方法也可以正常工作。

基本上,我想说的是,只需实施它并测量性能是否有问题即可。


但是,假设100 MB是ASCII编码,则在.NET中创建时应将其加倍。现在将其拆分,至少再加倍,再加上开销和新数组。再加上整数数组(每个4字节)。只有在x64之后,您才能自信地说它适合内存... - Marc Gravell

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