Source code for pythainlp.tag.thainer
# SPDX-FileCopyrightText: 2016-2026 PyThaiNLP Project
# SPDX-FileType: SOURCE
# SPDX-License-Identifier: Apache-2.0
"""Named entity recognizer."""
from __future__ import annotations
__all__: list[str] = ["ThaiNameTagger"]
from typing import Union
from pythainlp.corpus import get_corpus_path, thai_stopwords
from pythainlp.tag._utils import _iob_to_markup
from pythainlp.tag.crf import CRFTagger
from pythainlp.tag.pos_tag import pos_tag
from pythainlp.tokenize import word_tokenize
from pythainlp.util import is_thai
_TOKENIZER_ENGINE: str = "mm"
def _is_stopword(word: str) -> bool: # เช็คว่าเป็นคำฟุ่มเฟือย
return word in thai_stopwords()
def _doc2features(
doc: list[tuple[str, str]], i: int
) -> dict[str, Union[str, bool]]:
features: dict[str, Union[str, bool]]
word = doc[i][0]
postag = doc[i][1]
# Features from current word
features = {
"word.word": word,
"word.stopword": _is_stopword(word),
"word.isthai": is_thai(word),
"word.isspace": word.isspace(),
"postag": postag,
"word.isdigit": word.isdigit(),
}
if word.isdigit() and len(word) == 5:
features["word.islen5"] = True
# Features from previous word
if i > 0:
prevword = doc[i - 1][0]
prevpostag = doc[i - 1][1]
prev_features: dict[str, Union[str, bool]] = {
"word.prevword": prevword,
"word.previsspace": prevword.isspace(),
"word.previsthai": is_thai(prevword),
"word.prevstopword": _is_stopword(prevword),
"word.prevpostag": prevpostag,
"word.prevwordisdigit": prevword.isdigit(),
}
features.update(prev_features)
else:
features["BOS"] = True # Special "Beginning of Sequence" tag
# Features from next word
if i < len(doc) - 1:
nextword = doc[i + 1][0]
nextpostag = doc[i + 1][1]
next_features: dict[str, Union[str, bool]] = {
"word.nextword": nextword,
"word.nextisspace": nextword.isspace(),
"word.nextpostag": nextpostag,
"word.nextisthai": is_thai(nextword),
"word.nextstopword": _is_stopword(nextword),
"word.nextwordisdigit": nextword.isdigit(),
}
features.update(next_features)
else:
features["EOS"] = True # Special "End of Sequence" tag
return features
[docs]
class ThaiNameTagger:
"""
Thai named entity recognizer (Thai NER).
This class supports Thai NER 1.4 and 1.5 only.
:param str version: Thai NER version, ``"1.4"`` (default)
or ``"1.5"``
:Example:
>>> from pythainlp.tag.thainer import ThaiNameTagger # doctest: +SKIP
>>> thainer14 = ThaiNameTagger(version="1.4") # doctest: +SKIP
>>> thainer14.get_ner(
... "วันที่ 15 ก.ย. 61 ทดสอบระบบเวลา 14:49 น."
... ) # doctest: +SKIP
"""
crf: CRFTagger
pos_tag_name: str
[docs]
def __init__(self, version: str = "1.4") -> None:
"""
Initialize the Thai named entity recognizer.
:param str version: Thai NER version, ``"1.4"`` (default)
or ``"1.5"``
"""
self.crf: CRFTagger = CRFTagger()
if version == "1.4":
model_path = get_corpus_path("thainer-1.4", version="1.4")
if not model_path:
raise FileNotFoundError(
"corpus-not-found name='thainer-1.4'\n"
" Corpus 'thainer-1.4' not found.\n"
" Python: pythainlp.corpus.download('thainer-1.4')\n"
" CLI: thainlp data get thainer-1.4"
)
self.crf.open(model_path)
self.pos_tag_name: str = "orchid_ud"
elif version in ("1.5", "1.5.1"):
model_path = get_corpus_path("thainer", version="1.5.1")
if not model_path:
model_path = get_corpus_path("thainer", version="1.5")
if not model_path:
raise FileNotFoundError(
"corpus-not-found name='thainer'\n"
" Corpus 'thainer' not found.\n"
" Python: pythainlp.corpus.download('thainer')\n"
" CLI: thainlp data get thainer"
)
self.crf.open(model_path)
self.pos_tag_name = "blackboard"
[docs]
def get_ner(
self, text: str, pos: bool = True, tag: bool = False
) -> Union[list[tuple[str, str]], list[tuple[str, str, str]], str]:
"""
Tag named entities in text in IOB format.
:param str text: Thai text to be tagged
:param bool pos: include POS tags in the results (``True``,
default) or exclude them (``False``)
:param bool tag: return the text with HTML-like tags
instead of a list of tuples
:return: list of tuples of word, POS tag (if ``pos`` is ``True``),
and named entity tag; or the text with HTML-like tags
(if ``tag`` is ``True``)
:rtype: Union[list[tuple[str, str]], list[tuple[str, str, str]], str]
:Note:
* For the POS tags to be included in the results, this function
uses :func:`pythainlp.tag.pos_tag` with engine `perceptron`
and corpus `orchid_ud`.
:Example:
>>> from pythainlp.tag.thainer import ThaiNameTagger
>>>
>>> ner = ThaiNameTagger()
>>> ner.get_ner("วันที่ 15 ก.ย. 61 ทดสอบระบบเวลา 14:49 น.")
[('วันที่', 'NOUN', 'O'), (' ', 'PUNCT', 'O'),
('15', 'NUM', 'B-DATE'), (' ', 'PUNCT', 'I-DATE'),
('ก.ย.', 'NOUN', 'I-DATE'), (' ', 'PUNCT', 'I-DATE'),
('61', 'NUM', 'I-DATE'), (' ', 'PUNCT', 'O'),
('ทดสอบ', 'VERB', 'O'), ('ระบบ', 'NOUN', 'O'),
('เวลา', 'NOUN', 'O'), (' ', 'PUNCT', 'O'),
('14', 'NOUN', 'B-TIME'), (':', 'PUNCT', 'I-TIME'),
('49', 'NUM', 'I-TIME'), (' ', 'PUNCT', 'I-TIME'),
('น.', 'NOUN', 'I-TIME')]
>>>
>>> ner.get_ner("วันที่ 15 ก.ย. 61 ทดสอบระบบเวลา 14:49 น.",
pos=False)
[('วันที่', 'O'), (' ', 'O'),
('15', 'B-DATE'), (' ', 'I-DATE'),
('ก.ย.', 'I-DATE'), (' ', 'I-DATE'),
('61', 'I-DATE'), (' ', 'O'),
('ทดสอบ', 'O'), ('ระบบ', 'O'),
('เวลา', 'O'), (' ', 'O'),
('14', 'B-TIME'), (':', 'I-TIME'),
('49', 'I-TIME'), (' ', 'I-TIME'),
('น.', 'I-TIME')]
>>> ner.get_ner("วันที่ 15 ก.ย. 61 ทดสอบระบบเวลา 14:49 น.",
tag=True)
'วันที่ <DATE>15 ก.ย. 61</DATE> ทดสอบระบบเวลา <TIME>14:49 น.</TIME>'
"""
tokens = word_tokenize(text, engine=_TOKENIZER_ENGINE)
pos_tags = pos_tag(
tokens, engine="perceptron", corpus=self.pos_tag_name
)
x_test = ThaiNameTagger._extract_features(pos_tags)
y = self.crf.tag(x_test)
sent_ner = [(pos_tags[i][0], data) for i, data in enumerate(y)]
if tag:
return _iob_to_markup(sent_ner)
if pos:
return [
(pos_tags[i][0], pos_tags[i][1], data)
for i, data in enumerate(y)
]
return sent_ner
@staticmethod
def _extract_features(
doc: list[tuple[str, str]],
) -> list[dict[str, Union[str, bool]]]:
return [_doc2features(doc, i) for i in range(len(doc))]