From b380b0fcd7517d4e1b56a1991e37cf21e74594da Mon Sep 17 00:00:00 2001 From: helmutm Date: Thu, 1 Mar 2007 11:40:35 +0000 Subject: [PATCH] clean-up of cybertools.text; provide msword conversion git-svn-id: svn://svn.cy55.de/Zope3/src/cybertools/trunk@1603 fd906abe-77d9-0310-91a1-e0d9ade77398 --- text/README.txt | 48 +++++- text/base.py | 50 +++--- text/config/wvText.xml | 355 ++++++++++++++++++++++++++++++++++++++++ text/doc.py | 54 ++++++ text/interfaces.py | 25 ++- text/pdf.py | 41 +++-- text/testfiles/mary.doc | Bin 0 -> 62976 bytes text/testfiles/mary.odt | Bin 0 -> 8935 bytes 8 files changed, 529 insertions(+), 44 deletions(-) create mode 100755 text/config/wvText.xml create mode 100644 text/doc.py create mode 100644 text/testfiles/mary.doc create mode 100644 text/testfiles/mary.odt diff --git a/text/README.txt b/text/README.txt index 2f40c73..00cee9e 100644 --- a/text/README.txt +++ b/text/README.txt @@ -1,18 +1,56 @@ ================================================= -Text transformations, e.g. for full-text indexing +Text Transformations, e.g. for Full-text Indexing ================================================= ($Id$) +If a converter program needed is not available we want to put a warning +into Zope's server log; in order to be able to test this we register +a log handler for testing: + + >>> from zope.testing.loggingsupport import InstalledHandler + >>> log = InstalledHandler('zope.server') + +The test files are in a subdirectory of the text package: + >>> import os >>> from cybertools import text - >>> directory = os.path.dirname(text.__file__) - >>> fn = os.path.sep.join((directory, 'testfiles', 'mary.pdf')) - >>> f = open(fn) + >>> testdir = os.path.join(os.path.dirname(text.__file__), 'testfiles') + +PDF Files +--------- + +Let's start with a PDF file: >>> from cybertools.text.pdf import PdfTransform >>> transform = PdfTransform(None) - >>> words = transform(f).split() + >>> f = open(os.path.join(testdir, 'mary.pdf')) + +This will be transformed to plain text: + + >>> result = transform(f) + +Let's check the log, should be empty: + + >>> print log + +So what is in the plain text result? + + >>> words = result.split() + >>> len(words) + 89 + >>> u'lamb' in words + True + +Word Documents +-------------- + + >>> from cybertools.text.doc import DocTransform + >>> transform = DocTransform(None) + >>> f = open(os.path.join(testdir, 'mary.doc')) + >>> result = transform(f) + >>> print log + >>> words = result.split() >>> len(words) 89 >>> u'lamb' in words diff --git a/text/base.py b/text/base.py index e384424..74057c3 100644 --- a/text/base.py +++ b/text/base.py @@ -19,32 +19,17 @@ """ Base classes for text transformations. -Based on code provided by zc.index. +Based on code provided by zc.index and TextIndexNG3. $Id$ """ -__docformat__ = "reStructuredText" - import os, shutil, sys, tempfile +import logging from zope.interface import implements from cybertools.text.interfaces import ITextTransform, IFileTransform -def haveProgram(name): - """Return true if the program `name` is available.""" - if sys.platform.lower().startswith("win"): - extensions = (".com", ".exe", ".bat") - else: - extensions = ("",) - execpath = os.environ.get("PATH", "").split(os.path.pathsep) - for path in execpath: - for ext in extensions: - fn = os.path.join(path, name + ext) - if os.path.isfile(fn): - return True - return False - class BaseTransform(object): @@ -54,11 +39,9 @@ class BaseTransform(object): self.context = context self.text = None - def __call__(self, f): + def __call__(self, fr): if self.text is None: - fr = open(f, 'r') self.text = fr.read() - fr.close() return self.text @@ -66,22 +49,45 @@ class BaseFileTransform(BaseTransform): implements(IFileTransform) + extension = '.txt' + def __call__(self, fr): if self.text is None: - #fr = f.open("rb") dirname = tempfile.mkdtemp() filename = os.path.join(dirname, "temp" + self.extension) try: fw = open(filename, "wb") shutil.copyfileobj(fr, fw) - #fr.close() fw.close() text = self.extract(dirname, filename) finally: shutil.rmtree(dirname) + #fr.close() self.text = text return self.text def extract(self, dirname, filename): raise ValueError('Method extract() has to be implemented by subclass.') + def execute(self, com): + try: + import win32pipe + result = win32pipe.popen(com).read() + except ImportError: + result = os.popen(com).read() + return result + + def checkAvailable(self, name, logMessage=''): + if sys.platform.lower().startswith("win"): + extensions = (".com", ".exe", ".bat") + else: + extensions = ("",) + execpath = os.environ.get("PATH", "").split(os.path.pathsep) + for path in execpath: + for ext in extensions: + fn = os.path.join(path, name + ext) + if os.path.isfile(fn): + return True + if logMessage: + logging.getLogger('zope.server').warn(logMessage) + return False diff --git a/text/config/wvText.xml b/text/config/wvText.xml new file mode 100755 index 0000000..e05d9fb --- /dev/null +++ b/text/config/wvText.xml @@ -0,0 +1,355 @@ +
+ +ABW + + + + + + + + + +
+ + + + +
+ + + + +
+ + +
+ + +type="1" +type="I" +type="i" +type="A" +type="a" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +0 +0 +0 +0 +0 + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/text/doc.py b/text/doc.py new file mode 100644 index 0000000..3850246 --- /dev/null +++ b/text/doc.py @@ -0,0 +1,54 @@ +# +# Copyright (c) 2007 Helmut Merz helmutm@cy55.de +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +""" +Searchable text support for Portable Document Format (PDF) files. + +This uses the pdftotext command from xpdf to perform the extraction. +interface definitions for text transformations. + +Based on code provided by zc.index and TextIndexNG3. + +$Id$ +""" + +import os, sys + +from cybertools.text import base + +try: + from Globals import package_home + wvConf = os.path.join(package_home(globals()), 'config', 'wvText.xml') +except ImportError: + wvConf = os.path.join(os.path.dirname(__file__), 'config', 'wvText.xml') + + +class DocTransform(base.BaseFileTransform): + + extension = ".doc" + + def extract(self, directory, filename): + if not self.checkAvailable('wvWare', 'wvWare is not available'): + return u'' + if sys.platform == 'win32': + data = self.execute('wvWare -c utf-8 --nographics -x "%s" "%s" 2> nul:' + % (wvConf, filename)) + else: + data = self.execute('wvWare -c utf-8 --nographics -x "%s" "%s" 2> /dev/null' + % (wvConf, filename)) + return data.decode('UTF-8') diff --git a/text/interfaces.py b/text/interfaces.py index d795868..c74d6ff 100644 --- a/text/interfaces.py +++ b/text/interfaces.py @@ -27,17 +27,32 @@ from zope.interface import Interface class ITextTransform(Interface): - def __call__(f): - """ Transform the content of file f to plain text and return - the result as unicode. + def __call__(fr): + """ Transform the content of file fr (readfile) to plain text and + return the result as unicode. """ class IFileTransform(ITextTransform): - """ A transformation that uses an intermediate disk file. + """ A transformation that is performed by calling some external program + and that typically uses an intermediate disk file. """ def extract(dirname, filename): - """ Extract text contents from the file specified by dirnam, filename, + """ Extract text contents from the file specified by ``filename``, using some external programm, and return the result as unicode. + ``dirname`` is the path to a temporary directory that + usually (but not necessarily) contains the file and may + be used for creating other (temporary) files if needed. + """ + + def execute(command): + """ Execute a system command and return the output of the program + called. + """ + + def checkAvailable(progname, logMessage=''): + """ Check the availability of the program named ``progname``. + Return True if available; if ``logMessage`` is given, put this + as a warning message into the log if the program is not available. """ diff --git a/text/pdf.py b/text/pdf.py index f687219..55b30cd 100644 --- a/text/pdf.py +++ b/text/pdf.py @@ -1,9 +1,31 @@ -"""Searchable text support for Portable Document Format (PDF) files. - -This uses the pdftotext command from xpdf to perform the extraction. +# +# Copyright (c) 2007 Helmut Merz helmutm@cy55.de +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# """ -__docformat__ = "reStructuredText" +Searchable text support for Portable Document Format (PDF) files. + +This uses the pdftotext command from xpdf to perform the extraction. +interface definitions for text transformations. + +Based on code provided by zc.index and TextIndexNG3. + +$Id$ +""" import os, sys @@ -15,12 +37,7 @@ class PdfTransform(base.BaseFileTransform): extension = ".pdf" def extract(self, directory, filename): - if not base.haveProgram("pdftotext"): - print 'Warning: pdftotext is not available' + if not self.checkAvailable('pdftotext', 'pdftotext is not available'): return u'' - txtfile = os.path.join(directory, "words.txt") - st = os.system("pdftotext -enc UTF-8 %s %s" % (filename, txtfile)) - f = open(txtfile, "rb") - data = f.read() - f.close() - return unicode(data, "utf-8") + data = self.execute('pdftotext -enc UTF-8 "%s" -' % filename) + return data.decode('UTF-8') diff --git a/text/testfiles/mary.doc b/text/testfiles/mary.doc new file mode 100644 index 0000000000000000000000000000000000000000..847a6de7350b4ea5cfd1deb4aa84e6992c5cf43b GIT binary patch literal 62976 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 Br2PN@ literal 0 HcmV?d00001