2012-08-16 16 views
13

Tengo la tarea de crear un script que toma un gran archivo de texto como entrada. Luego necesita encontrar todas las palabras y el número de ocurrencias y crear un nuevo archivo con cada línea mostrando una palabra única y su ocurrencia.¿Es posible hacer este script de shell más rápido?

Como ejemplo tener un archivo con este contenido:

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor 
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud 
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure 
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt 
mollit anim id est laborum. 

Necesito crear un archivo que se parece a esto:

1 AD 
1 ADIPISICING 
1 ALIQUA 
... 
1 ALIQUIP 
1 DO 
2 DOLOR 
2 DOLORE 
... 

Por esta Escribí un guión usando tr, sort y uniq:

#!/bin/sh 
INPUT=$1 
OUTPUT=$2 
if [ -a $INPUT ] 
then 
    tr '[:space:][\-_?!.;\:]' '\n' < $INPUT | 
     tr -d '[:punct:][:special:][:digit:]' | 
     tr '[:lower:]' '[:upper:]' | 
     sort | 
     uniq -c > $OUTPUT 
fi 

¿Qué hacer? es se divide las palabras por espacio como el delimitador. Si la palabra contiene -_?!.;: los rompo en palabras de nuevo. Elimino las puntuaciones, los caracteres especiales y los dígitos y convierto toda la cadena en mayúscula. Una vez hecho esto, lo ordeno y lo paso por uniq para que llegue al formato que quiero.

Ahora descargué la Biblia en formato txt y la usé como entrada. Timing esto que tengo:

scripts|$ time ./text-to-word.sh text.txt b  
./text-to-word.sh text.txt b 16.17s user 0.09s system 102% cpu 15.934 total 

Hice lo mismo con una secuencia de comandos de Python:

import re 
from collections import Counter 
from itertools import chain 
import sys 

file = open(sys.argv[1]) 

c = Counter() 

for line in file.readlines(): 
    c.update([re.sub('[^a-zA-Z]', '', l).upper() 
      for l in chain(*[re.split('[-_?!.;:]', word) 
        for word in line.split()])]) 

file2 = open('output.txt', 'w') 
for key in sorted(c): 
    file2.write(key + ' ' + str(c[key]) + '\n') 

Cuando ejecuta la secuencia de comandos que tengo:

scripts|$ time python text-to-word.py text.txt 
python text-to-word.py text.txt 7.23s user 0.04s system 97% cpu 7.456 total 

Como se puede ver que corría en 7.23s en comparación con el script de shell que se ejecutó en 16.17s. Lo he intentado con archivos más grandes y siempre Python parece triunfar. Tengo algunas preguntas para el senario de arriba:

  1. ¿Por qué es más rápido el guión de Python dado que los comandos de la shell están escritos en C? Me doy cuenta de que el script de shell puede no ser el óptimo.
  2. ¿Cómo puedo mejorar el script de shell?
  3. ¿Puedo mejorar el script de Python?

Para ser claros, no estoy comparando Python con los scripts de shell. No estoy tratando de iniciar una guerra de llama o no necesito respuestas en cualquier otro idioma comparándose para ser más rápido. Utilizando la filosofía de UNIX de conectar comandos pequeños para hacer una tarea, ¿cómo hago que el script de shell sea más rápido?

+5

yo sugeriría cambiar el título a algo así como " ¿Es posible hacer este script de shell más rápido? ", Usando el script de python tan diferente y como punto de comparación. Esto eliminaría el riesgo de discusiones inútiles y fuera de tema sobre las diferencias entre python y shell. –

+5

No creo que la filosofía \ * nix de usar muchos comandos pequeños que hacen bien una sola tarea esté en su lugar porque es más * eficiente *. La razón por la que está en su lugar es porque con las herramientas que tenemos, puede lograr mucho y ahorrarse mucho tiempo desarrollando un nuevo programa para una tarea simple. – mgilson

+1

Python está escrito en C, también. Estar "escrito en C" no es suficiente para acelerar las cosas: las capas intermedias (y todas las líneas de lectura/escritura desde y hacia las tuberías) tienen una sobrecarga. –

Respuesta

7

Un punto importante aquí es probablemente la E/S entre procesos. La secuencia de comandos de Python tiene todos los datos en la memoria, por lo que no ocurre la E/S mientras procesa los datos.

