PDA

Ver la Versión Completa : Búsqueda de contenido en documentos Word en Linux


elaguna
06-09-2012, 22:54:15
Qué tal foro.

Antes que nada les agradezco a todos el tiempo que se dan para compartir conocimientos con los demás, en más de una ocasión me han sacado de apuros, considerando que soy un novato.

Pues así como dice el título. Les comento:

Tengo un sistema de gestión de documentos alojado en un servidor Linux; la base de datos es postgres, el programa está hecho en Delphi (en qué otra cosa?! :D) y en un directorio del mismo servidor se almacenan los documentos de Word principalmente.

El programa funciona bien; registran un documento de Word, le ponen un título, el sistema asigna un nombre de archivo que se almacena y posteriormente lo recuperan a modo de plantilla; también pueden hacer búsquedas de archivos y que se localizan por medio del título que hubieran escrito, todos éstos datos están en postgres; hasta ahí todo bien pero...

Al cliente se le ocurrió que quiere hacer búsquedas por palabras en el contenido de los documentos de Word y posteriormente listar los archivos para que puedan abrirlos y trabajar con ellos.

Después de mucho darle vueltas he logrado realizar la dichosa búsqueda, se tarda uno o dos minutos más que en el Explorer de Windows, pero no siendo yo Microsoft, es tolerable.

La cuestión es que para hacer esa búsqueda abro cada uno de los archivos de Word (de uno en uno) y realizo la búsqueda, mi pregunta es: ya que los archivos están en el servidor al momento de abrirlos y cerrarlos desde los equipos cliente, seguramente me va a generar bastante tráfico.

Había pensado en hacer ese mismo procedimiento de búsqueda desde dentro del servidor y únicamente enviar los resultados al cliente pero no tengo idea de como hacerlo en Linux.

Alguien tiene alguna sugerencia de cómo se podría solucionar esto?

De antemano, muchas gracias a todos.

Eduardo Laguna.

Casimiro Notevi
06-09-2012, 23:07:20
Con grep puedes buscar un texto dentro de ficheros y la salida puedes enviarla a un fichero.log (por ejemplo).
Aunque puede que te convenga más guardar el documento dentro de la base de datos y hacer la búsqueda ahí. Si no son muchos millones de documentos ;)

roman
06-09-2012, 23:14:25
Pero grep es para archivos de texto ¿no? En un documento de word, no sé si podrá buscar.

// Saludos

Casimiro Notevi
06-09-2012, 23:32:37
Pero grep es para archivos de texto ¿no? En un documento de word, no sé si podrá buscar.
Cierto, aunque si no está "cifrado" el texto (no lo sé) podría buscar palabras.
Creo que la mejor opción es crear un sistema de etiquetas/tags/palabras claves... que se guarden en un campo junto al registro, pero ahí se dependería de que el usuario que crea el registro lo haga correctamente y ponga las etiquetas adecuadas para luego encontrarlo.

movorack
06-09-2012, 23:34:03
Hola a todos.

Creo que Grep tratará el documento cual fuese un documento de texto cualquiera. así que en teoria si podrá encontrar una cadena dentro del documento.

elaguna
06-09-2012, 23:56:37
En principio había contemplado el guardar los documentos dentro de la base de datos, pero como son documentos con bastantes "cosillas" en su formato pues mejor los dejamos por fuera y en la base sólo guardo la ruta del archivo.

Por otra parte ya había intentado con catdoc y grep desde terminal pero no funciona con .docx

D-MO
07-09-2012, 00:20:57
En principio, recordemos que un archivo .doc/.docx/.odt, etc... no es mas que un paquete con mas archivos en su interior que definen el contenido y estructura del documento, entonces, si podemos desempaquetar ese archivo y manipular su contenido seremos los dioses del universo ...ah no, creo que ya me pasé.:D

