main.kr.bydelta.koala.arirang.proc.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of koalanlp-arirang Show documentation
Show all versions of koalanlp-arirang Show documentation
KoalaNLP는 한국어 처리의 통합 인터페이스를 지향하는 Java/Kotlin/Scala Library의 묶음입니다.
The newest version!
@file:JvmName("Util")
@file:JvmMultifileClass
package kr.bydelta.koala.arirang
import kr.bydelta.koala.POS
import kr.bydelta.koala.data.Morpheme
import kr.bydelta.koala.data.Sentence
import kr.bydelta.koala.data.Word
import kr.bydelta.koala.proc.CanTagOnlyASentence
import org.apache.lucene.analysis.ko.morph.AnalysisOutput
import org.apache.lucene.analysis.ko.morph.PatternConstants
import org.apache.lucene.analysis.ko.morph.WordSegmentAnalyzer
/**
* 아리랑 형태소 분석기입니다.
*
* @since 1.x
* @see [CanTagOnlyASentence]
*/
class Tagger : CanTagOnlyASentence>() {
/** 아리랑 형태소 분석기 원본 */
val tagger = WordSegmentAnalyzer()
/**
* List 타입의 분석결과 [result]를 변환, [Sentence]를 구성합니다.
*
* @since 1.x
* @param text 품사분석을 수행한 문단의 String입니다.
* @param result 변환할 분석결과.
* @return 변환된 [Sentence] 객체
*/
override fun convertSentence(text: String, result: List): Sentence {
var pos = 0
val words = mutableListOf()
val candidates = result.filter { it.source.trim().isNotEmpty() }
for (word in candidates) {
var surfaceCandidate = word.source.trim()
val morphCandidates = mutableListOf()
// 이전 결과에서 추정된 다음 위치(pos)와 실제 위치(newPos)를 비교하기 위해서, 추정 위치 직전부터 검색.
val newPos = minOf(maxOf(pos, text.indexOf(surfaceCandidate, startIndex = pos - 1)), text.length)
val token = text.substring(pos, newPos).trim()
// 혹시 이 문자열 앞에 다른 문자가 있었다면, 누락된 것이므로 복원함
if (token.isNotEmpty()) {
morphCandidates.add(0, Morpheme(token, POS.NA, " "))
surfaceCandidate = token + surfaceCandidate //Note: Order changed!
}
morphCandidates.addAll(interpretOutput(word).map {
Morpheme(it.first.trim(), it.second, it.third)
})
pos = newPos + surfaceCandidate.length
// Find Special characters and separate them as morphemes
var morphemes = morphCandidates.flatMap { m ->
val s = m.surface
if (SPRegex.containsMatchIn(s) || SFRegex.containsMatchIn(s) || SSRegex.containsMatchIn(s)) {
s.split(punctuationsSplit).map {
when {
it.matches(SPRegex) -> Morpheme(it, POS.SP, m.originalTag)
it.matches(SFRegex) -> Morpheme(it, POS.SF, m.originalTag)
it.matches(SSRegex) -> Morpheme(it, POS.SS, m.originalTag)
it.isBlank() -> Morpheme(it, POS.TEMP, m.originalTag)
else -> Morpheme(it.trim(), m.tag, m.originalTag)
}
}
} else {
listOf(m)
}
}
// Now separate by special characters or whitespace characters
while (morphemes.any { it.tag in checkSet }) {
val mIndex = morphemes.indexOfFirst { it.tag in checkSet }
val morph = morphemes.take(mIndex)
morphemes = morphemes.drop(mIndex)
val symbol = morphemes.getOrNull(0)?.surface // 다음 morpheme의 표면형
val sIndex =
if (symbol != null) maxOf(0, surfaceCandidate.indexOf(symbol))
else surfaceCandidate.length
val surface = surfaceCandidate.take(sIndex).trim()
// 현재 morpheme이 비어있는 단어가 아니라면
if (surface.isNotEmpty()) {
words.add(Word(surface = surface, morphemes = morph))
}
if (symbol != null && symbol.trim().isNotEmpty()) {
words.add(Word(surface = symbol.trim(), morphemes = listOf(morphemes[0])))
}
morphemes = morphemes.drop(1)
surfaceCandidate = surfaceCandidate.drop(sIndex + (symbol?.length ?: 0))
}
if (surfaceCandidate.trim().isNotEmpty()) {
words.add(Word(surface = surfaceCandidate.trim(), morphemes = morphemes))
}
}
val restOfText = text.drop(pos).trim()
if (restOfText.isNotEmpty()) {
words.add(Word(restOfText, morphemes = listOf(Morpheme(restOfText, POS.NA, " "))))
}
return Sentence(words.toList())
}
/**
* 변환되지않은, [text]의 분석결과 List를 반환합니다.
*
* @since 1.x
* @param text 분석할 String.
* @return 원본 분석기의 결과인 문장 1개
*/
override fun tagSentenceOriginal(text: String): List =
if (text.trim().isEmpty()) emptyList()
else {
val list = mutableListOf>()
tagger.analyze(text.trim(), list, false)
list.mapNotNull { out -> out.maxBy { it.score } }
}
/** 아리랑 분석기 분석 결과를 해석함
*
* **참고** 아리랑 분석기는 분석 결과를 형태소 형태로 들고 있지 않지만,
* 내부에서는 형태소 분석을 수행하므로, 이를 복원하는 게 필요함.
* */
private fun interpretOutput(o: AnalysisOutput): List> {
val morphs = mutableListOf>()
morphs.add(Triple(o.stem, when (o.pos) {
PatternConstants.POS_NPXM -> POS.NNG
PatternConstants.POS_VJXV -> POS.VV
PatternConstants.POS_AID -> POS.MAG
else -> POS.SW
}, o.pos.toString()))
if (o.nsfx != null) {
//NounSuffix
morphs.add(Triple(o.nsfx, POS.XSN, "_s"))
}
when (o.patn) {
2, 22 -> {
morphs.add(Triple(o.josa, POS.JX, "_j"))
}
3 -> {
//* 체언 + 용언화접미사 + 어미 */
morphs.add(Triple(o.vsfx, POS.XSV, "_t"))
if (o.pomi != null) {
morphs.add(Triple(o.pomi, POS.EP, "_f"))
}
morphs.add(Triple(o.eomi, POS.EF, "_e"))
}
4 -> {
//* 체언 + 용언화접미사 + '음/기' + 조사 */
morphs.add(Triple(o.vsfx, POS.XSV, "_t"))
if (o.pomi != null) {
morphs.add(Triple(o.pomi, POS.EP, "_f"))
}
morphs.add(Triple(o.elist[0], POS.ETN, "_n"))
morphs.add(Triple(o.josa, POS.JX, "_j"))
}
5 -> {
//* 체언 + 용언화접미사 + '아/어' + 보조용언 + 어미 */
morphs.add(Triple(o.vsfx, POS.XSV, "_t"))
morphs.add(Triple(o.elist[0], POS.EC, "_c"))
morphs.add(Triple(o.xverb, POS.VX, "_W"))
if (o.pomi != null) {
morphs.add(Triple(o.pomi, POS.EP, "_f"))
}
morphs.add(Triple(o.eomi, POS.EF, "_e"))
}
6 -> {
//* 체언 + '에서/부터/에서부터' + '이' + 어미 */
morphs.add(Triple(o.josa, POS.JKB, "_j"))
morphs.add(Triple(o.elist[0], POS.VCP, "_t"))
if (o.pomi != null) {
morphs.add(Triple(o.pomi, POS.EP, "_f"))
}
morphs.add(Triple(o.eomi, POS.EF, "_e"))
}
7 -> {
//* 체언 + 용언화접미사 + '아/어' + 보조용언 + '음/기' + 조사 */
morphs.add(Triple(o.vsfx, POS.XSV, "_t"))
morphs.add(Triple(o.elist[0], POS.EC, "_c"))
morphs.add(Triple(o.xverb, POS.VX, "_W"))
if (o.pomi != null) {
morphs.add(Triple(o.pomi, POS.EP, "_f"))
}
morphs.add(Triple(o.elist[0], POS.ETN, "_n"))
morphs.add(Triple(o.josa, POS.JX, "_j"))
}
11 -> {
//* 용언 + 어미 */
if (o.pomi != null) {
morphs.add(Triple(o.pomi, POS.EP, "_f"))
}
morphs.add(Triple(o.eomi, POS.EF, "_e"))
}
12 -> {
//* 용언 + '음/기' + 조사 */
if (o.pomi != null) {
morphs.add(Triple(o.pomi, POS.EP, "_f"))
}
morphs.add(Triple(o.elist[0], POS.ETN, "_n"))
morphs.add(Triple(o.josa, POS.JX, "_j"))
}
13 -> {
//* 용언 + '음/기' + '이' + 어미 */
morphs.add(Triple(o.elist[0], POS.ETN, "_n"))
morphs.add(Triple(o.elist[1], POS.VCP, "_s"))
if (o.pomi != null) {
morphs.add(Triple(o.pomi, POS.EP, "_f"))
}
morphs.add(Triple(o.eomi, POS.EF, "_e"))
}
14 -> {
//* 용언 + '아/어' + 보조용언 + 어미 */
morphs.add(Triple(o.elist[0], POS.EC, "_c"))
morphs.add(Triple(o.xverb, POS.VX, "_W"))
if (o.pomi != null) {
morphs.add(Triple(o.pomi, POS.EP, "_f"))
}
morphs.add(Triple(o.eomi, POS.EF, "_e"))
}
15 -> {
//* 용언 + '아/어' + 보조용언 + '음/기' + 조사 */
morphs.add(Triple(o.elist[1], POS.EC, "_c"))
morphs.add(Triple(o.xverb, POS.VX, "_W"))
if (o.pomi != null) {
morphs.add(Triple(o.pomi, POS.EP, "_f"))
}
morphs.add(Triple(o.elist[0], POS.ETN, "_n"))
morphs.add(Triple(o.josa, POS.JX, "_j"))
}
else -> {
}
}
return morphs.toList()
}
/** Static fields */
companion object {
/** 분리가 필요한 특수문자 */
@JvmStatic
private val checkSet = listOf(POS.SF, POS.SP, POS.SS, POS.TEMP)
/** 종결부호 */
@JvmStatic
private val SFRegex = "(?U)[.?!]+".toRegex()
/** 문장부호 */
@JvmStatic
private val SPRegex = "(?U)[,:;·/]+".toRegex()
/** 종결부호, 문장부호, 괄호 등으로 segmentize하는 Regex */
@JvmStatic
private val punctuationsSplit =
"(?U)((?<=[,.:;?!/·\\s\'\"(\\[{<〔〈《「『【‘“)\\]}>〕〉》」』】’”])|(?=[,.:;?!/·\\s\'\"(\\[{<〔〈《「『【‘“)\\]}>〕〉》」』】’”]+))".toRegex()
/** 괄호 */
@JvmStatic
private val SSRegex = "(?U)[\'\"(\\[{<〔〈《「『【‘“)\\]}>〕〉》」』】’”]+".toRegex()
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy