Esta es una guía para entender cómo contar las líneas de un archivo utilizando Javascript y NodeJs. Se detallan diversas soluciones y todo el proceso de cómo funcionan. Hay que tener en mente que no tengo experiencia en desarrollo y estos son mis primeros pasos.

Estoy iniciando mi vida en la programación con Javascript para crear una aplicación escalable, por ello comencé un curso gratuito con NodeSchool que ha resultado bastante interesante.

En uno de los ejercicios  se me ha pedido contar las líneas de un archivo, después de investigar un par de días, con diversas respuestas de StackOverflow, leyendo la documentación en NodeJs y haciendo debug de los diversos códigos y funciones he dado con algunas soluciones las cuales voy a mostrar a continuación.

Cabe resaltar  nuevamente que no soy desarrollador, pero nunca es tarde para aprender ¿No?

Los consejos para desarrollar la solución

En el curso, no dan la solución obviamente, pero dan «hints» (pistas) para comenzar, entre ellas:

  1. var fs = require(‘fs’) | Para iniciar operaciones con el FileSystem
  2. fs.readFileSync(‘/path/to/file’) | Como método síncrono (cargar en el búfer)
  3. toString() – var str = buf.toString()  | para convertir el búfer a cadena de texto
  4. .split() | para separar las cadenas de texto

Esos cuatro pasos serían como decir:

  1. Preparar las librerías para operar con el Filesystem
  2. Abrir el archivo en memoria (almacenar su contenido en un objeto, variable)
  3. Convertir ese objeto o variable en texto
  4. Tomar ese contenido y dividirlo por cada nueva línea

Y al final de todo (uno supone) algún método para contar cada una de esas líneas.

Entonces, de un lado tenemos NodeJS  con nuestro script y por el otro un archivo (preferiblemente de texto plano).

¿Cómo ejecutar el código?

Primero deben realizar la instalación de NodeJs (en mi caso utilizo Debian, por lo cual he seguido este tutorial)

Una vez instalado, copiamos el código de Javascript en un archivo, por ejemplo «script.js» y ejecutamos:

nodejs script.js (archivo.txt)

Siendo archivo.txt el que contiene las líneas que queremos contar, en mi caso, este es el contenido de archivo.txt

cantores
colores
sabores
amores

Es decir, que mi resultado al ejecutar el script debería ser  ‘4’ (cuatro líneas).

Para que el tercer parámetro sea el archivo de texto que deseamos analizar, lo único que se debe hacer es utilizar el array de NodeJs y utilizar el tercer objeto:

require('fs').createReadStream(process.argv[2])

Si imprimimos con console.log el contenido de process.argv, podrán notar que es un array donde los dos primeros elementos corresponden a información de node js y de allí en adelante cada parámetro se presentará como un objeto en el array.

Veamos:

//contenido de example.js

console.log(process.argv);


// Ejecuto el script "example.js" y en la misma linea escribo cualquier cosa (después de "example.js")

root@Alberta:/home/mortiz/projects/nodejs# nodejs example.js archivo1 archivo2 ejemplo variable etc
[ '/usr/bin/nodejs',
  '/home/mortiz/projects/nodejs/example.js',
  'archivo1',
  'archivo2',
  'ejemplo',
  'variable',
  'etc' ]

Como el array comienza con 0=/usr/bin/nodejs, 1=/home/mortiz/projects/nodejs/example.js, finalmente 2=archivo1 y por eso es que en todos los ejemplos utilizamos   require(‘fs’).createReadStream(process.argv[2])

Primera Solución (método asíncrono)

Me costó bastante entender la respuesta de Andrey Sidorov en este post de StackOverflow su código para contar las líneas del archivo luce como esto:

var i;
var count = 0;
require('fs').createReadStream(process.argv[2])
  .on('data', function(chunk) {
    for (i=0; i < chunk.length; ++i)
      if (chunk[i] == 10) count++;
  })
  .on('end', function() {
    console.log(count);
  });

Cuando ejecuté:

nodejs script.js (archivo.txt)

El resultado fue el esperado, el script devolvió ‘4’.

Explicación

No estoy muy familiarizado con la programación, pero conozco algunos conceptos, por ejemplo entiendo las variables, los condicionales como if y los loops como for.

También el código es bastante claro con los métodos ‘fs’ (filesystem), ‘data’ y ‘end’, pero ¿qué estaba pasando internamente allí? ¿qué significa ese array chunk[i] con el condicional a == 10?

Pues bien, tras examinar con más detenimiento y leer dichas funciones comprendí mucho mejor que sucedía en esas líneas de código.

Primero se asignan dos variables la (i) para el for y (count) para que haga de contador de líneas del archivo.

Lo primero a lo que me enfrentaba era entender paso a paso qué sucedía, entonces se me ha ocurrido imprimir en pantalla las variables en cada uno de los pasos, para ello, modifiqué el código de esta forma:

var i;
var count = 0;
require('fs').createReadStream(process.argv[2])
  .on('data', function(chunk) { 
        console.log(chunk);
    for (i=0; i < chunk.length; ++i)
        console.log(chunk[i]); 
        if (chunk[i] == 10) count++;
  })
  .on('end', function() { 
    console.log(count);
  });

Siendo lo nuevo:

console.log(chunk);

console.log(chunk[i]);

Lo único que hace esto es imprimir lo que haya almacenado en «chunk» y el contenido del array «chunk[i]», habiendo realizado esta pequeña modificación pude entender qué sucedía.

Esto es lo que me imprimía en pantalla «chunk»