Así que, como el .doc y .docx son formatos cerrados, porqué no convertirlos a un .odt (automatizado, claro) o similar y buscar el contenido allí. Revisando el código de loook (http://www.danielnaber.de/loook/) podemos ver esto:

def processFile(self, filename, query):
suffix = self.getSuffix(filename)
try:
# Handle OpenOffice.org files:
if suffix in ('sxw', 'stw', # OOo 1.x swriter
'sxc', 'stc', # OOo 1.x scalc
'sxi', 'sti' # OOo 1.x simpress
'sxg', # OOo 1.x master document
'sxm', # OOo 1.x formula
'sxd', 'std', # OOo 1.x sdraw
'odt', 'ott', # OOo 2.x swriter
'odp', 'otp', # OOo 2.x simpress
'odf', # OOo 2.x formula
'odg', 'otg', # OOo 2.x sdraw
'ods', 'ots' # OOo 2.x scalc
):
zip = zipfile.ZipFile(filename, "r")
content = ""
docinfo = ""
try:
# TODO: are all OOo files utf-8?
content = unicode(zip.read("content.xml"), 'utf-8')
# TODO: is replace_with_space=0 correct?
content = self.removeXMLMarkup(content, replace_with_space=0)
docinfo = unicode(zip.read("meta.xml"), 'utf-8')
docinfo = self.removeXMLMarkup(docinfo, replace_with_space=0)
self.ooo_count = self.ooo_count + 1
except KeyError, err:
print "Warning: %s not found in '%s'" % (err, filename)
return None
title = ""
title_match = re.compile("<dc:title>(.*?)</dc:title>", re.DOTALL|re.IGNORECASE).search(docinfo)
if title_match:
title = title_match.group(1)
if self.match(query, "%s %s" % (content.lower(), docinfo.lower())):
return (filename, title)

except zipfile.BadZipfile, err:
print "Warning: Supposed ZIP file '%s' could not be opened: %s" % (filename, err)
except IOError, err:
print "Warning: File '%s' could not be opened: %s" % (filename, err)
return None

Este código está en python y hace lo que se requiere, extrae el contenido y busca si el patrón de búsqueda se encuentra en el título, contenido o metadata del documento.

No te digo que hagas esto con cada búsqueda, sino que, se me ocurre que al momento de que se carga un nuevo documento al sistema se extraiga el contenido de este y se almacene en una bd o en otro lugar donde se nos simplifique la búsqueda.

Saludos

Casimiro Notevi
07-09-2012, 00:46:25
Interesante ese loook :)

D-MO
07-09-2012, 03:54:33
Interesante ese loook :)
cough.. cough.. ¿dije que está hecho en python?:p

Casimiro Notevi
07-09-2012, 09:45:35
ya, ya... :)
Si tengo muchas pequeñas utilidades de línea de comando que hacen cosas fantásticas fácilmente y están hechas en python :)

mamcx
07-09-2012, 17:03:52
Qué tal foro.
La cuestión es que para hacer esa búsqueda abro cada uno de los archivos de Word (de uno en uno) y realizo la búsqueda, mi pregunta es: ya que los archivos están en el servidor al momento de abrirlos y cerrarlos desde los equipos cliente, seguramente me va a generar bastante tráfico.


Este paso se puede mejorar considerablemente.

Primero, si ya tienes resuelto como extraer el texto de los documentos de word, que por mucho es el paso MAS dificil, entonces lo puedes colocar en un campo TEXT de la BD de postgres.

Una vez alli, montas una busqueda de texto completo:

http://www.postgresql.org/docs/9.1/static/textsearch.html

Y obtendras resultados es milisegundos (con los indices adecuados, que te lo explica la documentacion) y busquedas tipo google.

El resto es mantener actualizado el cache del contenido que metes en la BD (recuerda, es el texto de word, NO el archivo). Para eso, puedes usar una comparación de timestamp + tamaño de archivo y/o MD5 de estos + un detector de que el archivo ha cambiado.

elaguna
11-09-2012, 19:27:58
Este paso se puede mejorar considerablemente.

Primero, si ya tienes resuelto como extraer el texto de los documentos de word, que por mucho es el paso MAS dificil, entonces lo puedes colocar en un campo TEXT de la BD de postgres.

Una vez alli, montas una busqueda de texto completo:

http://www.postgresql.org/docs/9.1/static/textsearch.html

Y obtendras resultados es milisegundos (con los indices adecuados, que te lo explica la documentacion) y busquedas tipo google.

El resto es mantener actualizado el cache del contenido que metes en la BD (recuerda, es el texto de word, NO el archivo). Para eso, puedes usar una comparación de timestamp + tamaño de archivo y/o MD5 de estos + un detector de que el archivo ha cambiado.


Interesante y práctico, lo voy a probar y les comento. Lo único aquí sería la cantidad de texto que entraría en el campo text, pero veremos qué pasa.

