//Go中文分词
package sego

import (
    "bufio"
    "fmt"
    "log"
    "math"
    "os"
    "strconv"
    "strings"
    "unicode"
    "unicode/utf8"
)

const (
    minTokenFrequency = 2 // 仅从字典文件中读取大于等于此频率的分词
)

// 分词器结构体
type Segmenter struct {
    dict *Dictionary
}

// 该结构体用于记录Viterbi算法中某字元处的向前分词跳转信息
type jumper struct {
    minDistance float32
    token       *Token
}

// 返回分词器使用的词典
func (seg *Segmenter) Dictionary() *Dictionary {
    return seg.dict
}

// 从文件中载入词典
//
// 可以载入多个词典文件,文件名用","分隔,排在前面的词典优先载入分词,比如
//     "用户词典.txt,通用词典.txt"
// 当一个分词既出现在用户词典也出现在通用词典中,则优先使用用户词典。
//
// 词典的格式为(每个分词一行):
//    分词文本 频率 词性
func (seg *Segmenter) LoadDictionary(files string) {
    seg.dict = NewDictionary()
    for _, file := range strings.Split(files, ",") {
        log.Printf("载入sego词典 %s", file)
        dictFile, err := os.Open(file)
        defer dictFile.Close()
        if err != nil {
            log.Fatalf("无法载入字典文件 \"%s\" \n", file)
        }

        reader := bufio.NewReader(dictFile)
        var text string
        var freqText string
        var frequency int
        var pos string

        // 逐行读入分词
        for {
            size, _ := fmt.Fscanln(reader, &text, &freqText, &pos)

            if size == 0 {
                // 文件结束
                break
            } else if size < 2 {
                // 无效行
                continue
            } else if size == 2 {
                // 没有词性标注时设为空字符串
                pos = ""
            }

            // 解析词频
            var err error
            frequency, err = strconv.Atoi(freqText)
            if err != nil {
                continue
            }

            // 过滤频率太小的词
            if frequency < minTokenFrequency {
                continue
            }

            // 将分词添加到字典中
            words := splitTextToWords([]byte(text))
            token := Token{text: words, frequency: frequency, pos: pos}
            seg.dict.addToken(token)
        }
    }

    // 计算每个分词的路径值,路径值含义见Token结构体的注释
    logTotalFrequency := float32(math.Log2(float64(seg.dict.totalFrequency)))
    for i := range seg.dict.tokens {
        token := &seg.dict.tokens[i]
        token.distance = logTotalFrequency - float32(math.Log2(float64(token.frequency)))
    }

    // 对每个分词进行细致划分,用于搜索引擎模式,该模式用法见Token结构体的注释。
    for i := range seg.dict.tokens {
        token := &seg.dict.tokens[i]
        segments := seg.segmentWords(token.text, true)

        // 计算需要添加的子分词数目
        numTokensToAdd := 0
        for iToken := 0; iToken < len(segments); iToken++ {
            if len(segments[iToken].token.text) > 1 {
                // 略去字元长度为一的分词
                // TODO: 这值得进一步推敲,特别是当字典中有英文复合词的时候
                numTokensToAdd++
            }
        }
        token.segments = make([]*Segment, numTokensToAdd)

        // 添加子分词
        iSegmentsToAdd := 0
        for iToken := 0; iToken < len(segments); iToken++ {
            if len(segments[iToken].token.text) > 1 {
                token.segments[iSegmentsToAdd] = &segments[iToken]
                iSegmentsToAdd++
            }
        }
    }

    log.Println("sego词典载入完毕")
}

// 对文本分词
//
// 输入参数:
//    bytes    UTF8文本的字节数组
//
// 输出:
//    []Segment    划分的分词
func (seg *Segmenter) Segment(bytes []byte) []Segment {
    return seg.internalSegment(bytes, false)
}

func (seg *Segmenter) internalSegment(bytes []byte, searchMode bool) []Segment {
    // 处理特殊情况
    if len(bytes) == 0 {
        return []Segment{}
    }

    // 划分字元
    text := splitTextToWords(bytes)

    return seg.segmentWords(text, searchMode)
}

