main.kr.bydelta.koala.etri.proc.kt Maven / Gradle / Ivy
@file:JvmName("Util")
@file:JvmMultifileClass
package kr.bydelta.koala.etri
import com.beust.klaxon.JsonObject
import com.beust.klaxon.Klaxon
import com.github.kittinunf.fuel.core.extensions.jsonBody
import com.github.kittinunf.fuel.httpPost
import kr.bydelta.koala.*
import kr.bydelta.koala.data.*
import kr.bydelta.koala.proc.*
import java.net.ConnectException
/**
* API 통신 과정에서 발생하는 문제를 담습니다.
* @param statusCode HTTP 통신의 상태코드입니다.
* @param msg 오류 메시지입니다.
*/
class APIException(val statusCode: Int, val msg: String) : Exception(msg)
/**a character
* ETRI Open API와 통신을 하는 부분입니다.
*/
interface CanCommunicateETRIApi {
/**
* ETRI Open API의 Access Token 값입니다.
*/
val apiKey: String
/**
* ETRI Open API와 통신합니다.
*
* @throws APIException ETRI 서버가 분석 결과값이 아닌 오류를 반환한 경우
* @return 요청의 결과로 전달된 [ResultContent].
*/
@Throws(APIException::class)
fun request(apiKey: String, type: String, text: String): ResultContent {
val klaxon = Klaxon()
val requestBody = JsonObject(mapOf(
"access_key" to apiKey,
"argument" to JsonObject(mapOf(
"text" to text,
"analysis_code" to type
))
)).toJsonString()
try {
val (_, resp, res) = "http://aiopen.etri.re.kr:8000/WiseNLU".httpPost()
.jsonBody(requestBody).responseString()
val resString = res.get().replace("\\.0([,}]+)".toRegex(), "$1")
val json = klaxon.parse(resString)
if (json?.code == 0) {
return json.result
} else {
throw APIException(resp.statusCode, json?.msg ?: "HTTP Status code ${resp.statusCode}")
}
} catch (e: ConnectException) {
throw APIException(-1, "http://aiopen.etri.re.kr 로 접근할 수 있는지 확인해주십시오. ${e.message}")
}
}
/**
* [ResultPayload] 유형의 요청 결과를 Koala의 [Sentence] 목록으로 변환합니다.
*
* @param result 변환할 요청 결과
* @return 변환된 [Sentence]의 목록.
*/
fun convertPayload(result: SentenceResponse): Sentence = result.sentence
}
/**
* ETRI API와 통신을 하는 [CanAnalyzeProperty] 인터페이스입니다.
*/
interface CanParseWithETRI : CanCommunicateETRIApi, CanAnalyzeProperty {
/** 분석을 요청할 유형 */
val requestType: String
/**
* [item]을 분석하여 property 값을 반환합니다.
*
* @since 2.0.0
* @param item 분석 단위 1개입니다.
* @param sentence 원본 문장입니다.
* @return 분석의 결과물입니다.
*/
override fun attachProperty(item: SentenceResponse, sentence: String): Sentence = convert(item)
/**
* 분석기의 중간 결과인 [sentence]를 조합하여 [Sentence] 객체로 변환합니다.
*
* @since 2.0.0
* @param sentence 변환할 문장입니다.
* @return [Sentence] 객체입니다.
*/
override fun convert(sentence: SentenceResponse): Sentence = sentence.sentence
/**
* String [sentence]를 품사 분석하여 분석기가 받아들이는 [List]>으로 변환합니다.
*
* @since 2.0.0
* @param sentence 텍스트에서 변환할 문장입니다.
* @return 분석기가 받아들일 수 있는 형태의 데이터입니다. 각 Pair의 [Pair.first] 값은 변환된 문장 객체이며, [Pair.second] 값은 해당 문장의 String 값입니다.
*/
override fun convert(sentence: String): List> {
if (sentence.isBlank())
return emptyList()
val result = request(apiKey, requestType, sentence)
return result.sentences.map { it to it.surfaceString }
}
/**
* Sentence [sentence]를 해체하여 분석기가 받아들이는 [SentenceResponse]로 변환합니다.
*
* @since 2.0.0
* @param sentence 변환할 문장입니다.
* @return 분석기가 받아들일 수 있는 형태의 데이터입니다.
*/
override fun convert(sentence: Sentence): SentenceResponse {
throw UnsupportedOperationException("ETRI Open API는 텍스트 문장만 입력으로 받을 수 있습니다.")
}
}
/**
* ETRI 품사 분석 API의 Wrapper입니다.
*
* * 품사 분석이 가장 기본적인 분석이기 때문에, ETRI에 다른 분석을 요청 할 때 품사 분석이 자동으로 진행됩니다.
* 따라서, [Parser], [EntityRecognizer], [RoleLabeler]등을 사용하는 경우, Tagger는 호출하지 않아도 됩니다.
*
* * Open API 통신 과정에서 [APIException]이 발생할 수 있습니다.
*
* ## 참고
* **형태소**는 의미를 가지는 요소로서는 더 이상 분석할 수 없는 가장 작은 말의 단위로 정의됩니다.
*
* **형태소 분석**은 문장을 형태소의 단위로 나누는 작업을 의미합니다.
* 예) '문장을 형태소로 나눠봅시다'의 경우,
* * 문장/일반명사, -을/조사,
* * 형태소/일반명사, -로/조사,
* * 나누-(다)/동사, -어-/어미, 보-(다)/동사, -ㅂ시다/어미
* 로 대략 나눌 수 있습니다.
*
* 아래를 참고해보세요.
* * [Morpheme] 형태소를 저장하는 클래스입니다.
* * [POS] 형태소의 분류를 담은 Enum class
*
* ## 사용법 예제
*
* ### Kotlin
* ```kotlin
* val tagger = Tagger()
* val sentence = tagger.tagSentence("문장 1개입니다.")
* val sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
* // 또는
* val sentences = tagger("문장들입니다. 결과는 목록이 됩니다.")
* ```
*
* ### Scala + [koalanlp-scala](https://koalanlp.github.io/scala-support/)
* ```scala
* import kr.bydelta.koala.Implicits._
* val tagger = new Tagger()
* val sentence = tagger.tagSentence("문장 1개입니다.")
* val sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
* // 또는
* val sentences = tagger("문장들입니다. 결과는 목록이 됩니다.")
* ```
*
* ### Java
* ```java
* Tagger tagger = new Tagger()
* Sentence sentence = tagger.tagSentence("문장 1개입니다.")
* List sentences = tagger.tag("문장들입니다. 결과는 목록이 됩니다.")
* // 또는
* List sentences = tagger.invoke("문장들입니다. 결과는 목록이 됩니다.")
* ```
*
* @since 2.0.0
* @constructor 분석기를 생성합니다.
* @param apiKey ETRI Open API Access token
* @param analysisType 품사 분석을 위한 분석 타입: "wsd"(동형이의어 분석), "wsd_poly"(다의어 분석), "morp"(형태소 분석)이며, 기본값은 "wsd"입니다.
*/
class Tagger @JvmOverloads constructor(override val apiKey: String,
val analysisType: String = "wsd") :
CanTagOnlyAParagraph(), CanCommunicateETRIApi {
init {
assert(analysisType in listOf("wsd", "wsd_poly", "morp"))
}
/**
* [ResultPayload] 타입의 분석결과 [result]를 변환, [Sentence]를 구성합니다.
*
* @since 2.0.0
* @param result 변환할 분석결과.
* @return 변환된 [Sentence] 객체
*/
override fun convertSentence(result: SentenceResponse): Sentence = convertPayload(result)
/**
* 변환되지않은, [text]의 분석결과 [List]<[ResultPayload]>를 반환합니다.
*
* @since 2.0.0
* @throws APIException ETRI 서버가 분석 결과값이 아닌 오류를 반환한 경우
* @param text 분석할 String.
* @return 원본 분석기의 결과인 문장의 목록입니다.
*/
@Throws(APIException::class)
override fun tagParagraphOriginal(text: String): List =
if (text.isBlank()) emptyList()
else request(apiKey, analysisType, text).sentences
}
/**
* 의존구문분석을 수행하는 Interface입니다.
*
* * 의존구문분석을 수행하면 자동으로 [Tagger], [Parser], [EntityRecognizer]가 함께 호출됩니다.
* * 의존구문분석은 [RoleLabeler]를 하기 위한 기초 분석 과정이므로, [RoleLabeler]를 수행하면 의존구문분석이 자동으로 진행됩니다.
*
* * Open API 통신 과정에서 [APIException]이 발생할 수 있습니다.
*
* ## 참고
* **의존구조 분석**은 문장의 구성 어절들이 의존 또는 기능하는 관계를 분석하는 방법입니다.
* 예) '나는 밥을 먹었고, 영희는 짐을 쌌다'라는 문장에는
* 가장 마지막 단어인 '쌌다'가 핵심 어구가 되며,
* * '먹었고'가 '쌌다'와 대등하게 연결되고
* * '나는'은 '먹었고'의 주어로 기능하며
* * '밥을'은 '먹었고'의 목적어로 기능합니다.
* * '영희는'은 '쌌다'의 주어로 기능하고,
* * '짐을'은 '쌌다'의 목적어로 기능합니다.
*
* 아래를 참고해보세요.
* * [Word.getDependentEdges] 어절이 직접 지배하는 하위 의존구조 [DepEdge]의 목록을 가져오는 API
* * [Word.getGovernorEdge] 어절이 지배당하는 상위 의존구조 [DepEdge]를 가져오는 API
* * [Sentence.getDependencies] 전체 문장을 분석한 의존구조 [DepEdge]의 목록을 가져오는 API
* * [DepEdge] 의존구조를 저장하는 형태
* * [PhraseTag] 의존구조의 형태 분류를 갖는 Enum 값 (구구조 분류와 같음)
* * [DependencyTag] 의존구조의 기능 분류를 갖는 Enum 값
*
* ## 사용법 예제
* 분석기 `Parser`가 `CanParseDependency`를 상속받았다면,
*
* ### Kotlin
* ```kotlin
* // 문장에서 바로 분석할 때
* val parser = Parser()
* val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
*
* // 타 분석기에서 분석한 다음 이어서 분석할 때
* val taggedSentence: Sentence = ...
* val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
*
* val taggedSentList: List = ...
* val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
* ```
*
* ### Scala + [koalanlp-scala](https://koalanlp.github.io/scala-support/)
* ```scala
* import kr.bydelta.koala.Implicits._
* // 문장에서 바로 분석할 때
* val parser = new Parser()
* val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
*
* // 타 분석기에서 분석한 다음 이어서 분석할 때
* val taggedSentence: Sentence = ...
* val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
*
* val taggedSentList: java.util.List[Sentence] = ...
* val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
* ```
*
* ### Java
* ```java
* // 문장에서 바로 분석할 때
* Parser parser = Parser()
* List sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser.invoke("문장 2개입니다. 결과는 목록이 됩니다.")
*
* // 타 분석기에서 분석한 다음 이어서 분석할 때
* Sentence taggedSentence = ...
* Sentence sentence = parser.analyze(taggedSentence) // 또는 parser.invoke(taggedSentence)
*
* List taggedSentList = ...
* List sentences = parser.analyze(taggedSentList) // 또는 parser.invoke(taggedSentList)
* ```
*
* @since 2.0.0
* @constructor 분석기를 생성합니다.
* @param apiKey ETRI Open API Access token
*/
class Parser(override val apiKey: String) : CanParseDependency, CanParseWithETRI {
/** 분석을 요청할 유형 */
override val requestType: String get() = "dparse"
}
/**
* 의미역 분석(Semantic Role Labeling)을 수행하는 Interface입니다.
*
* * 의미역 분석을 수행하면 자동으로 [Tagger], [Parser], [EntityRecognizer], [RoleLabeler]가 함께 호출됩니다.
*
* * Open API 통신 과정에서 [APIException]이 발생할 수 있습니다.
*
* ## 참고
* **의미역 결정**은 문장의 구성 어절들의 역할/기능을 분석하는 방법입니다.
* 예) '나는 밥을 어제 집에서 먹었다'라는 문장에는
* 동사 '먹었다'를 중심으로
* * '나는'은 동작의 주체를,
* * '밥을'은 동작의 대상을,
* * '어제'는 동작의 시점을
* * '집에서'는 동작의 장소를 나타냅니다.
*
* 아래를 참고해보세요.
* * [Word.getArgumentRoles] 어절이 술어인 논항들의 [RoleEdge] 목록을 가져오는 API
* * [Word.getPredicateRoles] 어절이 논항인 [RoleEdge]의 술어를 가져오는 API
* * [Sentence.getRoles] 전체 문장을 분석한 의미역 구조 [RoleEdge]를 가져오는 API
* * [RoleEdge] 의미역 구조를 저장하는 형태
* * [RoleType] 의미역 분류를 갖는 Enum 값
*
* ## 사용법 예제
* 분석기 `Parser`가 `CanLabelSemanticRole`을 상속받았다면,
*
* ### Kotlin
* ```kotlin
* // 문장에서 바로 분석할 때
* val parser = Parser()
* val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
*
* // 타 분석기에서 분석한 다음 이어서 분석할 때
* val taggedSentence: Sentence = ...
* val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
*
* val taggedSentList: List = ...
* val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
* ```
*
* ### Scala + [koalanlp-scala](https://koalanlp.github.io/scala-support/)
* ```scala
* import kr.bydelta.koala.Implicits._
* // 문장에서 바로 분석할 때
* val parser = new Parser()
* val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
*
* // 타 분석기에서 분석한 다음 이어서 분석할 때
* val taggedSentence: Sentence = ...
* val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
*
* val taggedSentList: java.util.List[Sentence] = ...
* val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
* ```
*
* ### Java
* ```java
* // 문장에서 바로 분석할 때
* Parser parser = Parser()
* List sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser.invoke("문장 2개입니다. 결과는 목록이 됩니다.")
*
* // 타 분석기에서 분석한 다음 이어서 분석할 때
* Sentence taggedSentence = ...
* Sentence sentence = parser.analyze(taggedSentence) // 또는 parser.invoke(taggedSentence)
*
* List taggedSentList = ...
* List sentences = parser.analyze(taggedSentList) // 또는 parser.invoke(taggedSentList)
* ```
*
* @since 2.0.0
* @constructor 분석기를 생성합니다.
* @param apiKey ETRI Open API Access token
*/
class RoleLabeler(override val apiKey: String) : CanLabelSemanticRole, CanParseWithETRI {
/** 분석을 요청할 유형 */
override val requestType: String get() = "srl"
}
/**
* 개체명 인식 (Named Entity Recognition)을 수행하는 Interface입니다.
*
* * 개체명 인식을 수행하면 자동으로 [Tagger]가 함께 호출됩니다.
* * 개체명 인식은 [RoleLabeler], [Parser]를 하기 위한 기초 분석 과정이므로, 두 분석을 수행하면 의존구문분석이 자동으로 진행됩니다.
*
* * Open API 통신 과정에서 [APIException]이 발생할 수 있습니다.
*
* ## 참고
* **개체명 인식**은 문장에서 인물, 장소, 기관, 대상 등을 인식하는 기술입니다.
* 예) '철저한 진상 조사를 촉구하는 국제사회의 목소리가 커지고 있는 가운데, 트럼프 미국 대통령은 되레 사우디를 감싸고 나섰습니다.'에서, 다음을 인식하는 기술입니다.
* * '트럼프': 인물
* * '미국' : 국가
* * '대통령' : 직위
* * '사우디' : 국가
*
* 아래를 참고해보세요.
* * [Word.getEntities] 어절이 속하는 [Entity]를 가져오는 API
* * [Sentence.getEntities] 문장에 포함된 모든 [Entity]를 가져오는 API
* * [Entity] 개체명을 저장하는 형태
* * [CoarseEntityType] [Entity]의 대분류 개체명 분류구조 Enum 값
*
* ## 사용법 예제
* 분석기 `Parser`가 `CanRecognizeEntity`를 상속받았다면,
*
* ### Kotlin
* ```kotlin
* // 문장에서 바로 분석할 때
* val parser = Parser()
* val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
*
* // 타 분석기에서 분석한 다음 이어서 분석할 때
* val taggedSentence: Sentence = ...
* val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
*
* val taggedSentList: List = ...
* val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
* ```
*
* ### Scala + [koalanlp-scala](https://koalanlp.github.io/scala-support/)
* ```scala
* import kr.bydelta.koala.Implicits._
* // 문장에서 바로 분석할 때
* val parser = new Parser()
* val sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser("문장 2개입니다. 결과는 목록이 됩니다.")
*
* // 타 분석기에서 분석한 다음 이어서 분석할 때
* val taggedSentence: Sentence = ...
* val sentence = parser.analyze(taggedSentence) // 또는 parser(taggedSentence)
*
* val taggedSentList: java.util.List[Sentence] = ...
* val sentences = parser.analyze(taggedSentList) // 또는 parser(taggedSentList)
* ```
*
* ### Java
* ```java
* // 문장에서 바로 분석할 때
* Parser parser = Parser()
* List sentences = parser.analyze("문장 2개입니다. 결과는 목록이 됩니다.") // 또는 parser.invoke("문장 2개입니다. 결과는 목록이 됩니다.")
*
* // 타 분석기에서 분석한 다음 이어서 분석할 때
* Sentence taggedSentence = ...
* Sentence sentence = parser.analyze(taggedSentence) // 또는 parser.invoke(taggedSentence)
*
* List taggedSentList = ...
* List sentences = parser.analyze(taggedSentList) // 또는 parser.invoke(taggedSentList)
* ```
*
* @since 2.0.0
* @constructor 분석기를 생성합니다.
* @param apiKey ETRI Open API Access token
*/
class EntityRecognizer(override val apiKey: String) : CanRecognizeEntity, CanParseWithETRI {
/** 분석을 요청할 유형 */
override val requestType: String get() = "ner"
} © 2015 - 2025 Weber Informatics LLC | Privacy Policy