Con la sugerencia del compañero D-MO, está muy interesante el proceso, sólo que los equipos clientes son todo Microsoft y los usuarios no tienen tiempo (ni ganas!!) de aprender como cambiar el formato de su archivo de .odt a .docx, aparte de que algunas cosas tales como texto resaltado, comentarios y otras cosas se manejan distinto de un formato a otro. Pero para documentos más sencillos funciona perfectamente.

Cuando hago la búsqueda directa en .docx se tarda unos minutos, pero al hacer la conversión, aún cuando se está realizando en el servidor Linux se tarda un poco más. Pero muy interesante el método.

Gracias a todos.

D-MO
11-09-2012, 20:31:24
...sólo que los equipos clientes son todo Microsoft y los usuarios no tienen tiempo (ni ganas!!) de aprender como cambiar el formato de su archivo...
Si lees bien mi mensaje anterior verás que indiqué que el proceso de extracción debería ser automatizado. Veámos cual sería la secuencia (siguiendo la recomendación de mamcx).

El usuario carga el archivo al sistema
El sistema extrae extrae la metadata y texto del archivo (basándonos en como loook hace la búsqueda)
Se almacena esta info en la BD, junto con la referencia al archivo original (¿path?)

Y al hacer búsquedas

EL usuario ingresa un término de búsqueda
Se hace la búsqueda en la bd (texto plano del doc)
Al encontrar un match, se devuelve el archivo original (.docx)

De esta forma, en la bd tendrías un texto plano que simplificaría las búsquedas y al usuario de devuelves el archivo original.

Saludos

elaguna
14-09-2012, 23:19:07
Muchas gracias a todos los que se tomaron el tiempo de leer, pero principalmente gracias a los que aportaron ideas.

Entre que ya había logrado sacar la información de los Word y con las sugerencias de mamcx y de D-MO se solucionó el problema, además de otra solución colateral.

Del modo en que en un principio lo estaba haciendo, mantenía oculto Word y realizaba las búsquedas en cada uno de los archivos, pero si mientras se ejecutaban las búsquedas, abrías Word entonces se quedaba abierto permanentemente o se cerraba (según la parte de código que se estuviera ejecutando) y se mostraba todo el proceso, pero no dejaba trabajar en ningún otro archivo (algo faltó por ahí al instanciar :o ).

Con la solución final, como todo se hace en el servidor, es mucho más rápido y no "molestamos" a Microsoft con nada.

Gracias a todos. Son geniales!!!

Eduardo Laguna

D-MO
15-09-2012, 02:37:35
Me alegra saber que sirvió de algo las aportaciones que se hicieron. Sería bueno que detallaras un poco mas de como quedó el proceso final para que nos sirva de retroalimentación.

Saludos

elaguna
15-09-2012, 02:42:54
Me alegra saber que sirvió de algo las aportaciones que se hicieron. Sería bueno que detallaras un poco mas de como quedó el proceso final para que nos sirva de retroalimentación.

Claro que sí, en este momento todavía estoy modificando este "busca lo que te digo", pero en un momento más lo subo y gracias de nuevo.

Eduardo Laguna

D-MO
15-09-2012, 04:16:41
No te apures, que no hay prisa ;)

elaguna
19-09-2012, 08:39:11
Pues como quedamos, esta es la solución que implemente para las búsquedas.

1. Modifiqué mi tabla en la DB (Postgresql) para almacenar al contenido de cada documento. Mi tabla se llama "planti_corr", agregué un campo de texto e indexé la tabla por ese campo siguiendo lo indicado por mamcx y en la documentación http://www.postgresql.org/docs/9.1/static/textsearch.html

CREATE INDEX pgcontenido_idx
ON planti_corr
USING gin
(to_tsvector('spanish'::regconfig, contenido));


2. Ahora que extraer el contenido de cada documento que se vaya a dar de alta, esto lo hice en el botón de "Altas" en el formulario de registro de documentos:

Crear una instancia del Portapapeles de Windows
Crear una instancia de Word y abrir el documento
Seleccionar todo el contenido del documento de Word y copiarlo en el Portapapeles de Windows
Cerrar Word
Poner el contenido del Portapapeles en una variable
El límite de tsvector en Postgresql es de 1048575 bytes por lo que limito el tamaño del contenido de la variable a 1040000 con la función LeftBStr()
Envío todo a Postgres en un Query
var
word : Variant;
plantiOrig : TClipBoard;
wnContenido : WideString;
wnGuarda : oleVariant;