func (seg *Segmenter) segmentWords(text []Text, searchMode bool) []Segment {
    // 搜索模式下该分词已无继续划分可能的情况
    if searchMode && len(text) == 1 {
        return []Segment{}
    }

    // jumpers定义了每个字元处的向前跳转信息,包括这个跳转对应的分词,
    // 以及从文本段开始到该字元的最短路径值
    jumpers := make([]jumper, len(text))

    tokens := make([]*Token, seg.dict.maxTokenLength)
    for current := 0; current < len(text); current++ {
        // 找到前一个字元处的最短路径,以便计算后续路径值
        var baseDistance float32
        if current == 0 {
            // 当本字元在文本首部时,基础距离应该是零
            baseDistance = 0
        } else {
            baseDistance = jumpers[current-1].minDistance
        }

        // 寻找所有以当前字元开头的分词
        numTokens := seg.dict.lookupTokens(
            text[current:minInt(current+seg.dict.maxTokenLength, len(text))], tokens)

        // 对所有可能的分词,更新分词结束字元处的跳转信息
        for iToken := 0; iToken < numTokens; iToken++ {
            location := current + len(tokens[iToken].text) - 1
            if !searchMode || current != 0 || location != len(text)-1 {
                updateJumper(&jumpers[location], baseDistance, tokens[iToken])
            }
        }

        // 当前字元没有对应分词时补加一个伪分词
        if numTokens == 0 || len(tokens[0].text) > 1 {
            updateJumper(&jumpers[current], baseDistance,
                &Token{text: []Text{text[current]}, frequency: 1, distance: 32, pos: "x"})
        }
    }

    // 从后向前扫描第一遍得到需要添加的分词数目
    numSeg := 0
    for index := len(text) - 1; index >= 0; {
        location := index - len(jumpers[index].token.text) + 1
        numSeg++
        index = location - 1
    }

    // 从后向前扫描第二遍添加分词到最终结果
    outputSegments := make([]Segment, numSeg)
    for index := len(text) - 1; index >= 0; {
        location := index - len(jumpers[index].token.text) + 1
        numSeg--
        outputSegments[numSeg].token = jumpers[index].token
        index = location - 1
    }

    // 计算各个分词的字节位置
    bytePosition := 0
    for iSeg := 0; iSeg < len(outputSegments); iSeg++ {
        outputSegments[iSeg].start = bytePosition
        bytePosition += textSliceByteLength(outputSegments[iSeg].token.text)
        outputSegments[iSeg].end = bytePosition
    }
    return outputSegments
}

// 更新跳转信息:
//     1. 当该位置从未被访问过时(jumper.minDistance为零的情况),或者
//    2. 当该位置的当前最短路径大于新的最短路径时
// 将当前位置的最短路径值更新为baseDistance加上新分词的概率
func updateJumper(jumper *jumper, baseDistance float32, token *Token) {
    newDistance := baseDistance + token.distance
    if jumper.minDistance == 0 || jumper.minDistance > newDistance {
        jumper.minDistance = newDistance
        jumper.token = token
    }
}

// 取两整数较小值
func minInt(a, b int) int {
    if a > b {
        return b
    }
    return a
}

// 取两整数较大值
func maxInt(a, b int) int {
    if a > b {
        return a
    }
    return b
}

// 将文本划分成字元
func splitTextToWords(text Text) []Text {
    output := make([]Text, 0, len(text)/3)
    current := 0
    inAlphanumeric := true
    alphanumericStart := 0
    for current < len(text) {
        r, size := utf8.DecodeRune(text[current:])
        if size <= 2 && (unicode.IsLetter(r) || unicode.IsNumber(r)) {
            // 当前是拉丁字母或数字(非中日韩文字)
            if !inAlphanumeric {
                alphanumericStart = current
                inAlphanumeric = true
            }
        } else {
            if inAlphanumeric {
                inAlphanumeric = false
                if current != 0 {
                    output = append(output, toLower(text[alphanumericStart:current]))
                }
            }
            output = append(output, text[current:current+size])
        }
        current += size
    }

    // 处理最后一个字元是英文的情况
    if inAlphanumeric {
        if current != 0 {
            output = append(output, toLower(text[alphanumericStart:current]))
        }
    }

    return output
}

// 将英文词转化为小写
func toLower(text []byte) []byte {
    output := make([]byte, len(text))
    for i, t := range text {
        if t >= 'A' && t <= 'Z' {
            output[i] = t - 'A' + 'a'
        } else {
            output[i] = t
        }
    }
    return output
}

