58

我正在尝试阅读一些文本文件,其中每一行都需要处理。目前我只是使用 StreamReader,然后单独读取每一行。

我想知道是否有更有效的方法(在 LoC 和可读性方面)使用 LINQ 来做到这一点,而不会影响操作效率。我看到的示例涉及将整个文件加载到内存中,然后对其进行处理。但是,在这种情况下,我认为这不会非常有效。在第一个示例中,文件可以达到大约 50k,而在第二个示例中,并非文件的所有行都需要读取(大小通常小于 10k)。

您可能会争辩说,如今这些小文件并不重要,但是我相信这种方法会导致代码效率低下。

第一个例子:

// Open file
using(var file = System.IO.File.OpenText(_LstFilename))
{
    // Read file
    while (!file.EndOfStream)
    {
        String line = file.ReadLine();

        // Ignore empty lines
        if (line.Length > 0)
        {
            // Create addon
            T addon = new T();
            addon.Load(line, _BaseDir);

            // Add to collection
            collection.Add(addon);
        }
    }
}

第二个例子:

// Open file
using (var file = System.IO.File.OpenText(datFile))
{
    // Compile regexs
    Regex nameRegex = new Regex("IDENTIFY (.*)");

    while (!file.EndOfStream)
    {
        String line = file.ReadLine();

        // Check name
        Match m = nameRegex.Match(line);
        if (m.Success)
        {
            _Name = m.Groups[1].Value;

            // Remove me when other values are read
            break;
        }
    }
}
4

5 回答 5

95

您可以使用迭代器块轻松编写基于 LINQ 的行阅读器:

static IEnumerable<SomeType> ReadFrom(string file) {
    string line;
    using(var reader = File.OpenText(file)) {
        while((line = reader.ReadLine()) != null) {
            SomeType newRecord = /* parse line */
            yield return newRecord;
        }
    }
}

或让乔恩高兴:

static IEnumerable<string> ReadFrom(string file) {
    string line;
    using(var reader = File.OpenText(file)) {
        while((line = reader.ReadLine()) != null) {
            yield return line;
        }
    }
}
...
var typedSequence = from line in ReadFrom(path)
                    let record = ParseLine(line)
                    where record.Active // for example
                    select record.Key;

然后你有ReadFrom(...)一个懒惰的评估序列,没有缓冲,非常适合Where等。

请注意,如果您使用OrderByor 标准GroupBy,则必须将数据缓冲在内存中;如果您需要分组和聚合,“PushLINQ”有一些花哨的代码允许您对数据执行聚合但丢弃它(无缓冲)。乔恩的解释在这里

于 2009-08-13T10:45:02.847 回答
24

读取一行并检查它是否为空比一直检查 EndOfStream 更简单。

但是,我在MiscUtilLineReader中也有一个类,它使所有这一切变得更加简单 - 基本上它公开了一个文件(或一个允许您在其上执行 LINQ 的东西。所以您可以执行以下操作:Func<TextReader>IEnumerable<string>

var query = from file in Directory.GetFiles("*.log")
            from line in new LineReader(file)
            where line.Length > 0
            select new AddOn(line); // or whatever

的核心LineReader是这个实现IEnumerable<string>.GetEnumerator

public IEnumerator<string> GetEnumerator()
{
    using (TextReader reader = dataSource())
    {
        string line;
        while ((line = reader.ReadLine()) != null)
        {
            yield return line;
        }
    }
}

几乎所有其余的源代码都只是提供灵活的设置方式dataSource(即 a Func<TextReader>)。

于 2009-08-13T10:45:59.833 回答
2

从 .NET 4.0 开始,该File.ReadLines()方法可用。

int count = File.ReadLines(filepath).Count(line => line.StartsWith(">"));
于 2019-04-10T18:23:42.660 回答
1

注意:您需要注意IEnumerable<T>解决方案,因为它会导致文件在处理期间处于打开状态。

例如,对于 Marc Gravell 的回应:

foreach(var record in ReadFrom("myfile.csv")) {
    DoLongProcessOn(record);
}

该文件将在整个处理过程中保持打开状态。

于 2009-08-13T10:50:08.137 回答
0

谢谢大家的回答!我决定混合使用,主要关注 Marc,因为我只需要从文件中读取行。我猜你可能会争辩说到处都需要分离,但是,生命太短暂了!

关于保持文件打开,在这种情况下这不会成为问题,因为代码是桌面应用程序的一部分。

最后我注意到你们都使用了小写字符串。我知道在 Java 中大写和非大写字符串之间存在区别,但我认为在 C# 中小写字符串只是对大写字符串的引用?

public void Load(AddonCollection<T> collection)
{
    // read from file
    var query =
        from line in LineReader(_LstFilename)
        where line.Length > 0
        select CreateAddon(line);

    // add results to collection
    collection.AddRange(query);
}

protected T CreateAddon(String line)
{
    // create addon
    T addon = new T();
    addon.Load(line, _BaseDir);

    return addon;
}

protected static IEnumerable<String> LineReader(String fileName)
{
    String line;
    using (var file = System.IO.File.OpenText(fileName))
    {
        // read each line, ensuring not null (EOF)
        while ((line = file.ReadLine()) != null)
        {
            // return trimmed line
            yield return line.Trim();
        }
    }
}
于 2009-08-13T16:21:03.290 回答