如何在C#中高效下载、读取和处理CSV

How to efficiently download, read and process CSV in C#

我正在开发一项服务,该服务将从在线资源收集大型 CSV 文件,然后在下载时读取行(最好是分批),并将它们发送到数据库。这应该不会在任何时候使用超过 256MB 的 RAM,并且不会将文件保存到磁盘。

这是一项服务,每 7 天 运行 一次,收集挪威公司登记册中的所有公司,(一个漂亮的 250MB,110 万行 CSV 可以在这里找到:http://hotell.difi.no/download/brreg/enhetsregisteret )

我的应用程序可以轻松下载文件并将其添加到列表<>,然后处理它,但它使用 3.3 GB 的 RAM

public async Task<bool> CollectAndUpdateNorwegianCompanyRegistry()
{
    var request = await _httpClient.GetAsync(_options.Value.Urls["BrregCsv"]);

    request.EnsureSuccessStatusCode();

    using (var stream = await request.Content.ReadAsStreamAsync())
    using (var streamReader = new StreamReader(stream))
    {
        while (!streamReader.EndOfStream)
        {
            using (var csv = new CsvReader(streamReader)) // CsvReader is from the CsvHelper -nuget
            {
                csv.Configuration.Delimiter = ";";
                csv.Configuration.BadDataFound = null;
                csv.Configuration.RegisterClassMap<NorwegianCompanyClassMap>();

                await _sqlRepository.UpdateNorwegianCompaniesTable(csv.GetRecords<NorwegianCompany>().ToList());
            }
        }
    }

    return true;
}

关于 SqlRepository 的小提示:我已将其替换为一个简单的 "destroyer" 方法,该方法仅清除数据,以便在调试时不使用任何额外资源

我希望垃圾收集器 "destroy" 处理文件行时使用的资源,但它没有。

简而言之,我希望发生以下情况: 当 CSV 下载时,它会读取几行,然后将这些发送到一个方法,然后刷新内存中的行

我在处理大型数据集方面绝对没有经验,所以我正在处理其他人的工作,并没有得到我期望的结果

感谢您的宝贵时间和协助

因此,从 Sami Kuhmonen (@sami-kuhmonen) 那里得到一些建议很有帮助,这就是我想出的:

public async Task<bool> CollectAndUpdateNorwegianCompanyRegistry()
{
    using (var stream = await _httpClient.GetStreamAsync(_options.Value.Urls["BrregCsv"]))
    using (var streamReader = new StreamReader(stream))
    using (var csv = new CsvReader(streamReader))
    {
        csv.Configuration.Delimiter = ";";
        csv.Configuration.BadDataFound = null;
        csv.Configuration.RegisterClassMap<NorwegianCompanyClassMap>();

        await _sqlRepository.UpdateNorwegianCompaniesTable(csv.GetRecords<NorwegianCompany>());
    }

    return true;
}

它会在 20 秒内下载整个文件并将其发送到 SqlRepository,从未超过 15% CPU 或 30MB RAM

现在,我的下一个挑战是 SqlRepository,但这个问题已经解决了

我正在实施的另一个解决方案,它的资源使用更可预测:

public async Task<bool> CollectAndUpdateNorwegianCompanyRegistryAlternate()
{
    using (var stream = await _httpClient.GetStreamAsync(_options.Value.Urls["BrregCsv"]))
    using (var reader = new StreamReader(stream))
    using (var csv = new CsvReader(reader))
    {
        csv.Configuration.RegisterClassMap<NorwegianCompanyClassMap>();
        csv.Configuration.Delimiter = ";";
        csv.Configuration.BadDataFound = null;

        var tempList = new List<NorwegianCompany>();

        while (csv.Read())
        {
            tempList.Add(csv.GetRecord<NorwegianCompany>());

            if (tempList.Count() > 50000)
            {
                await Task.Factory.StartNew(() => _sqlRepository.UpdateNorwegianCompaniesTable(tempList));

                tempList.Clear();
            }
        }
    }
    return true;
}

现在它使用 3 分钟,但从来没有达到 200MB 的峰值,使用了 7-12% CPU,即使在执行 SQL "bulk updates" 时,(SqlBulkTool -NuGet 非常适合我这里需要),每 X 行