segmenter.go的更多相关文章

  1. Windows平台下使用ffmpeg和segmenter实现m3u8直播点播

    1.安装windows media service 实现 流媒体服务器功能   2.windows media编码器 实现 直播推流   3.使用 vlc 将 mms://127.0.0.1/live ...

  2. [转]Iphone m3u8 segmenter from ffmpeg for video streaming

    源地址:http://lukasz.cepowski.com/devlog/30,iphone-m3u8-segmenter-from-ffmpeg-for-video-streaming Recen ...

  3. Stanford Word Segmenter使用

    1,下载 Stanford Word Segmenter软件包: Download Stanford Word Segmenter version 2014-06-16 2,在eclipse上建立一个 ...

  4. Configure the Stanford segmenter for NLTK

    >>> from nltk.tokenize.stanford_segmenter import StanfordSegmenter >>> segmenter = ...

  5. Stanford Word Segmenter的特定领域训练

    有没有人自己训练过Stanford Word Segmenter分词器,因为我想做特定领域的分词,但在使用Stanford Word Segmenter分词的时候发现对于我想做的领域的一些词分词效果并 ...

  6. 【NLP】干货!Python NLTK结合stanford NLP工具包进行文本处理

    干货!详述Python NLTK下如何使用stanford NLP工具包 作者:白宁超 2016年11月6日19:28:43 摘要:NLTK是由宾夕法尼亚大学计算机和信息科学使用python语言实现的 ...

  7. Python自然语言处理工具小结

    Python自然语言处理工具小结 作者:白宁超 2016年11月21日21:45:26 目录 [Python NLP]干货!详述Python NLTK下如何使用stanford NLP工具包(1) [ ...

  8. 【NLP】Python NLTK处理原始文本

    Python NLTK 处理原始文本 作者:白宁超 2016年11月8日22:45:44 摘要:NLTK是由宾夕法尼亚大学计算机和信息科学使用python语言实现的一种自然语言工具包,其收集的大量公开 ...

  9. 【NLP】Python NLTK获取文本语料和词汇资源

    Python NLTK 获取文本语料和词汇资源 作者:白宁超 2016年11月7日13:15:24 摘要:NLTK是由宾夕法尼亚大学计算机和信息科学使用python语言实现的一种自然语言工具包,其收集 ...

随机推荐

  1. window配置mongodb集群(副本集)

    参数解释: dbpath:数据存放目录 logpath:日志存放路径 pidfilepath:进程文件,有利于关闭服务 logappend:以追加的方式记录日志(boolean值) replSet:副 ...

  2. ionic3-ng4学习见闻--(轮播图完美方案)

    ionic上 轮播图是最坑的插件了吧,各种bug和 问题. 事件也不好用.. 于是,我终于搞出来了一个完美的方案, 适用于,动态获取轮播图数据,自动循环播放,跳转其他页面回来后自动播放,手指触摸后自动 ...

  3. <mate name="viewport">移动端设置详解

    <meta name="viewport" content="width=device-width,height=device-height,initial-sca ...

  4. sqlplus 分析执行计划

    转载 http://xm-koma.iteye.com/blog/1048451 对于oracle9i,需要手工设置plustrace角色,步骤如下: 1.在SQL>connect sys/密码 ...

  5. 集群中几种session同步解决方案的比较[转]

    集群中session安全和同步是个最大的问题,下面是我收集到的几种session同步的方案,希望能通过分析其各自的优劣找出其适应的场景. 1. 客户端cookie加密 这是我以前采用的方式,简单,高效 ...

  6. MySQL运维工具

    Mysql运维过程中设计的各类工具以及各个场景的的命令行的分类.大体总结如下的xmind图片(.xmind附件 加 Q1123654342). 大体上分为: 实例管理工具.高可用工具.慢日志查询工具. ...

  7. Netty入门

    一.NIO Netty框架底层是对NIO的高度封装,所以想要更好的学习Netty之前,应先了解下什么是NIO - NIO是non-blocking的简称,在jdk1.4 里提供的新api,他的他的特性 ...

  8. mongodb查询语句

    左边是mongodb语句,右边是sql语句 db.users.find() select * from users db.users.find({"age" : 27}) sele ...

  9. Python自学编程开发路线图(文中有免费资源)

    Python核心编程 免费视频资源<Python入门教程>:http://yun.itheima.com/course/145.html Python 基础学习大纲 所处阶段 主讲内容 技 ...

  10. spring cloud 入门系列六:使用Zuul 实现API网关服务

    通过前面几次的分享,我们了解了微服务架构的几个核心设施,通过这些组件我们可以搭建简单的微服务架构系统.比如通过Spring Cloud Eureka搭建高可用的服务注册中心并实现服务的注册和发现: 通 ...