如何从头搭建一个搜索引擎_pylucene,分词,语言编码问题

时间:2021-07-18 03:13:43
日期:2016年10月13日
标题:pylucene,分词,语言编码问题
编号:3

一.pylucene
  • pylucene以及前置包的安装
    • 安装Java(JDK)
      • sudo apt-get install default-jdk
      • 输入javac以测试
    • 安装python-dev
      • sudo apt-get install python-dev
    •  安装ant
      • sudo apt-get install ant
    •  安装jcc(首先检查是否已经安装g++和gcc)
      •  法1:sudo easy_install jcc (//不靠谱)
      •  法2:
 svn co http://svn.apache.org/repos/asf/lucene/pylucene/trunk/jcc jcc


(转换到jcc源码目录)
/*修改jcc目录下的setup.py文件,把JDK这个变量 对应的值改成你系统上的值。
原来默认的是这样的:
JDK = {
    'darwin': '/System/Library/Frameworks/JavaVM.framework/Versions/Current',
    'ipod': '/usr/include/gcc',
    'linux2': '/usr/lib/jvm/java-6-openjdk',
    'sunos5': '/usr/jdk/instances/jdk1.6.0',
    'win32': 'o:/Java/jdk1.6.0_02',
}*/


 python setup.py build
 sudo python setup.py install
    • 安装pylucene:
      • 首先到官网下包
      • 接着解压
- pushd jcc
- <edit setup.py to match your environment>
- python setup.py build
- sudo python setup.py install
- popd
- <edit Makefile to match your environment>
- make
- make test (look for failures)
- sudo make install
    • 或者在anaconda平台下使用:
- 首先在你的虚拟机上安装anaconda环境,下载链接:https://www.continuum.io/downloads#linux (记得下载py27的)
- Cd到下载目录,在terminal中输入:
- bash Anaconda2-4.2.0-Linux-x86_64.sh
来安装anaconda环境
安装成功后使用which python命令查看python的路径是否已经指向你的anaconda的python,如果没有的话就手动加入
- 之后,在terminal里面输入:
conda install -c kalefranz pylucene=4.9.0
就会看到anaconda会自动帮你把lucene与其前置库(包括jdk的静态库以及jcc)全部安装,完全不需要再去安装及配置java和jcc啦!
- 但是这个时候你如果在python里面import jcc会报缺少lib的错误,因此我们需要在terminal里面制定lib的路径:
export PREFIX=/你/的/anaconda/的/路/径/
export LD_LIBRARY_PATH=$PREFIX/lib:$PREFIX/jre/lib:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH=$PREFIX/jre/lib/amd64:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH=$PREFIX/jre/lib/amd64/server:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH=$PREFIX/lib/python2.7/site-packages:$LD_LIBRARY_PATH
#!/usr/bin/env python
#IndexFiles.py

INDEX_DIR = "IndexFiles.index"

import sys, os, lucene, threading, time
from datetime import datetime

#4版本的pylucene必须这样导入
from java.io import File
from org.apache.lucene.analysis.miscellaneous import LimitTokenCountAnalyzer
from org.apache.lucene.analysis.standard import StandardAnalyzer
from org.apache.lucene.document import Document, Field, FieldType
from org.apache.lucene.index import FieldInfo, IndexWriter, IndexWriterConfig
from org.apache.lucene.store import SimpleFSDirectory
from org.apache.lucene.analysis.core import SimpleAnalyzer
from org.apache.lucene.util import Version

"""
This class is loosely based on the Lucene (java implementation) demo class
org.apache.lucene.demo.IndexFiles.  It will take a directory as an argument
and will index all of the files in that directory and downward recursively.
It will index on the file path, the file name and the file contents.  The
resulting Lucene index will be placed in the current directory and called
'index'.
"""

class Ticker(object):

    def __init__(self):
        self.tick = True

    def run(self):
        while self.tick:
            sys.stdout.write('.')
            sys.stdout.flush()  #此处一行是为了每隔一秒输出一行(不至于一次哗全圈出来),为了效率可以删掉
            time.sleep(1.0)

class IndexFiles(object):
    """Usage: python IndexFiles <doc_directory>"""

    def __init__(self, root, storeDir, analyzer):

        if not os.path.exists(storeDir):     #如果存储路径不存在就创建一个
            os.mkdir(storeDir)     #mkdir(obj):在当前目录下建立一个名为obj的文件夹

        store = SimpleFSDirectory(File(storeDir)) #一个告诉IndexWriter索引存在哪里的变量,
                                                  #FSDirectory是Lucene对文件系统的操作,
                                                  #它有下面三个子类SimpleFSDirectory、MmapDirectory、NIOFSDirectory;
                                                  #详见 http://www.cnblogs.com/skyme/p/3457723.html
        analyzer = LimitTokenCountAnalyzer(analyzer, 1048576) #This Analyzer limits the number of tokens while indexing. It is a replacement for the maximum field length setting inside IndexWriter
        config = IndexWriterConfig(Version.LUCENE_CURRENT, analyzer)     #config:设置,此处为IndexWriter的设置变量,Version: Use by certain classes to match version compatibility across releases of Lucene.
        config.setOpenMode(IndexWriterConfig.OpenMode.CREATE)
        writer = IndexWriter(store, config)

        self.indexDocs(root, writer)
        ticker = Ticker()
        print 'commit index',
        threading.Thread(target=ticker.run).start()
        writer.commit()
        writer.close()
        ticker.tick = False
        print 'done'

    def indexDocs(self, root, writer):

        t1 = FieldType()
        t1.setIndexed(True)
        t1.setStored(True)
        t1.setTokenized(False)
        t1.setIndexOptions(FieldInfo.IndexOptions.DOCS_AND_FREQS)     #t1为一个config变量

        t2 = FieldType()
        t2.setIndexed(True)
        t2.setStored(False)
        t2.setTokenized(True)
        t2.setIndexOptions(FieldInfo.IndexOptions.DOCS_AND_FREQS_AND_POSITIONS)

        for root, dirnames, filenames in os.walk(root):     
            for filename in filenames:
                if not filename.endswith('.txt'):
                    continue
                print "adding", filename
                try:
                    path = os.path.join(root, filename)
                    file = open(path)
                    contents = unicode(file.read(), 'iso-8859-1')      #iso-8859-1是针对西文的编码,对中文一般不用unicode编码
                    file.close()
                    doc = Document()     #保存索引的document
                    doc.add(Field("name", filename, t1))
                    doc.add(Field("path", root, t1))
                    if len(contents) > 0:
                        doc.add(Field("contents", contents, t2))
                    else:
                        print "warning: no content in %s" % filename
                    writer.addDocument(doc)     #将document加到writer中
                except Exception, e:
                    print "Failed in indexDocs:", e
/*Lucene在doc.add(lucene.Field(“contents”, contents, lucene.Field.Store.NO, lucene.Field.Index.ANALYZED)) 时,Field有2个属性可选:存储和索引。 通过存储属性可以控制是否对这个Field进行存储:     Field.Store.YES:存储字段值(未分词前的字段值)Field.Store.NO:不存储,存储与索引没有关系 通过索引属性可以控制是否对该Field进行索引:     Field.Index.ANALYZED:分词建索引     Field.Index.NOT_ANALYZED:不分词且索引 例子中,path属性为(Field.Store.YES, Field.Index.NOT_ANALYZED),要存储,不要索引。这是不能被搜索的,因为没人用他来查询。他只是搜索结果的附属物。用户通常在搜索content之后,附带返回path,因此需要存储。 contents属性为(Field.Store.NO, Field.Index.ANALYZED)。要分词索引,但不存储。因为contents太大,界面也不用显示整个内容。 属性为Field.Store.YES的Field,在SearchFiles.py中可以通过doc.get()得到。例如doc.get("path"),doc.get("name") */

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print IndexFiles.__doc__
        sys.exit(1)
    lucene.initVM(vmargs=['-Djava.awt.headless=true'])
    print 'lucene', lucene.VERSION
    start = datetime.now()
    try:
        base_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
        IndexFiles(sys.argv[1], os.path.join(base_dir, INDEX_DIR),
                   StandardAnalyzer(Version.LUCENE_CURRENT))
        end = datetime.now()
        print end - start
    except Exception, e:
        print "Failed: ", e
        raise e

#!/usr/bin/env python
#SearchFiles.py

INDEX_DIR = "IndexFiles.index"

import sys, os, lucene

from java.io import File
from org.apache.lucene.analysis.standard import StandardAnalyzer
from org.apache.lucene.index import DirectoryReader
from org.apache.lucene.queryparser.classic import QueryParser
from org.apache.lucene.store import SimpleFSDirectory
from org.apache.lucene.search import IndexSearcher
from org.apache.lucene.util import Version

"""
This script is loosely based on the Lucene (java implementation) demo class
org.apache.lucene.demo.SearchFiles.  It will prompt for a search query, then it
will search the Lucene index in the current directory called 'index' for the
search query entered against the 'contents' field.  It will then display the
'path' and 'name' fields for each of the hits it finds in the index.  Note that
search.close() is currently commented out because it causes a stack overflow in
some cases.
"""
def run(searcher, analyzer):
    while True:
        print
        print "Hit enter with no input to quit."
        command = raw_input("Query:")     # raw_input() 直接读取控制台的输入(任何类型的输入它都可以接收)
                                          #而对于 input() ,它希望能够读取一个合法的 python 表达式,即你输入字符串的时候必须使用引号将它括起来,否则它会引发一个 SyntaxError
        if command == '':
            return

        print
        print "Searching for:", command
        query = QueryParser(Version.LUCENE_CURRENT, "contents",
                            analyzer).parse(command)       #QueryParser实际上就是一个解析用户输入的工具,可以通过扫描用户输入的字符串,生成Query对象
        scoreDocs = searcher.search(query, 50).scoreDocs
        print "%s total matching documents." % len(scoreDocs)

        for scoreDoc in scoreDocs:
            doc = searcher.doc(scoreDoc.doc)
            print 'path:', doc.get("path"), 'name:', doc.get("name")


if __name__ == '__main__':
    lucene.initVM(vmargs=['-Djava.awt.headless=true'])     #使用pylucene之前要初始化虚拟机
    print 'lucene', lucene.VERSION
    base_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
    directory = SimpleFSDirectory(File(os.path.join(base_dir, INDEX_DIR)))
    searcher = IndexSearcher(DirectoryReader.open(directory))
    analyzer = StandardAnalyzer(Version.LUCENE_CURRENT)
    run(searcher, analyzer)
    del searcher

  • 网页预处理(除tag)
    • 网页源代码中包含HTML tag(例如<html>,<body>等),在加入lucene前,可以用BeautifulSoup等库过滤文档中的 HTML tag。
    • 方法一:BeautifulSoup过滤tag   
      • ''.join(soup.findAll(text=True))
    • 方法二:nltk库过滤tag(通过easy_install nltk安装,速度比BeautifulSoup快)   
      • nltk.clean_html(content)
  • 中文分词问题
    • 由于汉字词条长度主要在2~4之间,StandardAnalyzer的词汇单元与汉语中词相差甚远。CJKAnalyzer虽然在某种程度更符合汉语的习惯,但是这样分词使得每个汉字都在两个词语中,使得词语的效率只有50%左右。 让Lucene支持中文分词,有两种做法:一种是实现自己的Analyzer,一般需要实现自己的Analyzer,Filter,Tokenizer类;一种是用现有的分词库,将文本先以空格方式分好词后,再给WhitespaceAnalyzer或SimpleAnalyzer这些英文分词器处理(他们以空格做为分割分词)
  • 常用的Analyzer(分词,语言处理) :
    • StopAnalyzer:能过滤词汇中的特定字符串和词汇,并且完成大写转小写的功能。
    • StandardAnalyzer     StandardAnalyzer 根据空格和符号来完成分词,还可以完成数字、字母、E-mail地址、IP地址以及中文字符的分析处理。对中文分词时,他将每个汉字作为一个词。
    • SimpleAnalyzer     SimpleAnalyzer具备基本西文字符词汇分析的分词器,处理词汇单元时,以非字母字符作为分割符号。分词器不能做词汇的过滤。输出地词汇单元完成小写字符转换,去掉标点符号等分割符。
    • WhitespaceAnalyzer     WhitespaceAnalyzer使用空格作为间隔符的词汇分割分词器。处理词汇单元的时候,以空格字符作为分割符号。分词器不做词汇过滤,也不进行小写字符转换。
    • CJKAnalyzer     CJKAnalyzer根据汉语中词条长度为2居多的特点,将每相邻2个字作为词汇单元。
  • reference:


二.jieba分词
  • 三种模式:
    • 精确模式,试图将句子最精确地切开,适合文本分析;
    • 全模式,把句子中所有的可以成词的词语都扫描出来, 速度非常快,但是不能解决歧义;
    • 搜索引擎模式,在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词。
  • 分词:
    • jieba.cut方法接受两个输入参数: 1) 第一个参数为需要分词的字符串 2)cut_all参数用来控制是否采用全模式

    • jieba.cut_for_search方法接受一个参数:需要分词的字符串,该方法适合用于搜索引擎构建倒排索引的分词,粒度比较细

    • 注意:待分词的字符串可以是gbk字符串、utf-8字符串或者unicode

    • jieba.cut以及jieba.cut_for_search返回的结构都是一个可迭代的generator,可以使用for循环来获得分词后得到的每一个词语(unicode),也可以用list(jieba.cut(...))转化为list
