Identificar las principales palabras recurrentes de una lista de correos electrónicos basada en un diccionario de palabras interesantes

Un directorio D contiene algunos miles de correos electrónicos en formato .eml. Algunos correos electrónicos son texto sin formato, otros provienen de Outlook, otros tienen un encabezado ASCII y contenido HTML/MIME, etc. Existe un archivo de diccionario F que contiene una lista de palabras interesantes (es decir, rojo\nazul\nverde\n...) para buscar en los archivos debajo del directorio D. El directorio D tiene una gran cantidad de subcarpetas, pero no tiene archivos aparte de los archivos .eml mencionados anteriormente. Se debe hacer una lista de las principales palabras recurrentes con estas especificaciones:

  • Para cada palabra interesante, se debe proporcionar información sobre cuántas veces aparece y dónde aparece. Si ocurre varias veces dentro de un archivo, debe informarse varias veces para ese archivo. Informar una ocurrencia significa informar una tupla (L,P) de números enteros, donde L es el número de línea desde la parte superior de la fuente del correo electrónico y P es la posición, dentro de esa línea, del comienzo de la ocurrencia.

Esto crearía tanto un índice para referirse a las diferentes ocurrencias como un resumen de las palabras interesantes que ocurren con mayor frecuencia.

La salida debe estar en un solo archivo de salida y el formato no está estrictamente definido, siempre que se incluya la información anterior: palabras interesantes, número de veces que aparece cada palabra interesante y dónde aparece -> archivo/línea/posición de inicio.

Este no es un ejercicio de tarea, sino un análisis de texto real que me gustaría hacer de un conjunto de datos bastante grande. El desafío que tengo es elegir la herramienta adecuada para filtrar de manera eficiente. Un enfoque iterativo, producto cartesiano de palabras/correos electrónicos/etc., es demasiado lento y sería deseable combinar el filtrado de múltiples palabras para cada línea de cada archivo.

Experimenté creando una expresión regular de alternativas a partir de la lista de palabras interesantes, w1|w2|w3|..., compilándola y ejecutándola en cada línea de cada correo electrónico, pero sigue siendo lenta, especialmente cuando necesito verificar varias ocurrencias dentro de una sola línea.

Ejemplo:

El correo electrónico E tiene una línea que contiene el texto:

^ ... blah ... manzanas rojas ... arándanos azules ... bandera roja, blanca y azul.$\n

la expresión regular informa correctamente rojo (2) y azul (2) pero es lento cuando se usa el diccionario real y muy grande de palabras interesantes.

Otro enfoque que he probado es:

use una base de datos Sqlite para volcar tokens a medida que se analizan, incluida la información (columna, posición) para cada entrada, y solo consulta la salida al final. Las inserciones por lotes ayudan mucho, con el búfer en memoria apropiado, pero aumentan la complejidad.

Todavía no he experimentado con la paralelización de datos, ya que no estoy seguro de que los tokens/análisis sean lo correcto en primer lugar. ¿Quizás un árbol de letras sería más adecuado?

Estoy interesado en soluciones en, en orden de preferencia:

  • Herramientas Bash/GNU CLI (especialmente algo paralelizable a través de GNU 'paralelo' para ejecución solo CLI)
  • Python (¿PNL?)
  • C / C ++

No Perl porque no lo entiendo, desafortunadamente.

preguntado el 11 de junio de 12 a las 19:06

Sugiero dividir el problema en subsecciones más pequeñas. En otras palabras, si su problema es analizar archivos .eml, debe hablar de eso y nada más. Si su problema es sobre la frecuencia de palabras, estamos dispuestos a asumir que ya tiene (analizado) los datos y solo necesita analizarlos. -

Suponga que la extracción de texto de una variedad de formatos de origen (Outlook, texto sin formato con encabezados, MIME) ya se resolvió, ya que estoy usando una biblioteca para hacerlo. -

Tiene curiosidad por saber por qué le preocupa tanto el rendimiento, ya que solo está enviando a un archivo ... a menos que esté haciendo esto varias veces cada segundo/minuto/hora. Si fuera una vez al día, etc., parecería que el rendimiento no sería un gran problema. -

Como tengo que pagarlo, es un proceso por lotes en la nube. -

¿Puede replantear el problema en un par de párrafos y algún código de ejemplo, por favor? -

3 Respuestas

Supongo que puede crear/encontrar un convertidor de eml a texto. Entonces esto está bastante cerca de lo que quieres:

find -type f | parallel --tag 'eml-to-text {} | grep -o -n -b -f /tmp/list_of_interesting_words'

La salida no tiene el formato 100% como lo desea:

nombre de archivo \t línea no: byte no (desde el inicio del archivo): palabra

Si tienes muchas palabras interesantes, la '-f' en grep es lento para iniciarse, por lo que si puede crear una versión desempaquetada de su maildir, puede hacer un inicio paralelo grep menos veces:

find . -type f | parallel 'eml-to-text {} >/tmp/unpacked/{#}'
find /tmp/unpacked -type f | parallel -X grep -H -o -n -b -f /tmp/list_of_interesting_words

