2010-08-28 18 views
6

Tengo un archivo de texto de 100 GB, que es un volcado de BCP desde una base de datos. Cuando intento importar con BULK INSERT, me sale un error críptico en la línea número 219506324. Antes de resolver este problema me gustaría ver esta línea, pero por desgracia mi método favorito deManera eficiente de leer un número de línea específico de un archivo. (BONUS: Error de impresión manual de Python)

import linecache 
print linecache.getline(filename, linenumber) 

es lanzar una MemoryError. Curiosamente the manual says que "Esta función nunca arrojará una excepción". En esta archivo grande que arroja una, ya que tratar de leer la línea número 1, y tengo aproximadamente 6 GB RAM libre ...

Me gustaría saber cuál es el más elegante método para llegar a esa línea inalcanzable . Las herramientas disponibles son Python 2, Python 3 y C# 4 (Visual Studio 2010). Sí, entiendo que siempre puedo hacer algo como

var line = 0; 
using (var stream = new StreamReader(File.OpenRead(@"s:\source\transactions.dat"))) 
{ 
    while (++line < 219506324) stream.ReadLine(); //waste some cycles 
    Console.WriteLine(stream.ReadLine()); 
} 

que funcionaría, pero dudo que sea la manera elegante más.

EDIT: Estoy esperando para cerrar este hilo, porque el disco duro que contiene el archivo está siendo utilizado en este momento por otro proceso. Voy a probar los métodos sugeridos y los tiempos del informe. Gracias a todos por sus sugerencias y comentarios.

Los resultados están en Implementé los métodos de Gabes y Alexes para ver cuál era más rápido. Si estoy haciendo algo mal, dígalo. Voy por la línea número 10 millones en mi archivo de 100GB usando el método que Gabe sugirió y luego uso el método que Alex sugirió y que traducido libremente a C# ... Lo único que estoy agregando de mí mismo es la primera lectura en un 300 MB de archivo en la memoria solo para borrar la caché HDD.

const string file = @"x:\....dat"; // 100 GB file 
const string otherFile = @"x:\....dat"; // 300 MB file 
const int linenumber = 10000000; 

ClearHDDCache(otherFile); 
GabeMethod(file, linenumber); //Gabe's method 

ClearHDDCache(otherFile); 
AlexMethod(file, linenumber); //Alex's method 

// Results 
// Gabe's method: 8290 (ms) 
// Alex's method: 13455 (ms) 

La aplicación del método de Gabe es el siguiente:

var gabe = new Stopwatch(); 
gabe.Start(); 
var data = File.ReadLines(file).ElementAt(linenumber - 1); 
gabe.Stop(); 
Console.WriteLine("Gabe's method: {0} (ms)", gabe.ElapsedMilliseconds); 

mientras que el método de Alex es un poco Tricker:

var alex = new Stopwatch(); 
alex.Start(); 
const int buffersize = 100 * 1024; //bytes 
var buffer = new byte[buffersize]; 
var counter = 0; 
using (var filestream = File.OpenRead(file)) 
{ 
    while (true) // Cutting corners here... 
    { 
     filestream.Read(buffer, 0, buffersize); 
     //At this point we could probably launch an async read into the next chunk... 
     var linesread = buffer.Count(b => b == 10); //10 is ASCII linebreak. 
     if (counter + linesread >= linenumber) break; 
     counter += linesread; 
    } 
} 
//The downside of this method is that we have to assume that the line fit into the buffer, or do something clever...er 
var data = new ASCIIEncoding().GetString(buffer).Split('\n').ElementAt(linenumber - counter - 1); 
alex.Stop(); 
Console.WriteLine("Alex's method: {0} (ms)", alex.ElapsedMilliseconds); 

Así que a menos que Alex le importa a comentar que marcaré solución de Gabe como aceptado.

+0

¿Le sugiero que ejecute su archivo gigante a través de una utilidad como * nix 'split' para descomponerlo en un conjunto de archivos más pequeños con los que sería más fácil trabajar? http://ss64.com/bash/split.html – Amber

+1

Cuando se trata de archivos de 100 GB, agregar un pase adicional para dividir los datos, crear otros 100 GB de datos, es un gran golpe de rendimiento, que debe ser evitable. –

+1

Elegante, shmelegant. No olvide que el problema original fue el problema de inserción de la base de datos, no la forma más hábil de leer la línea 'n' del archivo de datos grandes z.dat. Escribe el forro C# cuatro, déjalo correr durante 10 minutos, consigue la línea problemática y continúa. O use 'split' para dividir el archivo como se sugirió @Amber, y vea si simplemente usar archivos más pequeños es suficiente. (Mi sospecha es que BULK INSERT está tratando de hacer todas estas inserciones en una sola transacción, y está explotando algún archivo de base de datos antes de la imagen, ya que tiene que ser ROLLBACK. Vea si tiene una opción BULK INSERT COMMIT EVERY 1000000.) – PaulMcG

