Source code for rhoknp.units.document
import logging
from collections.abc import Sequence
try:
from typing import override # type: ignore[attr-defined]
except ImportError:
from typing_extensions import override
from rhoknp.cohesion.pas import Pas
from rhoknp.props.named_entity import NamedEntity
from rhoknp.units.base_phrase import BasePhrase
from rhoknp.units.clause import Clause
from rhoknp.units.morpheme import Morpheme
from rhoknp.units.phrase import Phrase
from rhoknp.units.sentence import Sentence
from rhoknp.units.unit import Unit
from rhoknp.utils.comment import is_comment_line
logger = logging.getLogger(__name__)
[docs]
class Document(Unit):
"""文書クラス.
Args:
text: 文書の文字列.
"""
EOD = "EOD"
count = 0
def __init__(self, text: str | None = None) -> None:
super().__init__()
Sentence.count = 0
# child units
self._sentences: list[Sentence] | None = None
if text is not None:
self.text = text
self.index = self.count
Document.count += 1
self.doc_id: str = "" #: 文書 ID.
@override
def __post_init__(self) -> None:
super().__post_init__()
# Set doc_id.
if not self.is_senter_required() and len(self.sentences) > 0:
doc_ids = [sentence.doc_id for sentence in self.sentences]
self.doc_id = doc_ids[0]
if not all(doc_id == self.doc_id for doc_id in doc_ids):
logger.warning(
f"'doc_id' is not consistent; use 'doc_id' extracted from the first sentence: {self.doc_id}."
)
@override
def __hash__(self) -> int:
return hash((self.doc_id, self.text))
@override
def __eq__(self, other: object) -> bool:
if not isinstance(other, type(self)):
return False
return self.doc_id == other.doc_id and self.text == other.text
@property
def parent_unit(self) -> None:
"""上位の言語単位.文書は最上位の言語単位なので常に None."""
return
@property
def child_units(self) -> list[Sentence] | None:
"""下位の言語単位(文)のリスト.解析結果にアクセスできないなら None."""
return self._sentences
@property
def did(self) -> str:
"""文書 ID(doc_id のエイリアス)."""
return self.doc_id
@did.setter
def did(self, did: str) -> None:
"""文書 ID(doc_id のエイリアス).
Args:
did: 文書 ID.
"""
self.doc_id = did
@property
def sentences(self) -> list[Sentence]:
"""文のリスト.
Raises:
AttributeError: 解析結果にアクセスできない場合.
"""
if self._sentences is None:
raise AttributeError("sentences have not been set")
return self._sentences
@sentences.setter
def sentences(self, sentences: list[Sentence]) -> None:
"""文のリスト.
Args:
sentences: 文のリスト.
"""
for sentence in sentences:
sentence.document = self
self._sentences = sentences
@property
def clauses(self) -> list[Clause]:
"""節のリスト.
Raises:
AttributeError: 解析結果にアクセスできない場合.
"""
return [clause for sentence in self.sentences for clause in sentence.clauses]
@property
def phrases(self) -> list[Phrase]:
"""文節のリスト.
Raises:
AttributeError: 解析結果にアクセスできない場合.
"""
return [phrase for sentence in self.sentences for phrase in sentence.phrases]
@property
def base_phrases(self) -> list[BasePhrase]:
"""基本句のリスト.
Raises:
AttributeError: 解析結果にアクセスできない場合.
"""
return [base_phrase for sentence in self.sentences for base_phrase in sentence.base_phrases]
@property
def morphemes(self) -> list[Morpheme]:
"""形態素のリスト.
Raises:
AttributeError: 解析結果にアクセスできない場合.
"""
return [morpheme for sentence in self.sentences for morpheme in sentence.morphemes]
@property
def named_entities(self) -> list[NamedEntity]:
"""固有表現のリスト.
Raises:
AttributeError: 解析結果にアクセスできない場合.
"""
return [ne for sentence in self.sentences for ne in sentence.named_entities]
@property
def pas_list(self) -> list[Pas]:
"""述語項構造のリスト.
Raises:
AttributeError: 解析結果にアクセスできない場合.
"""
return [pas for sentence in self.sentences for pas in sentence.pas_list]
[docs]
@classmethod
def from_raw_text(cls, text: str) -> "Document":
"""文書クラスのインスタンスを文書の生テキストから初期化.
Args:
text: 文書の生テキスト.
Example:
>>> from rhoknp import Document
>>> text = "天気が良かったので散歩した。途中で先生に会った。"
>>> doc = Document.from_raw_text(text)
"""
document = cls(text.strip())
document.__post_init__()
return document
[docs]
@classmethod
def from_line_by_line_text(cls, text: str) -> "Document":
"""文書クラスのインスタンスを一行一文形式のテキストから初期化.
Args:
text: 一行一文形式に整形された文書のテキスト.
Example:
>>> from rhoknp import Document
>>> sents = \"\"\"
... # S-ID:1
... 天気が良かったので散歩した。
... # S-ID:2
... 途中で先生に会った。
... \"\"\"
>>> doc = Document.from_line_by_line_text(sents)
.. note::
# から始まる行は直後の文に対するコメントとして認識される.
"""
document = cls()
sentences = []
sentence_lines: list[str] = []
for line in text.split("\n"):
if line.strip() == "":
continue
sentence_lines.append(line)
if is_comment_line(line):
continue
sentences.append(Sentence.from_raw_text("\n".join(sentence_lines), post_init=False))
sentence_lines = []
document.sentences = sentences
document.__post_init__()
return document
[docs]
@classmethod
def from_sentences(cls, sentences: Sequence[Sentence | str]) -> "Document":
"""文書クラスのインスタンスを文のリストから初期化.
Args:
sentences: 文(文の文字列)のリスト.
Example:
>>> from rhoknp import Document
>>> sents = ["天気が良かったので散歩した。", "途中で先生に会った。"]
>>> doc = Document.from_sentences(sents)
"""
document = cls()
sentences_ = []
for sentence in sentences:
if isinstance(sentence, Sentence):
if sentence.is_jumanpp_required():
sentences_.append(Sentence.from_raw_text(sentence.text, post_init=False))
elif sentence.is_knp_required():
sentences_.append(Sentence.from_jumanpp(sentence.to_jumanpp(), post_init=False))
else:
sentences_.append(Sentence.from_knp(sentence.to_knp(), post_init=False))
else:
sentences_.append(Sentence.from_raw_text(sentence.strip(), post_init=False))
document.sentences = sentences_
document.__post_init__()
return document
[docs]
@classmethod
def from_jumanpp(cls, jumanpp_text: str) -> "Document":
"""文書クラスのインスタンスを Juman++ の解析結果から初期化.
Args:
jumanpp_text: Juman++ の解析結果.
Raises:
ValueError: 解析結果読み込み中にエラーが発生した場合.
Example:
>>> from rhoknp import Document
>>> jumanpp_text = \"\"\"
... # S-ID:1
... 天気 てんき 天気 名詞 6 普通名詞 1 * 0 * 0 "代表表記:天気/てんき カテゴリ:抽象物"
... が が が 助詞 9 格助詞 1 * 0 * 0 NIL
... 良かった よかった 良い 形容詞 3 * 0 イ形容詞アウオ段 18 タ形 8 "代表表記:良い/よい 反義:形容詞:悪い/わるい"
... ので ので のだ 助動詞 5 * 0 ナ形容詞 21 ダ列タ系連用テ形 12 NIL
... 散歩 さんぽ 散歩 名詞 6 サ変名詞 2 * 0 * 0 "代表表記:散歩/さんぽ ドメイン:レクリエーション カテゴリ:抽象物"
... した した する 動詞 2 * 0 サ変動詞 16 タ形 10 "代表表記:する/する 自他動詞:自:成る/なる 付属動詞候補(基本)"
... 。 。 。 特殊 1 句点 1 * 0 * 0 NIL
... EOS
... # S-ID:2
... 途中 とちゅう 途中 名詞 6 時相名詞 10 * 0 * 0 "代表表記:途中/とちゅう カテゴリ:抽象物 弱時相名詞 修飾(デ格)"
... で で で 助詞 9 格助詞 1 * 0 * 0 NIL
... 先生 せんせい 先生 名詞 6 普通名詞 1 * 0 * 0 "代表表記:先生/せんせい ドメイン:教育・学習 カテゴリ:人 人名末尾"
... に に に 助詞 9 格助詞 1 * 0 * 0 NIL
... 会った あった 会う 動詞 2 * 0 子音動詞ワ行 12 タ形 10 "代表表記:会う/あう 反義:動詞:分かれる/わかれる;動詞:別れる/わかれる"
... EOS
... \"\"\"
>>> doc = Document.from_jumanpp(jumanpp_text)
.. note::
複数文の解析結果が含まれている場合,一つの文書として扱われる.
"""
document = cls()
sentences = []
sentence_lines: list[str] = []
for line in jumanpp_text.split("\n"):
if line.strip() == "":
continue
sentence_lines.append(line)
if line.strip() == Sentence.EOS:
sentences.append(Sentence.from_jumanpp("\n".join(sentence_lines) + "\n", post_init=False))
sentence_lines = []
if sentence_lines:
logger.warning(f"the last sentence does not end with EOS: {sentence_lines}")
sentence_lines.append(Sentence.EOS)
sentences.append(Sentence.from_jumanpp("\n".join(sentence_lines) + "\n", post_init=False))
document.sentences = sentences
document.__post_init__()
return document
[docs]
@classmethod
def from_knp(cls, knp_text: str) -> "Document":
"""文書クラスのインスタンスを KNP の解析結果から初期化.
Args:
knp_text: KNP の解析結果.
Raises:
ValueError: 解析結果読み込み中にエラーが発生した場合.
Example:
>>> from rhoknp import Document
>>> knp_text = \"\"\"
... # S-ID:1
... * 1D
... + 1D
... 天気 てんき 天気 名詞 6 普通名詞 1 * 0 * 0 "代表表記:天気/てんき カテゴリ:抽象物"
... が が が 助詞 9 格助詞 1 * 0 * 0 NIL
... * 2D
... + 2D <節-区切><節-主辞>
... 良かった よかった 良い 形容詞 3 * 0 イ形容詞アウオ段 18 タ形 8 "代表表記:良い/よい 反義:形容詞:悪い/わるい"
... ので ので のだ 助動詞 5 * 0 ナ形容詞 21 ダ列タ系連用テ形 12 NIL
... * -1D
... + -1D <節-区切><節-主辞>
... 散歩 さんぽ 散歩 名詞 6 サ変名詞 2 * 0 * 0 "代表表記:散歩/さんぽ ドメイン:レクリエーション カテゴリ:抽象物"
... した した する 動詞 2 * 0 サ変動詞 16 タ形 10 "代表表記:する/する 自他動詞:自:成る/なる 付属動詞候補(基本)"
... 。 。 。 特殊 1 句点 1 * 0 * 0 NIL
... EOS
... # S-ID:2
... * 2D
... + 2D
... 途中 とちゅう 途中 名詞 6 時相名詞 10 * 0 * 0 "代表表記:途中/とちゅう カテゴリ:抽象物 弱時相名詞 修飾(デ格)"
... で で で 助詞 9 格助詞 1 * 0 * 0 NIL
... * 2D
... + 2D
... 先生 せんせい 先生 名詞 6 普通名詞 1 * 0 * 0 "代表表記:先生/せんせい ドメイン:教育・学習 カテゴリ:人 人名末尾"
... に に に 助詞 9 格助詞 1 * 0 * 0 NIL
... * -1D
... + -1D <節-区切><節-主辞>
... 会った あった 会う 動詞 2 * 0 子音動詞ワ行 12 タ形 10 "代表表記:会う/あう 反義:動詞:分かれる/わかれる;動詞:別れる/わかれる"
... 。 。 。 特殊 1 句点 1 * 0 * 0 NIL
... EOS
... \"\"\"
>>> doc = Document.from_knp(knp_text)
.. note::
複数文の解析結果が含まれている場合,一つの文書として扱われる.
"""
document = cls()
sentences = []
sentence_lines: list[str] = []
for line in knp_text.split("\n"):
if line.strip() == "":
continue
sentence_lines.append(line)
if line.strip() == Sentence.EOS:
sentences.append(Sentence.from_knp("\n".join(sentence_lines) + "\n", post_init=False))
sentence_lines = []
if sentence_lines:
logger.warning(f"the last sentence does not end with EOS: {sentence_lines}")
sentence_lines.append(Sentence.EOS)
sentences.append(Sentence.from_knp("\n".join(sentence_lines) + "\n", post_init=False))
document.sentences = sentences
document.__post_init__()
return document
[docs]
def is_senter_required(self) -> bool:
"""文分割がまだなら True."""
return self._sentences is None
[docs]
def is_jumanpp_required(self) -> bool:
"""Juman++ による形態素解析がまだなら True."""
return self.is_senter_required() or any(sentence.is_jumanpp_required() for sentence in self.sentences)
[docs]
def is_knp_required(self) -> bool:
"""KNP による構文解析がまだなら True."""
return self.is_senter_required() or any(sentence.is_knp_required() for sentence in self.sentences)
[docs]
def is_clause_tag_required(self) -> bool:
"""KNP による節-主辞・節-区切のタグ付与がまだなら True."""
return self.is_senter_required() or any(sentence.is_clause_tag_required() for sentence in self.sentences)
[docs]
def reparse(self) -> "Document":
"""文書を再構築.
.. note::
解析結果に対する編集を有効にする際に実行する必要がある.
"""
if not self.is_knp_required():
return Document.from_knp(self.to_knp())
if not self.is_jumanpp_required():
return Document.from_jumanpp(self.to_jumanpp())
if not self.is_senter_required():
return Document.from_line_by_line_text(self.to_raw_text())
return Document.from_raw_text(self.to_raw_text())
[docs]
def to_raw_text(self) -> str:
"""生テキストフォーマットに変換.
.. note::
文分割済みの場合は一行一文の形式で出力.
"""
if self.is_senter_required():
return self.text.rstrip() + "\n"
return "".join(sentence.to_raw_text() for sentence in self.sentences)
[docs]
def to_jumanpp(self) -> str:
"""Juman++ フォーマットに変換.
Raises:
AttributeError: 解析結果にアクセスできない場合.
"""
return "".join(sentence.to_jumanpp() for sentence in self.sentences)
[docs]
def to_knp(self) -> str:
"""KNP フォーマットに変換.
Raises:
AttributeError: 解析結果にアクセスできない場合.
"""
return "".join(sentence.to_knp() for sentence in self.sentences)