begin
...
...
Word := CreateOleObject('Word.Application');
Word.Visible := False;

try
Word.Documents.Open(<Nombre_del_archivo.docx>, EmptyParam, EmptyParam, EmptyParam,
EmptyParam, EmptyParam, EmptyParam, EmptyParam,
EmptyParam, EmptyParam);

Word.Selection.WholeStory; // Seleccionar todo el contenido del documento de Word
Word.Selection.Copy; // Copiar al portapapeles
Word.ActiveDocument.Close(wnGuarda, EmptyParam, EmptyParam);
Word.Quit;

wnContenido := LeftBStr(plantiOrig.AsText, 1040000); // Extrae únicamente 1040000 bytes
plantiOrig.Clear;

Query1.Active := False;
Query1.SQL.Clear;
Query1.SQL.Add('insert into planti_corr (nombre, archivo, contenido) values (:vNombre, :vArchivo, :vContenido)');

Query1.ParamByName('vNombre').AsString := <Título del archivo, solo de referencia>;
Query1.ParamByName('vArchivo').AsString := <Aquí va la ruta y nombre del archivo guardado>
Query1.ParamByName('vContenido').AsWideString := wnContenido;
Query1.ExecSQL;
Except
Application.MessageBox('Error al cargar el archivo en la base de datos', 'Buscador', MB_OK or MB_ICONERROR);
End;
...
...
end;


3. Ahora la búsqueda. En otro botón:

Tomo el texto con las palabras que desean buscar, éstas deben quedar separadas únicamente por el símbolo '&' (primerapalabra&segundapalabra&tercerapalabra); (no espacios, no comas, no puntos, no... nada) y asigno la nueva cadena a una variable, para esto utilizo la función ReplaceStr()
Ejecuto la consulta esta debe ser con la función "tsquery" de Postgres en el campo indexado, (que el servidor haga todo!!!^\||/)
... y hago muy feliz a mi usuario :D!!!
var
w_palabras : String;
wpTotalReg : Integer;



begin
...
...


w_palabras := ReplaceStr(edtFrases.Text,' ', '&');


Query1.Close;
Query1.SQL.Clear;
Query1.SQL.Add('SELECT id_planti, nombre, archivo FROM planti_corr WHERE contenido @@ to_tsquery(:wqPalabras)');
Query1.ParamByName('wqPalabras').AsString := w_Palabras;
Query1.Open;

wpTotalReg := Query1.RecordCount;
...
...
end;




Estas son las instrucciones básicas, obviamente hay cosas antes y después, pero concretando esta es la forma en que lo solucioné y efectivamente el tiempo de la búsqueda se redujo de algunos minutos a pocos segundos, bastante bien diría yo!!!.


Muchas gracias a todos, especialmente a mamcx y a D-MO por sus ideas (prácticamente la solución)


Saludos.


Eduardo Laguna

roman
19-09-2012, 15:36:50
Muchas felicidades y muchas gracias por la solución :)

Este hilo me parece que puede ser de ayuda para mucha gente.

// Saludos

D-MO
19-09-2012, 16:53:27
...especialmente a mamcx y a D-MO por sus ideas (prácticamente la solución)...
Pues no, no tienes nada que agradecer ;)

A ti te agradecemos que hayas compartido la solución.

Saludos.

elaguna
19-09-2012, 19:51:51
Ups!

Sólo falto agregar la línea donde se crea el objeto para leer el contenido del portapapeles antes de abrir el documento de Word


var
...
...
plantiOrig : TClipBoard;
...
...

begin
...
...
plantiOrig := TClipBoard.Create;
...
...

roman
19-09-2012, 19:55:51
Por cierto, ¿por qué creas un objeto TClipboard en lugar de usar el que por defecto viene en la unidad clipbrd?

// Saludos

elaguna
19-09-2012, 21:09:39
Por cierto, ¿por qué creas un objeto TClipboard en lugar de usar el que por defecto viene en la unidad clipbrd?

// Saludos

Tienes razón!!!. ^\||/

Todo se debe a la inercia de:

"Si lo tomas..., regrésalo;
si lo usas..., límpialo;
si lo asignas..., decláralo!!! "

Pero es cierto, se puede utilizar directamente "ClipBoard"

Gracias, por el comentario.

Eduardo Laguna