Respuesta

8

Aquí está mi versión elegante en C#:

Console.Write(File.ReadLines(@"s:\source\transactions.dat").ElementAt(219506323)); 

o más general:

Console.Write(File.ReadLines(filename).ElementAt(linenumber - 1)); 

Por supuesto, es posible que desee mostrar un poco de contexto antes y después de la línea dada:

Console.Write(string.Join("\n", 
       File.ReadLines(filename).Skip(linenumber - 5).Take(10))); 

o más con fluidez:

File 
.ReadLines(filename) 
.Skip(linenumber - 5) 
.Take(10) 
.AsObservable() 
.Do(Console.WriteLine); 

Por cierto, el módulo linecache no hace nada inteligente con archivos de gran tamaño. Simplemente lee todo, conserva todo en la memoria. Las únicas excepciones que capta son relacionadas con E/S (no se puede acceder al archivo, archivo no encontrado, etc.). Aquí está la parte importante del código:

fp = open(fullname, 'rU') 
    lines = fp.readlines() 
    fp.close() 

En otras palabras, es tratar de encajar todo el archivo de 100 GB en 6 GB de RAM! Lo que debería decir el manual es quizás "Esta función nunca emitirá una excepción si no puede acceder al archivo".

+0

+1, Console.Write (File.ReadLines (filename) .ElementAt (linenumber - 1)); es ciertamente elegante. Teniendo en cuenta que todos los archivos .NET se almacenan en búfer, sería eficiente desde el punto de vista de la entrada/salida del disco, así como también. – VinayC

+0

Este parece ser el método más rápido, ¡gracias! – Gleno

6

Bueno, la memoria puede agotarse en cualquier momento, de forma asíncrona y de manera impredecible - es por eso que el "nunca más una excepción" promesa no se aplica realmente allí (como, por ejemplo, en Java, donde cada método debe especificar qué excepciones puede plantear, algunas excepciones están exentas de esta regla, ya que casi cualquier método, de forma impredecible, puede provocarlas en cualquier momento debido a la escasez de recursos u otras cuestiones de todo el sistema).

linecache intenta leer todo el archivo. Su única alternativa sencilla (esperemos que no tiene prisa) es leer una línea a la vez desde el principio ...:

def readoneline(filepath, linenum): 
    if linenum < 0: return '' 
    with open(filepath) as f: 
     for i, line in enumerate(filepath): 
      if i == linenum: return line 
     return '' 

Aquí, se basa linenum-0 (si no te gusta eso, y su Python es 2.6 o superior, pase un valor inicial de 1 a enumerate), y el valor de retorno es la cadena vacía para los números de línea no válidos.

Algo más rápido (y un lote más complicado) es leer, digamos, 100 MB a la vez (en modo binario) en un búfer; contar el número de extremos de línea en el búfer (solo una llamada .count('\n') en el objeto de cadena de búfer); una vez que el total acumulado de los extremos de línea excede el lienzo que está buscando, busque el N-ésimo final de línea actualmente en el búfer (donde N es la diferencia entre linenum, aquí 1 y el total anterior de extremos de línea), Lea un poco más si el N+1 st-end no está también en el búfer (ya que ese es el punto donde termina su línea), extraiga la subcadena relevante. No solo un par de líneas netas de with y devoluciones para casos anómalos ... ;-).

Editar: dado que OP comenta que leer by-buffer en lugar de by-line puede hacer una diferencia en el rendimiento, he desenredado un código antiguo donde estaba midiendo los dos enfoques para tareas relacionadas: - contar el número de líneas con el enfoque de búfer, un bucle en líneas o leer todo el archivo en la memoria de un trago (por readlines). El archivo de destino es kjv.txt, el texto estándar de Inglés versión de la Biblia del Rey James, una línea por cada verso, ASCII:

$ wc kjv.txt 
    114150 821108 4834378 kjv.txt 

Plataforma Pro es un portátil MacOS, OSX 10.5.8, procesador Intel Core 2 Duo a 2.4 GHz, Python 2.6.5.

El módulo para la prueba, readkjv.py:

def byline(fn='kjv.txt'): 
    with open(fn) as f: 
     for i, _ in enumerate(f): 
      pass 
    return i +1 

def byall(fn='kjv.txt'): 
    with open(fn) as f: 
     return len(f.readlines()) 

def bybuf(fn='kjv.txt', BS=100*1024): 
    with open(fn, 'rb') as f: 
     tot = 0 
     while True: 
      blk = f.read(BS) 
      if not blk: return tot 
      tot += blk.count('\n') 

if __name__ == '__main__': 
    print bybuf() 
    print byline() 
    print byall() 

Los print s son sólo para confirmar la exactitud de curso (y hacer ;-).

