Индексирование .PDF, .XLS, .DOC, .PPT с использованием Lucene.NET - PullRequest
33 голосов
/ 05 февраля 2011

Я слышал о Lucene.Net и слышал о Apache Tika .Вопрос - как мне проиндексировать эти документы, используя C # против Java?Я думаю, что проблема в том, что не существует .Net-эквивалента Tika, который извлекает соответствующий текст из этих типов документов.

ОБНОВЛЕНИЕ - 05 февраля 2011

На основе данных ответов, кажется, что в настоящее время нативный .Net эквивалент Тики.Были упомянуты 2 интересных проекта, каждый из которых интересен сам по себе:

  1. Xapian Project (http://xapian.org/) - альтернатива Lucene, написанная в неуправляемом коде. Проектутверждает, что поддерживает "swig", который допускает привязки C #. В проекте Xapian есть встроенная поисковая система под названием Omega. Omega использует различные компоненты с открытым исходным кодом для извлечения текста из документов различных типов.
  2. IKVM.NET (http://www.ikvm.net/) - позволяет запускать Java из .Net. Пример использования IKVM для запуска Tika можно найти здесь .

Учитывая вышеупомянутые 2 проекта, я вижу несколько вариантов: чтобы извлечь текст, я мог бы либо: а) использовать те же компоненты, которые использует Omega, либо б) использовать IKVM для запуска Tika.Мне вариант b) кажется более чистым, поскольку существует только две зависимости.

Интересная часть заключается в том, что теперь есть несколько поисковых систем, которые, вероятно, можно использовать из .Net.Есть Xapian, Lucene.Net или даже Lucene (использующий IKVM).

ОБНОВЛЕНИЕ - 07 февраля 2011

Другой ответ пришел, порекомендовав мне проверить ifilters.Оказывается, это то, что MS использует для поиска Windows, поэтому фильтры Office легко доступны.Кроме того, есть некоторые PDF-фильтры.Недостатком является то, что они реализованы в неуправляемом коде, поэтому для их использования необходимо COM-взаимодействие.Я обнаружил приведенный ниже фрагмент кода в архиве DotLucene.NET (больше не является активным проектом):

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;

namespace IFilter
{
    [Flags]
    public enum IFILTER_INIT : uint
    {
        NONE = 0,
        CANON_PARAGRAPHS = 1,
        HARD_LINE_BREAKS = 2,
        CANON_HYPHENS = 4,
        CANON_SPACES = 8,
        APPLY_INDEX_ATTRIBUTES = 16,
        APPLY_CRAWL_ATTRIBUTES = 256,
        APPLY_OTHER_ATTRIBUTES = 32,
        INDEXING_ONLY = 64,
        SEARCH_LINKS = 128,
        FILTER_OWNED_VALUE_OK = 512
    }

    public enum CHUNK_BREAKTYPE
    {
        CHUNK_NO_BREAK = 0,
        CHUNK_EOW = 1,
        CHUNK_EOS = 2,
        CHUNK_EOP = 3,
        CHUNK_EOC = 4
    }