También tenga en cuenta que Python no es lento como tal. La mayoría de las funcionalidades en Python están implementadas en C.

El script de shell debe iniciar 5 procesos y cada uno de ellos debe leer el texto completo desde stdin y escribir el texto completo en stdout cuatro veces.

Puede haber una manera de hacer que el script de Python un poco más rápido: Usted puede leer el texto completo en una sola cadena, entonces retirar todos puntuacion, separar palabras y luego contarlos:

text = file.read() 
text = re.sub(r'[.,:;-_]', '', text) 
text = text.upper() 
words = re.split(r'\\s+', text) 
c = Counter() 
c.update(words) 

Eso haría evitar la sobrecarga de varios bucles anidados.

En cuanto al script de shell: Debe intentar reducir el número de procesos. Los tres procesos tr probablemente podrían reemplazarse con una llamada a sed.

+0

Supongo que el factor más importante es la sobrecarga de lanzar muchos subprocesos. –

+1

@SvenMarnach: No; solo hay cinco procesos involucrados en total. Comenzarlos tomará menos de 1s y sus scripts se ejecutarán durante 16s. –

+0

Sí, tienes razón. (Ya he votado anteriormente). –

3

No es una cuestión de un idioma frente a otro. Tu enfoque es diferente.

En Python, está incrementando un contador para cada palabra a medida que la encuentra, y luego itera su contador para producir la salida. Esto va a ser O (n).

En bash, está poniendo todas sus palabras individualmente en una tupla larga, ordenando la tupla, y luego contando las instancias. Probablemente sea O (nlogn) para el género.

+3

El 'contador' todavía está ordenado que está en el mejor' O (N * log (N)) ' – mgilson

+0

el n del contador es menor que el N de la tupla larga porque hay muchos duplicados –

+0

* Ambos lo tienen mal . De los documentos de Python: * Un contador es una subclase dict para contar objetos hashable. Es una colección desordenada donde los elementos se almacenan como claves de diccionario y sus recuentos se almacenan como valores de diccionario. * El orden de tiempo del contador sigue siendo N porque tiene que inspeccionar todos los N elementos para obtener el recuento de cada uno. Tiene razón en que el orden de memoria del contador es K, donde K es el número de unidades únicas. –

1

usted puede mejorar su escritura del golpe:

sed 's/[^a-zA-Z][^a-zA-Z]*/\'$'\n/g' <$INPUT | sort -f -u >$OUTPUT 

Pero el corto y derecha respuesta a su pregunta es: Puesto que está utilizando totalmente diferentes algoritmos.

+0

Gracias, pero su secuencia de comandos no me da las ocurrencias y se ejecuta más lento. Pero tienes razón al señalar la diferencia en los algoritmos. – satran

0

Puede probar esto:

Considerando archivo de entrada para ser Entrada.txt

script de Bash

cat Input.txt | tr [:space:] '\n' | grep -v "^\s*$" | sort | uniq -c | sort -bnr | tr [:lower:] [:upper:] 
0

Una forma usando GNU awk:

WHINY_USERS=1 awk '{ for (i=1; i<=NF; i++) { sub("[,.]","",$i); array[toupper($i)]++ } } END { for (j in array) print array[j], j }' file.txt 

Pseudocódigo/explicación:

## WHINY_USERS=1 enables sorting by keys. A bit of a trick. 
## Now loop through each word on each line, removing commas, full-stops, 
## adding each word in uppercase to an array. 
## Loop through the array printing vals and keys 

YMMV

0

una solución fiesta

#!/bin/bash 
IFS=' -_?!.;\:,' 
while read -r line; do 
    for word in $line; do 
    word=${word//[^[:alpha:]]/} 
    [ $word ] || continue 
    word=$(tr '[:lower:]' '[:upper:]' <<<"$word") 
    ((_w_$word++)) 
    done 
done <"$INPUT" 
IFS=' ' 
for wword in ${!_w_*}; do echo "${!wword} ${wword#_w_}"; done > $OUTPUT.v1 

una solución Perl campo

perl -nle '$h{uc()}++for/(\w+)/g}{print"$h{$_} $_"for sort keys%h' $INPUT > $OUTPUT.v2 
Cuestiones relacionadas