La medida, por supuesto, después de un par de simulacros para garantizar que todo el mundo está beneficiando por igual de los sistemas operativos, controlador de disco de, y del sistema de archivos funcionalidad prelectura (si lo hay):

$ py26 -mtimeit -s'import readkjv' 'readkjv.byall()' 
10 loops, best of 3: 40.3 msec per loop 
$ py26 -mtimeit -s'import readkjv' 'readkjv.byline()' 
10 loops, best of 3: 39 msec per loop 
$ py26 -mtimeit -s'import readkjv' 'readkjv.bybuf()' 
10 loops, best of 3: 25.5 msec per loop 

Los números son bastante repetible. Como puede ver, incluso en un archivo tan pequeño (¡menos de 5 MB!), Los enfoques por línea son más lentos que los basados ​​en el búfer, ¡demasiado esfuerzo desperdiciado!

Para comprobar la escalabilidad, que junto utilizó un archivo de 4-tiempos-más grandes, de la siguiente manera:

$ cat kjv.txt kjv.txt kjv.txt kjv.txt >k4.txt 
$ wc k4.txt 
    456600 3284432 19337512 k4.txt 
$ py26 -mtimeit -s'import readkjv' 'readkjv.bybuf()' 
10 loops, best of 3: 25.4 msec per loop 
$ py26 -mtimeit -s'import readkjv' 'readkjv.bybuf("k4.txt")' 
10 loops, best of 3: 102 msec per loop 

y, como se predijo, el enfoque por tampón escalas casi exactamente linealmente. Extrapolación (siempre un esfuerzo arriesgado, por supuesto ;-), un poco menos de 200 MB por segundo parece ser el rendimiento predecible, llámelo 6 segundos por GB, o tal vez 10 minutos para 100 GB. Por supuesto, lo que este pequeño programa hace es solo conteo de líneas, pero (una vez que hay suficiente E/S t amortiza los gastos generales constantes ;-) un programa para leer una línea específica debería tener un rendimiento similar (aunque necesita más procesamiento) una vez que se encuentra "el" búfer de interés, es aproximadamente cantidad constante de procesamiento, para un búfer de un tamaño dado, presumiblemente, se reduce a la mitad el búfer para identificar una parte lo suficientemente pequeña, luego un poco de esfuerzo lineal en el tamaño del "resto de tampón" multiplicado por la mitad).

Elegante? No realmente ... pero, por velocidad, ¡bastante difícil de superar!-)

+0

¿De verdad crees que sería mucho más rápido hacer un recuento de final de línea de bajo nivel? Pensaría que de cualquier manera sería tan lento como leer todo el archivo desde el disco. – Gabe

+2

Cuando algo que se llama a sí mismo "caché" proclama que nunca lanzará una excepción, me implica que descarta los miembros antiguos de su caché cuando se queda sin memoria. De acuerdo con los documentos para la función, * * * no esperaba que todos los archivos solicitados se leyeran en la memoria y ¡nunca los libere! – Gabe

+0

@Gabe, entiendo lo que quieres decir con los documentos, y te recomiendo que propongas el parche específico en los documentos de Python que te aclararía el problema de forma más clara (ver http://www.python.org/dev/patches/) Velocidad de Wrt, leer un archivo secuencialmente 100MB a la vez es más rápido que leerlo 100GB a la vez a menos que tengas más de 100GB de RAM disponible (ya que el último curso falla o se agita en la paginación); y es más rápido que leer una línea a la vez, ya que evita mucho análisis, asignación de objetos, etc., que no necesita, y utiliza un búfer más grande. –

0

No es una solución elegante pero más rápida sería usar múltiples hilos (o tareas en .NET 4.0) para leer & procesar múltiples fragmentos de un archivo al mismo tiempo.

+0

Es poco probable que pueda leer los datos más rápido de lo que puede procesarlos, por lo que tratar de hacerlo en paralelo no tiene sentido. – Gabe

+0

@Gabe, en realidad estoy sugiriendo leer los datos paralelos (es posible buscando la posición del archivo); dependiendo del sistema del disco, puede mejorar el rendimiento. – VinayC

+0

No entiendo lo que estás sugiriendo. Dado que la única manera de saber en qué número de línea está un byte específico es haber observado cada byte antes de ese específico, ¿cómo va a ayudarlo la búsqueda? – Gabe

0

Si espera tener esta operación a menudo en el mismo archivo, tendría sentido hacer un índice.

Realice un índice recorriendo todo el archivo una vez y registrando las posiciones de inicio de línea, por ejemplo en una base de datos sqlite. Luego, cuando necesite ir a una línea específica, consulte el índice, busque esa posición y lea la línea.

1

Puede probar este sed one-liner: sed '42q;d' para buscar el número de línea 42. No está en Python o C#, pero supongo que tiene sed en su Mac.

Cuestiones relacionadas