    [Flags]
    public enum CHUNKSTATE
    {
        CHUNK_TEXT = 0x1,
        CHUNK_VALUE = 0x2,
        CHUNK_FILTER_OWNED_VALUE = 0x4
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct PROPSPEC
    {
        public uint ulKind;
        public uint propid;
        public IntPtr lpwstr;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct FULLPROPSPEC
    {
        public Guid guidPropSet;
        public PROPSPEC psProperty;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct STAT_CHUNK
    {
        public uint idChunk;
        [MarshalAs(UnmanagedType.U4)] public CHUNK_BREAKTYPE breakType;
        [MarshalAs(UnmanagedType.U4)] public CHUNKSTATE flags;
        public uint locale;
        [MarshalAs(UnmanagedType.Struct)] public FULLPROPSPEC attribute;
        public uint idChunkSource;
        public uint cwcStartSource;
        public uint cwcLenSource;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct FILTERREGION
    {
        public uint idChunk;
        public uint cwcStart;
        public uint cwcExtent;
    }

    [ComImport]
    [Guid("89BCB740-6119-101A-BCB7-00DD010655AF")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IFilter
    {
        [PreserveSig]
        int Init([MarshalAs(UnmanagedType.U4)] IFILTER_INIT grfFlags, uint cAttributes, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=1)] FULLPROPSPEC[] aAttributes, ref uint pdwFlags);

        [PreserveSig]
        int GetChunk(out STAT_CHUNK pStat);

        [PreserveSig]
        int GetText(ref uint pcwcBuffer, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder buffer);

        void GetValue(ref UIntPtr ppPropValue);
        void BindRegion([MarshalAs(UnmanagedType.Struct)] FILTERREGION origPos, ref Guid riid, ref UIntPtr ppunk);
    }

    [ComImport]
    [Guid("f07f3920-7b8c-11cf-9be8-00aa004b9986")]
    public class CFilter
    {
    }

    public class IFilterConstants
    {
        public const uint PID_STG_DIRECTORY = 0x00000002;
        public const uint PID_STG_CLASSID = 0x00000003;
        public const uint PID_STG_STORAGETYPE = 0x00000004;
        public const uint PID_STG_VOLUME_ID = 0x00000005;
        public const uint PID_STG_PARENT_WORKID = 0x00000006;
        public const uint PID_STG_SECONDARYSTORE = 0x00000007;
        public const uint PID_STG_FILEINDEX = 0x00000008;
        public const uint PID_STG_LASTCHANGEUSN = 0x00000009;
        public const uint PID_STG_NAME = 0x0000000a;
        public const uint PID_STG_PATH = 0x0000000b;
        public const uint PID_STG_SIZE = 0x0000000c;
        public const uint PID_STG_ATTRIBUTES = 0x0000000d;
        public const uint PID_STG_WRITETIME = 0x0000000e;
        public const uint PID_STG_CREATETIME = 0x0000000f;
        public const uint PID_STG_ACCESSTIME = 0x00000010;
        public const uint PID_STG_CHANGETIME = 0x00000011;
        public const uint PID_STG_CONTENTS = 0x00000013;
        public const uint PID_STG_SHORTNAME = 0x00000014;
        public const int FILTER_E_END_OF_CHUNKS = (unchecked((int) 0x80041700));
        public const int FILTER_E_NO_MORE_TEXT = (unchecked((int) 0x80041701));
        public const int FILTER_E_NO_MORE_VALUES = (unchecked((int) 0x80041702));
        public const int FILTER_E_NO_TEXT = (unchecked((int) 0x80041705));
        public const int FILTER_E_NO_VALUES = (unchecked((int) 0x80041706));
        public const int FILTER_S_LAST_TEXT = (unchecked((int) 0x00041709));
    }

    /// 
    /// IFilter return codes
    /// 
    public enum IFilterReturnCodes : uint
    {
        /// 
        /// Success
        /// 
        S_OK = 0,
        /// 
        /// The function was denied access to the filter file. 
        /// 
        E_ACCESSDENIED = 0x80070005,
        /// 
        /// The function encountered an invalid handle, probably due to a low-memory situation. 
        /// 
        E_HANDLE = 0x80070006,
        /// 
        /// The function received an invalid parameter.
        /// 
        E_INVALIDARG = 0x80070057,
        /// 
        /// Out of memory
        /// 
        E_OUTOFMEMORY = 0x8007000E,
        /// 
        /// Not implemented
        /// 
        E_NOTIMPL = 0x80004001,
        /// 
        /// Unknown error
        /// 
        E_FAIL = 0x80000008,
        /// 
        /// File not filtered due to password protection
        /// 
        FILTER_E_PASSWORD = 0x8004170B,
        /// 
        /// The document format is not recognised by the filter
        /// 
        FILTER_E_UNKNOWNFORMAT = 0x8004170C,
        /// 
        /// No text in current chunk
        /// 
        FILTER_E_NO_TEXT = 0x80041705,
        /// 
        /// No more chunks of text available in object
        /// 
        FILTER_E_END_OF_CHUNKS = 0x80041700,
        /// 
        /// No more text available in chunk
        /// 
        FILTER_E_NO_MORE_TEXT = 0x80041701,
        /// 
        /// No more property values available in chunk
        /// 
        FILTER_E_NO_MORE_VALUES = 0x80041702,
        /// 
        /// Unable to access object
        /// 
        FILTER_E_ACCESS = 0x80041703,
        /// 
        /// Moniker doesn't cover entire region
        /// 
        FILTER_W_MONIKER_CLIPPED = 0x00041704,
        /// 
        /// Unable to bind IFilter for embedded object
        /// 
        FILTER_E_EMBEDDING_UNAVAILABLE = 0x80041707,
        /// 
        /// Unable to bind IFilter for linked object
        /// 
        FILTER_E_LINK_UNAVAILABLE = 0x80041708,
        /// 
        /// This is the last text in the current chunk
        /// 
        FILTER_S_LAST_TEXT = 0x00041709,
        /// 
        /// This is the last value in the current chunk
        /// 
        FILTER_S_LAST_VALUES = 0x0004170A
    }

    /// 
    /// Convenience class which provides static methods to extract text from files using installed IFilters
    /// 
    public class DefaultParser
    {
        public DefaultParser()
        {
        }

        [DllImport("query.dll", CharSet = CharSet.Unicode)]
        private extern static int LoadIFilter(string pwcsPath, [MarshalAs(UnmanagedType.IUnknown)] object pUnkOuter, ref IFilter ppIUnk);

        private static IFilter loadIFilter(string filename)
        {
            object outer = null;
            IFilter filter = null;

            // Try to load the corresponding IFilter
            int resultLoad = LoadIFilter(filename,  outer, ref filter);
            if (resultLoad != (int) IFilterReturnCodes.S_OK)
            {
                return null;
            }
            return filter;
        }

        public static bool IsParseable(string filename)
        {
            return loadIFilter(filename) != null;
        }

        public static string Extract(string path)
        {
            StringBuilder sb = new StringBuilder();
            IFilter filter = null;

            try
            {
                filter = loadIFilter(path);

                if (filter == null)
                    return String.Empty;

                uint i = 0;
                STAT_CHUNK ps = new STAT_CHUNK();

                IFILTER_INIT iflags =
                    IFILTER_INIT.CANON_HYPHENS |
                    IFILTER_INIT.CANON_PARAGRAPHS |
                    IFILTER_INIT.CANON_SPACES |
                    IFILTER_INIT.APPLY_CRAWL_ATTRIBUTES |
                    IFILTER_INIT.APPLY_INDEX_ATTRIBUTES |
                    IFILTER_INIT.APPLY_OTHER_ATTRIBUTES |
                    IFILTER_INIT.HARD_LINE_BREAKS |
                    IFILTER_INIT.SEARCH_LINKS |
                    IFILTER_INIT.FILTER_OWNED_VALUE_OK;

                if (filter.Init(iflags, 0, null, ref i) != (int) IFilterReturnCodes.S_OK)
                    throw new Exception("Problem initializing an IFilter for:\n" + path + " \n\n");

                while (filter.GetChunk(out ps) == (int) (IFilterReturnCodes.S_OK))
                {
                    if (ps.flags == CHUNKSTATE.CHUNK_TEXT)
                    {
                        IFilterReturnCodes scode = 0;
                        while (scode == IFilterReturnCodes.S_OK || scode == IFilterReturnCodes.FILTER_S_LAST_TEXT)
                        {
                            uint pcwcBuffer = 65536;
                            System.Text.StringBuilder sbBuffer = new System.Text.StringBuilder((int)pcwcBuffer);

                            scode = (IFilterReturnCodes) filter.GetText(ref pcwcBuffer, sbBuffer);

                            if (pcwcBuffer > 0 && sbBuffer.Length > 0)
                            {
                                if (sbBuffer.Length < pcwcBuffer) // Should never happen, but it happens !
                                    pcwcBuffer = (uint)sbBuffer.Length;

                                sb.Append(sbBuffer.ToString(0, (int) pcwcBuffer));
                                sb.Append(" "); // "\r\n"
                            }

                        }
                    }

                }
            }
            finally
            {
                if (filter != null) {
                    Marshal.ReleaseComObject (filter);
                    System.GC.Collect();
                    System.GC.WaitForPendingFinalizers();
                }
            }

            return sb.ToString();
        }
    }
}

На данный момент это кажется наилучшим способом извлечения текста из документов с использованием платформы .NET насервер Windows.Спасибо всем за вашу помощь.

ОБНОВЛЕНИЕ - 08 марта 2011

Хотя я все еще думаю, что ifilters - хороший способ пойти, я думаю, если вы ищете индексДля документов, использующих Lucene из .NET, очень хорошей альтернативой будет использование Solr .Когда я впервые начал исследовать эту тему, я никогда не слышал о Solr.Итак, для тех из вас, у кого этого нет, Solr - это отдельная поисковая служба, написанная на Java поверх Lucene.Идея состоит в том, что вы можете запустить Solr на компьютере с брандмауэром и обмениваться данными с ним через HTTP из вашего приложения .NET.Solr действительно написан как сервис и может делать все, что может делать Lucene (включая использование Tika для извлечения текста из .PDF, .XLS, .DOC, .PPT и т. Д.), А затем и некоторых других.У Solr, похоже, тоже очень активное сообщество, и в этом я не уверен, что касается Lucene.NET.

Ответы [ 4 ]

6 голосов
/ 05 февраля 2011

Вы также можете проверить ifilters - есть ряд ресурсов, если вы выполните поиск asp.net ifilters:

Конечно, есть дополнительные хлопоты, если вы распространяете это на клиентские системы, потому что вам нужно будет либо включить ifiltersс вашим дистрибутивом и установите те, у кого ваше приложение, на свои компьютеры, или у них не будет возможности извлекать текст из любых файлов, для которых у них нет ifilters.

4 голосов
/ 05 февраля 2011

Это одна из причин, почему я был недоволен Lucene для проекта, над которым я работал. Xapian является конкурирующим продуктом, и в некоторых случаях он на несколько порядков быстрее, чем Lucene, и обладает другими неотразимыми характеристиками (ну, в то время они были мне интересны).Большая проблема?Он написан на C ++, и вы должны взаимодействовать с ним.Это для индексации и поиска.Для фактического разбора текста, вот где Lucene действительно падает - вы должны сделать это самостоятельно.Xapian имеет омега-компонент, который управляет вызовом сторонних компонентов для извлечения данных.В моем ограниченном тестировании это работало довольно чертовски хорошо.Я не закончил проект (больше, чем POC), но я записал мой опыт компиляции для 64 бит.Конечно, это было почти год назад, поэтому все могло измениться.

Если вы загляните в документацию Omega , вы увидите инструменты, которые они используют для анализа документов.

PDF (.pdf), если pdftotext доступен (поставляется с xpdf)

PostScript (.ps, .eps, .ai), если ps2pdf (из ghostscript) и pdftotext (поставляется с xpdf)доступно

Документы OpenOffice / StarOffice (.sxc, .stc, .sxd, .std, .sxi, .sti, .sxm, .sxw, .sxg, .stw), если доступна распаковка

Документы формата OpenDocument (.odt, .ods, .odp, .odg, .odc, .odf, .odb, .odi, .odm, .ott, .ots, .otp, .otg, .otc, .otf, .oti, .oth) если доступно распаковывание

документы MS Word (.doc, .dot), если доступно антислово

документы MS Excel (.xls, .xlb, .xlt)если xls2csv доступен (поставляется с catdoc)

документы MS Powerpoint (.ppt, .pps), если catppt доступен, (поставляется с catdoc)

документы MS Office 2007 (.docx,.dotx, .xlsx, .xlst, .pptx,.potx, .ppsx) если доступно распаковывание

документы Wordperfect (.wpd), если доступен wpd2text (поставляется с libwpd)

документы MS Works (.wps, .wpt), если доступен wps2text(поставляется с libwps)

Сжатые документы AbiWord (.zabw), если доступен gzip

Документы в расширенном текстовом формате (.rtf), если доступно unrtf

Документация Perl POD (.pl, .pm, .pod), если доступен pod2text

Файлы TeX DVI (.dvi), если catdvi доступен

Файлы DjVu (.djv, .djvu), если доступен djvutxt

XPS-файлы (.xps), если распаковка доступна

3 голосов
/ 05 февраля 2011

Очевидно, вы можете использовать Tika из .net ( ссылка )

Я сам не пробовал.

2 голосов
/ 08 марта 2011

Другим аспектом здесь является то, что индексы Lucene двоично совместимы между Java и .NET.Таким образом, вы можете написать индекс с помощью Tika и прочитать его с помощью C #.

...