代码示例
#encoding=utf-8
import jieba

seg_list = jieba.cut("我来到北京清华大学",cut_all=True)
print "Full Mode:", "/ ".join(seg_list) #全模式

seg_list = jieba.cut("我来到北京清华大学",cut_all=False)
print "Default Mode:", "/ ".join(seg_list) #精确模式

seg_list = jieba.cut("他来到了网易杭研大厦") #默认是精确模式
print ", ".join(seg_list)

seg_list = jieba.cut_for_search("小明硕士毕业于中国科学院计算所,后在日本京都大学深造") #搜索引擎模式
print ", ".join(seg_list)
  • 自定义词典:
    • jieba.load_userdict(file_name) # file_name为自定义词典的路径

    • 词典格式和dict.txt一样,一个词占一行;每一行分三部分,一部分为词语,另一部分为词频,最后为词性(可省略),用空格隔开
  • 关键词(文学意义)提取:
    • jieba.analyse.extract_tags(sentence,topK) #需要先import jieba.analyse

    • setence为待提取的文本

    • topK为返回几个TF/IDF权重最大的关键词,默认值为20
  • 在cut()之后,可以提取词性
>>> import jieba.posseg as pseg
>>> words =pseg.cut("我爱北京*")
>>> for w in words:
...    print w.word,w.flag     #word.flag为词性
...
我 r
爱 v
北京 ns
* ns
  • 安装:

    Python 2.x 下的安装

    • 全自动安装:easy_install jieba 或者 pip install jieba

    • 半自动安装:先下载http://pypi.python.org/pypi/jieba/ ,解压后运行python setup.py install

    • 手动安装:将jieba目录放置于当前目录或者site-packages目录

    • 通过import jieba 来引用 (第一次import时需要构建Trie树,需要几秒时间)

  • Python 3.x 下的安装

    • 目前master分支是只支持Python2.x 的

    • Python3.x 版本的分支也已经基本可用: https://github.com/fxsjy/jieba/tree/jieba3k
      git clone https://github.com/fxsjy/jieba.git
      git checkout jieba3k
      python setup.py install

三.python下的编码问题