Tengo un conjunto de datos CSV bastante grande, alrededor de 13.5MB y con aproximadamente 120,000 filas y 13 columnas. El siguiente código es la solución actual que tengo implementada.

private IEnumerator readDataset()
{
    starsRead = 0;
    var totalLines = File.ReadLines(path).Count();
    totalStars = totalLines - 1;

    string firstLine = File.ReadLines(path).First();
    int columnCount = firstLine.Count(f => f == ',');

    string[,] datasetTable = new string[totalStars, columnCount];

    int lineLength;
    char bufferChar;
    var bufferString = new StringBuilder();
    int column;
    int row;

    using (FileStream fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
    using (BufferedStream bs = new BufferedStream(fs))
    using (StreamReader sr = new StreamReader(bs))
    {
        string line = sr.ReadLine();
        while ((line = sr.ReadLine()) != null)
        {
            row = 0;
            column = 0;
            lineLength = line.Length;
            for (int i = 0; i < lineLength; i++)
            {
                bufferChar = line[i];
                if (bufferChar == ',')
                {
                    datasetTable[row, column] = bufferString.ToString();
                    column++;
                }
                else
                {
                    bufferString.Append(bufferChar);
                }
            }
            row++;
            starsRead++;
            yield return null;
        }
    }
}

Afortunadamente, como estoy ejecutando esto a través de una rutina de Unity, el programa no se congela, pero esta solución actual tarda 31 minutos y 44 segundos en leer la totalidad del archivo CSV.

¿Hay alguna otra manera de hacer esto? Estoy tratando de apuntar a un tiempo de análisis de menos de 1 minuto.

0
SidS 27 may. 2020 a las 15:10

4 respuestas

La mejor respuesta

El error básico que está cometiendo es hacer solo 1 sola línea por cuadro para que pueda calcular básicamente cuánto tiempo tardará alrededor de 60 fps:

120,000 rows / 60fps = 2000 seconds = 33.3333 minutes

Debido a yield return null;

Creo que ya podría acelerarlo mucho simplemente usando un {{X0} } me gusta

...

var stopWatch = new Stopwatch();
stopWatch.Start();

// Use the last frame duration as a guide for how long one frame should take
var targetMilliseconds = Time.deltaTime * 1000f;

while ((line = sr.ReadLine()) != null)
{
    ....

    // If you are too long in this frame render one and continue in the next frame
    // otherwise keep going with the next line
    if(stopWatch.ElapsedMilliseconds > targetMilliseconds)
    {
        yield return null;
        stopWatch.Restart();
    }
}

Esto permite trabajar en varias líneas dentro de un cuadro mientras se intenta mantener una velocidad de cuadro de 60 fps. Es posible que desee experimentar un poco con él para encontrar un buen equilibrio entre la velocidad de fotogramas y la duración. P.ej. tal vez pueda permitir que se ejecute con solo 30 fps pero importando más rápido ya que así puede manejar más filas en un marco.


Entonces no deberías leer "manualmente" a través de cada byte / char. En su lugar, use los métodos incorporados para eso, por ejemplo String.Split.

De hecho, estoy usando un poco más avanzado Regex.Matches ya que si exportas un CSV desde Excel, permite casos especiales como una celda que contiene un , u otros caracteres especiales como p. Ej. saltos de línea(!).

Excel lo hace envolviendo la celda en " en este caso. Lo que agrega un segundo caso especial, es decir, una celda que contiene un ".

El Regex.Marches es bastante complejo, por supuesto y lento, pero cubre estos casos especiales.

Si conoce bien el formato de su CSV y no lo necesita, podría / debería preferir simplemente seguir

var columns = row.Split(new []{ ','});

Dividirlo siempre solo en , que correría más rápido.

private const char Quote = '\"';
private const string LineBreak = "\r\n";
private const string DoubleQuote = "\"\"";

private IEnumerator readDataset(string path)
{
    starsRead = 0;
    var totalLines = File.ReadLines(path).Count();
    totalStars = totalLines - 1;

    string firstLine = File.ReadLines(path).First();
    int columnCount = firstLine.Count(f => f == ',');

    string[,] datasetTable = new string[totalStars, columnCount];


    using (FileStream fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
    {
        using (BufferedStream bs = new BufferedStream(fs))
        {
            using (StreamReader sr = new StreamReader(bs))
            {
                var stopWatch = new Stopwatch();
                stopWatch.Start();

                // Use the last frame duration as a guide how long one frame should take
                // you can also try and experiment with hardcodd target framerates like e.g. "1000f / 30" for 30fps
                var targetMilliseconds = Time.deltaTime * 1000f;

                string row = sr.ReadLine();

                var columns = new List<string>();

                while ((row = sr.ReadLine()) != null)
                {
                    // Look for the following expressions:
                    // (?<x>(?=[,\r\n]+))           --> Creates a Match Group (?<x>...) of every expression it finds before a , a \r or a \n (?=[...])
                    // OR |
                    // ""(?<x>([^""]|"""")+)""      --> An Expression wrapped in single-quotes (escaped by "") is matched into a Match Group that is neither NOT a single-quote [^""] or is a double-quote
                    // OR |
                    // (?<x>[^,\r\n]+)),?)          --> Creates a Match Group (?<x>...) that does not contain , \r, or \n
                    var matches = Regex.Matches(row, @"(((?<x>(?=[,\r\n]+))|""(?<x>([^""]|"""")+)""|(?<x>[^,\r\n]+)),?)", RegexOptions.ExplicitCapture);

                    foreach (Match match in matches)
                    {
                        columns.Add(match.Groups[1].Value == "\"\"" ? "" : match.Groups[1].Value.Replace("\"\"", Quote.ToString()));
                    }

                    // If last thing is a `,` then there is an empty item missing at the end
                    if (row.Length > 0 && row[row.Length - 1].Equals(','))
                    {
                        columns.Add("");
                    }

                    for (var colIndex = 0; colIndex < Mathf.Min(columnCount, columns.Count); colIndex++)
                    {
                        datasetTable[starsRead, colIndex] = columns[colIndex];
                    }

                    columns.Clear();

                    starsRead++;

                    // If you are too long in this frame render one and continue in the next frame
                    // otherwise keep going with the next line
                    if (stopWatch.ElapsedMilliseconds > targetMilliseconds)
                    {
                        yield return null;
                        stopWatch.Restart();
                    }
                }
            }
        }
    }
}

En general: sería mucho más rápido, supongo que no usaría una Corsina en absoluto sino que haría todo en un hilo y solo devolvería el resultado. FileIO y el análisis de cadenas siempre es bastante lento.

0
derHugo 27 may. 2020 a las 18:04

¿Qué hay de esto?

private IEnumerable<string[]> ReadCsv(string path)
{
    using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None, 64 * 1024, FileOptions.SequentialScan)) 
    using (var reader = new StreamReader(fs))
    {
        string line = reader.ReadLine();
        while ((line = reader.ReadLine()) != null)
        {
            yield return line.Split(',');
        }
    }
}

Debería ser más rápido porque:

  • Lee el archivo solo una vez, lo lees dos veces.
  • FileOptions.SequentialScan ayuda al sistema operativo a almacenarlo en caché de manera más eficiente.
  • El búfer más grande reduce las llamadas al sistema.

También es más eficiente en términos de memoria ya que no mantiene toda la información en la memoria. ¿Necesita mantener toda la información en la memoria, o puede procesarla línea por línea?

0
Jesús López 27 may. 2020 a las 18:20

Es posible que tenga un problema de memoria. Abra el Administrador de tareas mientras se ejecuta el código para ver si está alcanzando la cantidad máxima de memoria.

Intenta lo siguiente:

        private void readDataset()
        {

            List<List<string>> datasetTable = new List<List<string>>(); ;


            using (StreamReader sr = new StreamReader(path))
            {
                string line = sr.ReadLine();  //skip header row
                while ((line = sr.ReadLine()) != null)
                {
                    datasetTable.Add(line.Split(new char[] { ',' }).ToList());
                }
            }
        }
-2
jdweng 27 may. 2020 a las 13:35

¿Ha probado CSVHelper? Es bastante optimizado y rápido: https://joshclose.github.io/CsvHelper/

-2
Nolmë Informatique 27 may. 2020 a las 12:36