Desde la complejidad del tiempo de grep -f es peor que lineal, es posible que desee dividir /tmp/list_of_interesting_words en bloques más pequeños:

cat /tmp/list_of_interesting_words | parallel --pipe --block 10k --files > /tmp/blocks_of_words

Y luego procesa los bloques y los archivos en paralelo:

find /tmp/unpacked -type f | parallel -j1 -I ,, parallel --arg-file-sep // -X grep -H -o -n -b -f ,, {} // - :::: /tmp/blocks_of_words

Esta salida está formateada como:

nombre de archivo: línea no: byte no (desde el inicio del archivo): palabra

Para tenerlo agrupado por word en lugar de nombre de archivo, canalice el resultado a través de la ordenación:

... | sort -k4 -t: > index.by.word

Para contar la frecuencia:

... | sort -k4 -t: | tee index.by.word | awk 'FS=":" {print $4}' | uniq -c

La buena noticia es que esto debería ser bastante rápido, y dudo que puedas lograr la misma velocidad usando Python.

Edit:

grep -F es mucho más rápido al comenzar, y querrá -w para grep (por lo que la palabra 'gram' no coincide con 'diagrams'); esto también evitará los archivos temporales y probablemente sea razonablemente rápido:

find . -type f | parallel --tag 'eml-to-text {} | grep -F -w -o -n -b -f /tmp/list_of_interesting_words' | sort -k3 -t: | tee index.by.word | awk 'FS=":" {print $3}' | uniq -c

Respondido el 12 de junio de 12 a las 11:06

Impresionante. Tuve que estudiar bastante esto antes de entenderlo, muchas opciones que nunca había usado antes. No resuelve mi problema por completo, pero aprendí tanto que aceptaré la respuesta. ¡Gracias! - robottinosino

Algunas observaciones:

  • No deseamos hacer algo como "para todos los correos electrónicos, realice una búsqueda de expresiones regulares y haga_algo();". Puedo imaginar que la longitud de la mayoría de los correos electrónicos sea más corta que la lista de palabras interesantes, así que trataría de procesar cada correo electrónico individualmente y extraer la información necesaria.
  • Cree una estructura de datos de cadena especializada (como un intento de cuerda or árbol de búsqueda ternario) para buscar rápidamente si una palabra es interesante o no. Tendría buenas experiencias con la construcción de un árbol de búsqueda ternario de palabras, ya que permite búsquedas rápidas de palabras.
  • El algoritmo entonces se vería así:

(en pseudocódigo, por supuesto)

result <- empty list
for each email e:
    for each word w:
        if is_interesting_word(w, string_data_structure):
            add (filename, line_number, start_position, word) to results
  • El problema ahora es bastante adecuado para paralelizar con técnicas como MapReduce (p.ej, Hadoop). Cada correo electrónico se puede procesar independientemente de los demás y no es necesario compartir información: la estructura de datos de cadena se puede calcular antes de procesar los correos electrónicos. En el paso del mapa, extrae la información necesaria de un correo electrónico y en el paso de reducción, fusiona los valores calculados de cada correo electrónico en un solo archivo de salida.

Reduciría la cantidad de procesamiento que necesita: sin expresiones regulares, sin análisis avanzado; simplemente camine sobre cada carácter/línea en un correo electrónico y realice un seguimiento de dónde se encuentra (número de línea, posición, etc.). Como paso final, perfile su código y optimice donde duele :)

Respondido el 12 de junio de 12 a las 15:06

Zelenski de Stanford me presentó a "Trie" en su curso CS106B grabado, eso es lo que estaba insinuando cuando mencioné que consideraría usar un "árbol de letras". (CS106B, Stanford, Zelenski, año 2007, minuto 20 en adelante de la lección 25). También estoy familiarizado con MapReduce y pensé, como usted, que no habría ningún problema con desplegar ampliamente la paralelización de los datos. ¿Qué pasa con la microtarea real de averiguar si una palabra dada está en el conjunto "interesante"? ¿Hashing? Realmente me pregunto sobre esto... - robottinosino

La tarea de encontrar si una palabra es interesante se realiza con la estructura de datos de cadena. Una vez que haya construido la estructura de datos de cadena con todas las palabras interesantes, existen algoritmos conocidos para verificar si una palabra está en la estructura de datos. No codifica las palabras, implementa el algoritmo para esa estructura de datos en particular que devuelve verdadero o falso dependiendo de si la palabra está allí o no. - Simeón Visser

Pitón:

list = ['a', 'bunch', 'of', 'interesting', 'words']
linepos = 0

with open("file") as f:
    for line in f:
        linepos += 1
        wordpos = 0
        for word in line.split():
            wordpos += 1
            if word in list:
                print "%s found at line %s, word %s" % (word, linepos, wordpos)

Respondido el 11 de junio de 12 a las 20:06

Sospecho que esto sería más lento que una expresión regular compilada que filtra cada línea... Un par de comentarios: 1) ¿sus contadores podrían simplificarse usando "enumerar ()"? 2) pos debe ser "posición exacta del carácter" 3) split() es solo aproximado (es decir, tenga cuidado con la puntuación...) - robottinosino

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.