<Buffer 63 61 6e 74 6f 72 65 73 0a 63 6f 6c 6f 72 65 73 0a 73 61 62 6f 72 65 73 0a 61 6d 6f 72 65 73 0a>

Y esto lo que imprimía «chunk[i]»

99
97
110
116
111
114
101
115
10
99
111
108
111
114
101
115
10
115
97
98
111
114
101
115
10
97
109
111
114
101
115
10
0

Viendo esto, entendí que <Buffer> es archivo.txt como objeto en memoria, que luego pasó a ser un array con el contenido del archivo en bytes.

Podemos notar el patrón (repetición) dentro del objeto en su hexadecimal: <0a> y su valor correspondiente en bytes <10> lo cual claramente está repesentando el linebreak (cada línea nueva) que en realidad es «n» (salto de línea).

Lo que hizo nuestro amigo Andrey fue lo siguiente:

  • Cargar el archivo en memoria: require(‘fs’).createReadStream(process.argv[2])
  • Pasar el contenido a un array con el método:  .on (‘data’ …
  • Con un loop parsear los bytes de cada línea «data»:   for …  chunk.length
  • Con un condicional sumar «1» al contador si el valor de ese objeto en el array es = a 10 (es decir un salto de línea): if (chunk[i] == 10)

O al menos es lo que yo pude deducir, hice pruebas creando líneas en blanco para verificar que ese 10 era el valor en bytes de «n».

Me queda la duda, del porqué ya teniendo en el búfer un patrón identificable como <0a> no planteó el condicional del contador directamente allí y lo llevo a bytes. ¿Tal vez sea porque sea más fácil parsear el contenido que el búfer? Lo descubriré más adelante.

Segunda solución (método síncrono)

Esta solución fue escrita por penartur y editada por funroll en este post de StackOverflow. Solo tomé la parte que realiza el conteo de líneas.

fs.readFileSync('./input.txt').toString().split('n').forEach(function (line) { 
    console.log(line);
    fs.appendFileSync("./output.txt", line.toString() + "n");
});

Y lo modifiqué un poco para ajustarlo a mis necesidades

buf.split('n').forEach(function (line) {
    i++
    console.log(i);
});

Sin embargo, leyendo me enteré que forEach no es performante, es decir, es mucho más lento que hacer un ‘for’ directamente.

También vi que la función de process.argv soporta un argumento adicional para el procesamiento del objeto, como sé que es un archivo de texto de cuatro líneas le pasé utf8 directamente, lo cual elimina la necesidad de toString() como vimos en el ejemplo de penartur.

Por lo tanto, modificando el código y «tuneandolo» un poco, llegué a este resultado:

var readline = require('readline');

// buf del archivo
var buf = fs.readFileSync(process.argv[2],'utf8');
var i = 0
for (var i = 0, chunk = buf.split('n'), len =chunk.length; i < len; i++){
}
Number(i);
result = i - 1;

console.log(result);

 Explicación

Mucho más sencillo que el primer ejemplo, simplemente se carga el archivo en buffer (el cual ya se interpreta como texto gracias al parámetro utf8) eliminando la necesidad de toString().

Posteriormente pasamos ese búfer con el contenido del archivo de texto a «split()» que lo va a separar con saltos de línea «n» el problema de split() es que al no tener un salto de línea al final del archivo quedará un elemento adicional en el array resultante de esta función, por lo cual, he restado -1 al final del script.

También podrán notar que pasé el contador a ser interpretado como número con Number(i) para poder hacer la operación de resta al final.

Tercera opción (método síncrono, solución oficial del ejercicio)

Esta es la solución oficial al problema de contar las líneas de un archivo que ofrece learnyounode;

var fs = require('fs')
  
var contents = fs.readFileSync(process.argv[2])
var lines = contents.toString().split('n').length - 1
console.log(lines)

Bastante simple, carga el archivo en el búfer, lo pasa a texto con toString y lo divide con split(‘n’), inmediatamente calcula el length y le resta -1 (por el tema que mencioné anteriormente).

Esta solución tan simple, elimina dos necesidades ‘for’ y ‘Number()’, .length ya devuelve un número y cuenta los elementos del array, por lo cual no hay necesidad de iterar los objetos, simplificando muchísimo más la solución a la que llegue en el punto anterior.

Por último, imprime la cantidad de líneas.

Cuarta Solución (Método asíncrono / Callback)

En esta solución implementamos algo de lo que se ve en las anteriores y un concepto nuevo «callback», donde esperamos a que se termine la lectura del archivo antes de mostrar el resultado.

Como podrán observar, utilizo casi las mismas funciones de los otros ejemplos, excepto que esta vez es «readFile» y la metodología es un callback.

Según lo explicado en la documentación del curso, estas variables se comportan como objetos. CountLines nos da el resultado y logMyResult lo imprime. En este caso, se ejecuta la función anidada y no podremos usar CountLines hasta que se ejecute el callback primero.

//metodo asíncrono, lectura de líneas

var fs = require('fs')
result = undefined

function countLines(cb) {

        fs.readFile(process.argv[2],'utf8', function doneReading(err, fileContents) {

        result = fileContents.toString().split('\n').length - 1

        cb()

        })

 }

function logMyResult() {
  console.log(result)
}

countLines(logMyResult)

 

Resumen

Ha sido un camino interesante, he aprendido a utilizar dos formas distintas de leer y trabajar con archivos como objetos desde NodeJs, también aprendí a optimizar un poco el código que está por allí en internet y finalmente vi como realizar todo esto en la expresión más simple posible.

Espero que tengan tanta diversión como yo al aprender NodeJs.