hojichar.filters.document_filters

   1import json
   2import logging
   3import pathlib
   4import re
   5import string
   6import time
   7import unicodedata
   8from collections import Counter
   9from itertools import groupby
  10from os import PathLike
  11from typing import Any, Dict, List, Optional, Union
  12
  13import numpy as np
  14
  15import hojichar
  16from hojichar.core.filter_interface import Filter
  17from hojichar.core.models import Document, Token
  18
  19try:
  20    import emoji
  21    from fugashi import Tagger  # type: ignore
  22
  23    is_loaded_extras = True
  24except ImportError:
  25    is_loaded_extras = False
  26
  27BASE_PATH = pathlib.Path(hojichar.__path__[0])
  28logger = logging.getLogger(__name__)
  29
  30
  31class ExampleHojiChar(Filter):
  32    """基本的なフィルタの実装例です. 末尾に'<hojichar>'を追加します."""
  33
  34    def apply(self, document: Document) -> Document:
  35        """
  36        >>> ExampleHojiChar()("hello, world")
  37        'hello, world<hojichar>'
  38        """
  39        document.text += "<hojichar>"
  40        return document
  41
  42
  43class ExampleDiscardDocumentContainKeyword(Filter):
  44    """特定のキーワードを持つドキュメントを破棄するようなフィルタの実装例です."""
  45
  46    def __init__(self, keyword: str, *args: Any, **kwargs: Any) -> None:
  47        super().__init__(*args, **kwargs)
  48        self.keyword = keyword
  49
  50    def apply(self, document: Document) -> Document:
  51        """
  52        >>> ExampleDiscardDocumentContainKeyword("バカ").apply(Document("あいつはバカだ")).is_rejected
  53        True
  54        """
  55        if self.keyword in document.text:
  56            document.is_rejected = True
  57        return document
  58
  59
  60class Identity(Filter):
  61    """何も変化を加えないフィルタです. テスト・デバッグに用いられます."""
  62
  63    def apply(self, document: Document) -> Document:
  64        return document
  65
  66
  67class DiscardAll(Filter):
  68    """
  69    すべてのドキュメントを破棄するフィルタです.
  70    テスト・デバッグに用いられます.
  71    """
  72
  73    def apply(self, document: Document) -> Document:
  74        document.is_rejected = True
  75        return document
  76
  77
  78class ApplyDiscard(Filter):
  79    """
  80    上流フィルタで破棄された`Document`を空文字列にします.
  81
  82    `Document.is_rejected=True` の ドキュメントは無視されるため,
  83    このフィルタを `Compose` のコンストラクタに渡しても動作しません.
  84    このフィルタは主に`Compose` 内部や, `discard_filtered=False` を指定
  85    したデバッグ時などに利用されます.
  86    """
  87
  88    def __init__(self, *args: Any, **kwargs: Any) -> None:
  89        super().__init__(*args, **kwargs)
  90
  91    def apply(self, document: Document) -> Document:
  92        """
  93        >>> ApplyDiscard().apply(Document(text="hello", is_rejected=True)).text
  94        ''
  95        """
  96        if document.is_rejected:
  97            document.text = ""
  98
  99        return document
 100
 101
 102class Sleep(Filter):
 103    """
 104    デバッグ用のフィルタです. 指定秒スリープします.
 105    """
 106
 107    def __init__(self, time: float = 1.0, *args: Any, **kwargs: Any) -> None:
 108        super().__init__(*args, **kwargs)
 109        self.time = time
 110
 111    def apply(self, document: Document) -> Document:
 112        """
 113        >>> Sleep(0.1)('hello')  # After 0.1 seconds,
 114        'hello'
 115        """
 116        time.sleep(self.time)
 117        return document
 118
 119
 120class DocumentNormalizer(Filter):
 121    """
 122    Unicode の正規化をします.
 123    """
 124
 125    def __init__(self, *args: Any, **kwargs: Any) -> None:
 126        super().__init__(*args, **kwargs)
 127
 128    def apply(self, document: Document) -> Document:
 129        document.text = unicodedata.normalize("NFKC", document.text)
 130        return document
 131
 132
 133class JSONLoader(Filter):
 134    """
 135    テキストを Json として解釈し, `key` で指定した要素を文字列として
 136    doument に格納します.デフォルトの `key` は 'text' です.
 137
 138    Json の読み込み, あるいは `key` の読み込みに失敗した際には例外を送出します.
 139    これらを無視する場合は, `ignore=True` にします. その際, 読み込みに失敗
 140    したドキュメントは破棄されます.
 141    """
 142
 143    def __init__(
 144        self,
 145        key: str = "text",
 146        ignore: bool = False,
 147        extra_keys: Optional[List[str]] = None,
 148        *args: Any,
 149        **kwargs: Any,
 150    ) -> None:
 151        super().__init__(*args, **kwargs)
 152        self.key = key
 153        self.ignore = ignore
 154        self.extra_keys = extra_keys
 155
 156    def apply(self, document: Document) -> Document:
 157        """
 158        >>> JSONLoader()( '{"text": "hello, world", "words": 2}' )
 159        'hello, world'
 160
 161        >>> JSONLoader()( '{"text": hello, world ....' ) # Broken JSON
 162        Traceback (most recent call last):
 163            ...
 164        json.decoder.JSONDecodeError: Expecting value: line 1 column 10 (char 9)
 165
 166        >>> JSONLoader()( '{"words": 2}' )
 167        Traceback (most recent call last):
 168            ...
 169        KeyError: 'text'
 170
 171        >>> JSONLoader(ignore=True).apply(Document('{"text": hello, world ....' )).is_rejected
 172        True
 173        """
 174        try:
 175            data = json.loads(document.text)
 176            document.text = str(data[self.key])
 177            if self.extra_keys is not None:
 178                document.extras = {key: data[key] for key in self.extra_keys if key in data}
 179        except Exception as e:
 180            logger.error(f"Failed to parsing in JSONLoader. Input document: \n{document.text}")
 181            if self.ignore:
 182                document.is_rejected = True
 183                return document
 184            else:
 185                raise e
 186
 187        return document
 188
 189
 190class JSONDumper(Filter):
 191    """
 192    Document.text の文字列を json に変換します.
 193    必要に応じ Document のメタデータを付与します. これはドキュメントの破棄事由が含まれ、偽陽性の分析に有効です。
 194    デフォルトで `skip_rejected` が `False` にセットされており、Document の破棄フラグにかかわらず
 195    処理されます。
 196    """
 197
 198    def __init__(
 199        self,
 200        dump_reason: bool = False,
 201        p: float = 1,
 202        skip_rejected: bool = False,
 203        *args: Any,
 204        **kwargs: Any,
 205    ) -> None:
 206        """
 207        Args:
 208            dump_reason (bool, optional): `is_rejected`, `reason` エントリをダンプします. Defaults to False.
 209            p (float, optional): Apply probability. Defaults to 1.
 210            skip_rejected (bool, optional): 破棄済みサンプルを排除しません.
 211        """
 212        super().__init__(p, skip_rejected, *args, **kwargs)
 213        self.dump_reason = dump_reason
 214
 215    def apply(self, document: Document) -> Document:
 216        """
 217        >>> JSONDumper()("hojichar")
 218        '{"text": "hojichar"}'
 219        """
 220        text = document.text
 221        if self.dump_reason:
 222            document.text = json.dumps(
 223                {
 224                    "text": text,
 225                    "is_rejected": document.is_rejected,
 226                    "reason": document.reject_reason,
 227                },
 228                ensure_ascii=False,
 229            )
 230        else:
 231            document.text = json.dumps({"text": text}, ensure_ascii=False)
 232        return document
 233
 234
 235class DocumentLengthFilter(Filter):
 236    """
 237    `min_doc_len`, `max_doc_len` で指定した上限・下限の範囲内にないドキュメントを破棄します.
 238    デフォルトでは 200字 以上 50000字以内のテキストが受理されます.
 239    """
 240
 241    def __init__(
 242        self,
 243        min_doc_len: Optional[int] = None,
 244        max_doc_len: Optional[int] = None,
 245        *args: Any,
 246        **kwargs: Any,
 247    ) -> None:
 248        super().__init__(*args, **kwargs)
 249
 250        self.min_doc_len = min_doc_len
 251        self.max_doc_len = max_doc_len
 252
 253    def apply(self, doc: Document) -> Document:
 254        """
 255        >>> DocumentLengthFilter(min_doc_len=5).apply(Document("1234")).is_rejected
 256        True
 257        """
 258        doc_len = len(doc.text)
 259        if self.min_doc_len is not None:
 260            if doc_len < self.min_doc_len:
 261                doc.is_rejected = True
 262        if self.max_doc_len is not None:
 263            if self.max_doc_len < doc_len:
 264                doc.is_rejected = True
 265        return doc
 266
 267
 268class NgWordsFilterJa(Filter):
 269    """
 270    日本語のNGワード(および不適切語)を含む文書を破棄します.
 271    `dict_path` で指定したファイルから, キーワードのリストを得ます.
 272    ファイルは単語が改行で羅列されたテキストファイルです.
 273
 274    `ignore_confused` を `True` にすると,
 275    偽陽性を軽減するために, カタカナのNGワードは前後にカタカナが無い場合のみNG判定されます.
 276    デフォルト値は `False` です.
 277    """
 278
 279    def __init__(
 280        self,
 281        dict_path: Union[str, PathLike],
 282        ignore_confused: bool = False,
 283        *args: Any,
 284        **kwargs: Any,
 285    ) -> None:
 286        super().__init__(*args, **kwargs)
 287
 288        with open(dict_path, encoding="utf-8") as fp:
 289            ng_words = fp.read().split("\n")
 290        ng_words = [w.strip() for w in ng_words if not len(w) == 0]
 291
 292        if ignore_confused:
 293            words_katakana = []
 294            words_not_katakana = []
 295            for w in ng_words:
 296                if re.fullmatch(r"[ァ-ヴー]+", w):
 297                    words_katakana.append(re.escape(w))
 298                else:
 299                    words_not_katakana.append(re.escape(w))
 300            katakana_pat = "|".join(words_katakana)
 301            katakana_pat = rf"(?<![ァ-ヴー])({katakana_pat})(?![ァ-ヴー])"
 302            pat = "|".join(words_not_katakana) + "|" + katakana_pat
 303            self.keyword_pat = re.compile(pat)
 304        else:
 305            ng_words = [re.escape(w) for w in ng_words]
 306            pat = "|".join(ng_words)
 307            self.keyword_pat = re.compile(pat)
 308
 309    def apply(self, doc: Document) -> Document:
 310        regex_match = self.keyword_pat.search(doc.text)
 311        if regex_match:
 312            doc.is_rejected = True
 313            self.matched_text = regex_match.group()
 314            self.matched_text_neighbor = doc.text[
 315                regex_match.start() - 20 : regex_match.end() + 20
 316            ]
 317
 318        return doc
 319
 320
 321class NgWordsFilterEn(Filter):
 322    """
 323    英語のNGワード(および不適切語)を含む文書を破棄します.
 324    `dict_path` で指定したファイルから, キーワードのリストを得ます.
 325    ファイルは単語が改行で羅列されたテキストファイルです.
 326    """
 327
 328    def __init__(self, dict_path: Union[str, PathLike], *args: Any, **kwargs: Any) -> None:
 329        super().__init__(*args, **kwargs)
 330
 331        with open(dict_path, encoding="utf-8") as fp:
 332            ng_words = fp.read().split("\n")
 333        ng_words = [re.escape(w.strip()) for w in ng_words if not len(w) == 0]
 334        pat = "|".join(ng_words)
 335        # 英語のパターンにマッチするようにしている, \s[単語]\s や [単語]. [単語], などにマッチ.
 336        self.keyword_pat = re.compile(rf"(?:^| )({pat})(?:( |,|\.)|$)", re.IGNORECASE)
 337
 338    def apply(self, doc: Document) -> Document:
 339        if self.keyword_pat.search(doc.text):
 340            doc.is_rejected = True
 341        return doc
 342
 343
 344class DiscardAdultContentJa(NgWordsFilterJa):
 345    """
 346    日本語のアダルトキーワード(および不適切語)を含む文書を破棄します.
 347    `dict_path` で指定したファイルから, キーワードのリストを得ます.
 348    ファイルは単語が改行で羅列されたテキストファイルです.
 349    デフォルトの`dict_path` は /hojichar/dict/adult_keywords_ja.txt です.
 350    """
 351
 352    def __init__(
 353        self,
 354        dict_path: Union[str, PathLike] = BASE_PATH / "dict/adult_keywords_ja.txt",
 355        *args: Any,
 356        **kwargs: Any,
 357    ) -> None:
 358        super().__init__(dict_path, *args, **kwargs)
 359
 360    def apply(self, doc: Document) -> Document:
 361        """
 362        >>> DiscardAdultContentJa().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected
 363        True
 364
 365        >>> DiscardAdultContentJa().apply(Document("ほうじ茶")).is_rejected
 366        False
 367
 368        挙動は正しいが誤検知しているケース. 他にも, サック in リュックサック,
 369        >>> DiscardAdultContentJa().apply(Document("アスパラガス")).is_rejected \
 370        # Matching with NG keyword "アス"
 371        True
 372        """
 373        return super().apply(doc)
 374
 375
 376class DiscardAdultContentEn(NgWordsFilterEn):
 377    """
 378    英語のアダルトキーワード(および不適切語)を含む文書を破棄します.
 379    `dict_path` で指定したファイルから, キーワードのリストを得ます.
 380    ファイルは単語が改行で羅列されたテキストファイルです.
 381    デフォルトの`dict_path` は /hojichar/dict/adult_keywords_en.txt です.
 382    """
 383
 384    def __init__(
 385        self,
 386        dict_path: Union[str, PathLike] = BASE_PATH / "dict/adult_keywords_en.txt",
 387        *args: Any,
 388        **kwargs: Any,
 389    ) -> None:
 390        super().__init__(dict_path, *args, **kwargs)
 391
 392    def apply(self, doc: Document) -> Document:
 393        """
 394        >>> DiscardAdultContentEn().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected
 395        True
 396
 397        >>> DiscardAdultContentEn().apply(Document("hojichar")).is_rejected
 398        False
 399        """
 400        return super().apply(doc)
 401
 402
 403class DiscardDiscriminationContentJa(NgWordsFilterJa):
 404    """
 405    日本語の差別キーワード(および不適切語)を含む文書を破棄します.
 406    `dict_path` で指定したファイルから, キーワードのリストを得ます.
 407    ファイルは単語が改行で羅列されたテキストファイルです.
 408    デフォルトの`dict_path` は /hojichar/dict/discrimination_keywords_ja.txt です.
 409    """
 410
 411    def __init__(
 412        self,
 413        dict_path: Union[str, PathLike] = BASE_PATH / "dict/discrimination_keywords_ja.txt",
 414        *args: Any,
 415        **kwargs: Any,
 416    ):
 417        super().__init__(dict_path, *args, **kwargs)
 418
 419    def apply(self, doc: Document) -> Document:
 420        """
 421        >>> DiscardDiscriminationContentJa().\
 422            apply(Document("<TEST_STRING_OF_DISCRIMINATION_KEYWORD>")).is_rejected
 423        True
 424
 425        >>> DiscardDiscriminationContentJa().apply(Document("ほうじ茶")).is_rejected
 426        False
 427        """
 428        return super().apply(doc)
 429
 430
 431class DiscardViolenceContentJa(NgWordsFilterJa):
 432    """
 433    日本語の暴力・脅迫を示唆するキーワードを含む文書を破棄します.
 434    `dict_path` で指定したファイルから, キーワードのリストを得ます.
 435    ファイルは単語が改行で羅列されたテキストファイルです.
 436    デフォルトの`dict_path` は /hojichar/dict/violence_keywords_ja.txt です.
 437    """
 438
 439    def __init__(
 440        self,
 441        dict_path: Union[str, PathLike] = BASE_PATH / "dict/violence_keywords_ja.txt",
 442        *args: Any,
 443        **kwargs: Any,
 444    ) -> None:
 445        super().__init__(dict_path, *args, **kwargs)
 446
 447    def apply(self, doc: Document) -> Document:
 448        """
 449        >>> DiscardViolenceContentJa()\
 450            .apply(Document("<TEST_STRING_OF_VIOLENCE_KEYWORD>")).is_rejected
 451        True
 452
 453        >>> DiscardViolenceContentJa().apply(Document("ほうじ茶")).is_rejected
 454        False
 455        """
 456        return super().apply(doc)
 457
 458
 459class DiscardBBSComments(Filter):
 460    """
 461    正規表現 "BBS Pattern" に `max_allow_num` 回よりたくさんマッチする文書を破棄します.
 462    `max_allow_num` のデフォルト値は14です.
 463    正規表現 "BBS Pattern" は下記のリンクで検証可能です.
 464    https://regex101.com/r/ybQvL2/1
 465    """
 466
 467    def __init__(self, max_allowed_num: int = 14, *args: Any, **kwargs: Any) -> None:
 468        super().__init__(*args, **kwargs)
 469
 470        self.max_allowed_num = max_allowed_num
 471        self.keyword_pat = re.compile(
 472            r"\d{4}[年\.\-\/][\ ]*\d{1,2}[月\.\-\/][\ ]*\d{1,2}[日]*|コメント|SOLD OUT|レビュー|投稿|ページ|\([月火水木金土日]\)|質問|\d+話|楽天市場|-"  # noqa
 473        )
 474
 475    def apply(self, doc: Document) -> Document:
 476        """
 477        >>> DiscardBBSComments().apply(Document("楽天市場 質問 投稿 コメント レビュー "*3)).is_rejected
 478        True
 479
 480        >>> DiscardBBSComments().apply(Document("鏡餅")).is_rejected
 481        False
 482        """
 483        bbs_factor = self.keyword_pat.findall(doc.text)
 484        if len(bbs_factor) > self.max_allowed_num:
 485            doc.is_rejected = True
 486        return doc
 487
 488
 489class DiscardAds(Filter):
 490    """
 491    主に広告キーワードを`max_allow_num`より多く含む文書を破棄します.
 492    デフォルトで`max_allow_num` は14です.
 493    `dict_path` で指定したファイルから, 広告キーワードのリストを得ます.
 494    ファイルは単語が改行で羅列されたテキストファイルです.
 495    デフォルトの`dict_path` は /hojichar/dict/advertisement_keywords_ja.txt です.
 496    """
 497
 498    def __init__(
 499        self,
 500        dict_path: Union[str, PathLike] = BASE_PATH / "dict/advertisement_keywords_ja.txt",
 501        max_allowed_num: int = 14,
 502        *args: Any,
 503        **kwargs: Any,
 504    ):
 505        super().__init__(*args, **kwargs)
 506
 507        self.max_allow_num = max_allowed_num
 508        with open(dict_path, encoding="utf-8") as fp:
 509            ng_words = fp.read().split("\n")
 510        ng_words = [re.escape(w.strip()) for w in ng_words if not len(w) == 0]
 511        pat = r"|".join(ng_words)
 512        self.keyword_pat = re.compile(pat)
 513
 514    def apply(self, doc: Document) -> Document:
 515        """
 516        >>> DiscardAds().apply(Document("お問い合わせください 営業時間 よくある質問"*5)).is_rejected
 517        True
 518
 519        >>> DiscardAds().apply(Document("おはよう")).is_rejected
 520        False
 521        """
 522        ads_factor = self.keyword_pat.findall(doc.text)
 523        if len(ads_factor) > self.max_allow_num:
 524            doc.is_rejected = True
 525        return doc
 526
 527
 528class AcceptJapanese(Filter):
 529    """
 530    日本語でないドキュメントを破棄します. 日本語判定は次の手順で行われます.
 531        1. テキストを左から`lookup_size` (デフォルトで50字) 参照し,
 532        ひらがな・カタカナが存在すれば日本語と判定する.
 533    """
 534
 535    def __init__(self, lookup_size: int = 50, *args: Any, **kwargs: Any) -> None:
 536        super().__init__(*args, **kwargs)
 537
 538        self.lookup_size = lookup_size
 539        self.hiragana_katakana_pat = re.compile(r"[ぁ-んァ-ン]")
 540
 541    def apply(self, doc: Document) -> Document:
 542        """
 543        >>> AcceptJapanese().apply(Document("This is English document")).is_rejected
 544        True
 545
 546        >>> AcceptJapanese().apply(Document("a"*50 + "あ")).is_rejected
 547        True
 548
 549        >>> AcceptJapanese().apply(Document("ほうじ茶")).is_rejected
 550        False
 551        """
 552        if not self.hiragana_katakana_pat.search(doc.text[: self.lookup_size]):
 553            doc.is_rejected = True
 554        return doc
 555
 556
 557class DiscardRareKuten(Filter):
 558    """
 559    日本語でないドキュメントを破棄します. 日本語判定は次の手順で行われます
 560    ドキュメントを句点"。"で区切り, 平均文長が
 561    `max_avarage_sentence_length` より長い場合は破棄します.
 562    `max_avarage_sentence_length` のデフォルト値は100です.
 563    このフィルタは, 文章中の句点の割合が少なすぎるドキュメントを破棄します.
 564    """
 565
 566    def __init__(self, max_average_sentence_length: int = 100, *args: Any, **kwargs: Any) -> None:
 567        super().__init__(*args, **kwargs)
 568
 569        self.max_average_sentence_length = max_average_sentence_length
 570        self.kuten_pat = re.compile(r"。")
 571
 572    def apply(self, doc: Document) -> Document:
 573        """
 574        >>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよ。")).is_rejected
 575        False
 576        >>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよう。")).is_rejected
 577        True
 578        """
 579        kuten_lst = self.kuten_pat.findall(doc.text)
 580        min_kuten_num = len(doc.text) / self.max_average_sentence_length
 581        if len(kuten_lst) < min_kuten_num:
 582            doc.is_rejected = True
 583        return doc
 584
 585
 586class HeaderFooterTagsRemover(Filter):
 587    """
 588    ドキュメントの冒頭・末尾のトークンを調査し, ヘッダー・フッダー的な
 589    タグが存在していた場合, そのトークンを除去します.
 590
 591    このフィルタを通す前に, 事前にセンテンスレベルにトーカナイズしておいてください.
 592    このフィルタでは Document.token にのみ変更が加えられるので, 出力前 あるいは 下流フィルタで
 593    Document.text に変更を加える前にトークンをマージしておいてください.
 594    """
 595
 596    def __init__(
 597        self,
 598        dict_path: Union[str, PathLike] = BASE_PATH / "dict/header_footer_keywords_ja.txt",
 599        *args: Any,
 600        **kwargs: Any,
 601    ) -> None:
 602        super().__init__(*args, **kwargs)
 603
 604        with open(dict_path) as fp:
 605            keywords = fp.read().split("\n")
 606        keywords = [re.escape(w.strip()) for w in keywords if not len(w) == 0]
 607        self.keyword_pat = re.compile(r"|".join(keywords))
 608
 609    def apply(self, doc: Document) -> Document:
 610        if len(doc.tokens) == 0:
 611            return doc
 612
 613        lookup_size = 0
 614        if 1 <= len(doc.tokens) < 4:
 615            lookup_size = 1
 616        elif 4 <= len(doc.tokens) < 6:
 617            lookup_size = 2
 618        elif 6 <= len(doc.tokens):
 619            lookup_size = 3
 620
 621        for i in range(lookup_size):
 622            if self.should_drop_token(doc.tokens[i]):
 623                doc.tokens[i].is_rejected = True
 624            if self.should_drop_token(doc.tokens[-(i + 1)]):
 625                doc.tokens[i].is_rejected = True
 626
 627        return doc
 628
 629    def should_drop_token(self, token: Token) -> bool:
 630        """
 631        >>> HeaderFooterTagsRemover().should_drop_token(Token("<TEST_STRING_OF_KEYWORD>"))
 632        True
 633
 634        >>> HeaderFooterTagsRemover().should_drop_token(Token("ほうじ茶"))
 635        False
 636
 637        Comment.
 638        Original legacy code removed a pattern r"« _ | Main | _ »" .
 639        In the pattern, "|" is not escaped, so **ANY** string was eliminated.
 640        It seems unintended behavior, so I fix this.
 641        """
 642        if self.keyword_pat.match(token.text):
 643            return True
 644        else:
 645            return False
 646
 647
 648class MaskPersonalInformation(Filter):
 649    """
 650    ドキュメントに含まれる電話番号・電子メールアドレスを一部マスキングします.
 651    """
 652
 653    def __init__(self, *args: Any, **kwargs: Any) -> None:
 654        super().__init__(*args, **kwargs)
 655
 656        self.phone_pat = re.compile(
 657            r"((0|\+\d{1,3}[- ]?)(\d{2}[- ]?\d{4}[- ]?|\d[- ]?\d{4}[- ]?|\d{2}[- ]?\d{3}[- ]?|\d{3}[- ]?\d{2}[- ]?|\d{4}[- ]?\d{1}[- ]?))\d{4}"  # noqa
 658        )
 659        self.email_pat = re.compile(
 660            r"[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~.]+@[A-Za-z0-9!#$%&'*+\-/=?^_`{|}~.]+(\.[A-Za-z0-9\-]+)"  # noqa
 661        )
 662
 663    def apply(self, doc: Document) -> Document:
 664        """
 665        >>> MaskPersonalInformation()('06-1234-5678')
 666        '06-1234-XXXX'
 667        >>> MaskPersonalInformation()('075-123-4567')
 668        '075-123-XXXX'
 669        >>> MaskPersonalInformation()('0166-12-3456')
 670        '0166-12-XXXX'
 671        >>> MaskPersonalInformation()('09808-1-2345')
 672        '09808-1-XXXX'
 673        >>> MaskPersonalInformation()('090-1234-5678')
 674        '090-1234-XXXX'
 675        >>> MaskPersonalInformation()('0751234567')
 676        '075123XXXX'
 677        >>> MaskPersonalInformation()('08012345678')
 678        '0801234XXXX'
 679        >>> MaskPersonalInformation()('連絡は075-123-4567 まで')
 680        '連絡は075-123-XXXX まで'
 681        >>> MaskPersonalInformation()('+81-80-1234-5678')
 682        '+81-80-1234-XXXX'
 683        >>> MaskPersonalInformation()('+818012345678')
 684        '+81801234XXXX'
 685        >>> MaskPersonalInformation()('hogehoge@example.com')
 686        'xxxx@yyy.com'
 687        >>> MaskPersonalInformation()('何かあれば hogehoge@example.ne.jp まで連絡')
 688        '何かあれば xxxx@yyy.jp まで連絡'
 689        """
 690        text = self.phone_pat.sub(r"\1XXXX", doc.text)
 691        text = self.email_pat.sub(r"xxxx@yyy\1", text)
 692        doc.text = text
 693        return doc
 694
 695
 696class DiscardTooManyNouns(Filter):
 697    """
 698    [!CAUTION] This filter requires `fugashi` package. Please install it
 699    by `pip install 'hojichar[all]'`.
 700
 701    A filter that removes document with too many nouns in Japanese i.e.,
 702    documents such as advertisement, word salad, etc ...
 703    """
 704
 705    def __init__(self, threshold: float = 0.80, *args: Any, **kwargs: Any) -> None:
 706        """
 707        Args:
 708            threshold: document whose noun ratio is higher than this value will be discarded
 709            *args:
 710            **kwargs:
 711        """
 712        super().__init__(*args, **kwargs)
 713        assert (
 714            is_loaded_extras
 715        ), "fugashi is required for this filter. Try pip install 'hojichar[all]'"
 716
 717        self.threshold = threshold
 718        self.tagger = Tagger("-Owakati")
 719        assert (
 720            "unidic" in self.tagger.dictionary_info[0]["filename"]
 721        ), "MeCab dictionary must be unidic"
 722
 723    def apply(self, doc: Document) -> Document:
 724        """
 725        >>> DiscardTooManyNouns().apply(Document("自然言語処理大好き!")).is_rejected
 726        False
 727        >>> DiscardTooManyNouns().apply(Document("リンゴ・オレンジ・ミカン・バナナ セール中")).is_rejected
 728        True
 729        >>> DiscardTooManyNouns().apply(Document("今日の仙台朝市ではリンゴがセール中")).is_rejected
 730        False
 731        """
 732        # remove "補助記号" from part-of-speech statistics
 733        # because they often decrease the noun ratio,
 734        # e.g., the sentence "リンゴ・オレンジ・バナナ・" has 補助記号 ratio of 0.5
 735        # however, we don't want such sentence
 736        pos_count = Counter(
 737            w.feature.pos1 for w in self.tagger(doc.text) if w.feature.pos1 != "補助記号"
 738        )
 739        try:
 740            noun_ratio = pos_count["名詞"] / sum(pos_count.values())
 741        except ZeroDivisionError:
 742            noun_ratio = 0.0
 743        if noun_ratio >= self.threshold:
 744            doc.is_rejected = True
 745        return doc
 746
 747
 748class CharRepetitionRatioFilter(Filter):
 749    """
 750    文字Ngramの重なり率(文書中で高頻度文字Ngramが占める割合)を計算して, 重なりの大きいものを除去します.
 751    名詞の連続からなるような広告テキストを取り除くのに有効です.
 752
 753    実装は, BigScience で採用されていた前処理を参考にしています.
 754    元実装: https://github.com/bigscience-workshop/data-preparation/blob/9d0588419073cc5bf0fb92b58f37f2a1016572c3/preprocessing/training/01b_oscar_cleaning_and_filtering/filtering.py#L425-L453  # noqa: E501
 755
 756    「高頻度文字Ngram」は、sqrt(ユニークなNgramの総数)によって求めていますが,
 757    これは文書長の影響を軽減するためだとされています.
 758
 759    掲示板のテキストが引っかかりやすい傾向があります.
 760    13: 名無しさん@実況で競馬板アウト 2019/08/18(日) 15:28:46.10 ID:eBvZg8h+0
 761    的なものが高頻度で登場するため、文字Ngramの重なり率も高くなってしまう
 762    """
 763
 764    def __init__(
 765        self, threshold: float = 0.33, ngram_size: int = 5, *args: Any, **kwargs: Any
 766    ) -> None:
 767        """
 768
 769        Args:
 770            threshold: document with character repetition ratio higher than this value will be discarded
 771            ngram_size: character ngram size. Larger value will decrease the false positive of long documents
 772            *args:
 773            **kwargs:
 774        """  # noqa: E501
 775
 776        super().__init__(*args, **kwargs)
 777        self.threshold = threshold
 778        self.ngram_size = ngram_size
 779
 780    def apply(self, doc: Document) -> Document:
 781        ratio = self.compute_character_repetition_ratio(doc.text, self.ngram_size)
 782        if ratio >= self.threshold:
 783            doc.is_rejected = True
 784        return doc
 785
 786    @staticmethod
 787    def compute_character_repetition_ratio(
 788        document: str, character_repetition_length: int
 789    ) -> float:
 790        def get_freq_character_ngrams(document: str, n: int) -> Dict[str, int]:
 791            character_ngrams: List[str] = [
 792                document[i : i + n] for i in range(len(document) - n + 1)
 793            ]
 794            freq_character_ngrams_dict: Dict[str, int] = {}
 795            for character_ngram in character_ngrams:
 796                freq_character_ngrams_dict[character_ngram] = (
 797                    freq_character_ngrams_dict.get(character_ngram, 0) + 1
 798                )
 799            return freq_character_ngrams_dict
 800
 801        freq_character_ngrams_dict = get_freq_character_ngrams(
 802            document, character_repetition_length
 803        )
 804        if len(freq_character_ngrams_dict) == 0:
 805            return 0.0
 806        freq_character_ngrams: List[int] = list(freq_character_ngrams_dict.values())
 807        freq_character_ngrams = sorted(freq_character_ngrams, reverse=True)
 808        val_one = len([el for el in freq_character_ngrams if el == 1])
 809        num_rep_character_ngrams = min(
 810            int(np.sqrt(len(freq_character_ngrams))),
 811            len(freq_character_ngrams) - val_one,
 812        )
 813        character_repetition_ratio = sum(freq_character_ngrams[:num_rep_character_ngrams]) / sum(
 814            freq_character_ngrams
 815        )
 816        return character_repetition_ratio
 817
 818
 819class WordRepetitionRatioFilter(Filter):
 820    """
 821    [!CAUTION] This filter requires `fugashi` package. Please install it
 822    by `pip install 'hojichar[all]'`.
 823
 824    単語Ngramの重なり率(文書中で重複する単語Ngramが占める割合)を計算して、重なりの大きいものを弾くためのフィルタ.
 825    BigScienceで採用されていた前処理を参考にしている.
 826
 827    名詞が連打されているような広告テキストを取り除くのに有効な様子
 828    まともな文書がたまたま2回繰り返されている場合もあり、これを取り除いて良いのかは分からない
 829    例:
 830    "ウェブ\n本文: ニコンの上昇率16%超える、今3月期は経常76%の大幅増益見込む(ニコン) 2013年05月10日[minkabu PRESS] - みんなの株式 (みんかぶ)\n2013/05/10(10:57)
 831    ニコン<7731.T>が急騰、寄り付き直後に前日比355円高の2537円まで買い上げ
 832    られ、上昇率は16%を超えた。外国為替市場で円が1ドル100円台、1ユーロ131円台に入るなど急速に円安が進み、輸出株が軒並み高になる
 833    なか、9日取引終了後に発表した前年3月期決算で、今3月期は2ケタ近い増収で大幅増益を見込んだことが買い気を強めさせた。連結売上
 834    高は前期比9.8%増の1兆1100億円、経常利益75.8%増の850億円を予想。前期は半導体、電子部品の低迷が足かせになり、2ケタ増収ながら
 835    経常46%の大幅減益になったが、レンズ交換式デジタルカメラの拡大や液晶ディスプレイの回復で収益が急回復する。ニコンの株価は10時
 836    56分現在2491円(△309円)出所:株経通信(株式会社みんかぶ)\n2013/05/10 - ニコン(7731) の関連ニュース。 ニコン<7731.T>が急騰、寄
 837    り付き直後に前日比355円高の2537円まで買い上げられ、上昇率は16%を超えた。外国為替市場で円が1ドル100円台、1ユーロ131円台に入
 838    るなど急速に円安が進み、輸出株が軒並み高になるなか、9日取引終了後に発表した前年3月期決算で、今3月期は2ケタ近い増収で大幅増
 839    益を見込んだことが買い気を強めさせた。連結売上高は前期比9.8%増の1兆1100億円、経常利益75.8%増の850億円を予想。前期は半導体、
 840    電子部品の低迷が足かせになり、2ケタ増収ながら経常46%の大幅減益になったが、レンズ交換式デジタルカメラの拡大や液晶ディスプレ
 841    イの回復で収益が急回"
 842    """  # noqa: E501
 843
 844    def __init__(
 845        self, threshold: float = 0.40, ngram_size: int = 7, *args: Any, **kwargs: Any
 846    ) -> None:
 847        """
 848
 849        Args:
 850            threshold: document whose character repetition ratio is higher than this value will be discarded
 851            ngram_size: character ngram size. Larger value will decrease the false positive of long documents
 852            *args:
 853            **kwargs:
 854        """  # noqa: E501
 855        super().__init__(*args, **kwargs)
 856        assert (
 857            is_loaded_extras
 858        ), "fugashi is required for this filter. Try pip install 'hojichar[all]'"
 859
 860        self.threshold = threshold
 861        self.ngram_size = ngram_size
 862        self.tagger = Tagger("-Owakati")
 863
 864    def apply(self, doc: Document) -> Document:
 865        ratio = self.compute_word_repetition_ratio(doc.text, self.ngram_size)
 866        if ratio >= self.threshold:
 867            doc.is_rejected = True
 868        return doc
 869
 870    def compute_word_repetition_ratio(self, document: str, word_repetition_length: int) -> float:
 871        def get_freq_word_ngrams(document: str, n: int) -> Dict[str, int]:
 872            # tokenizing given document
 873            words = [w.surface for w in self.tagger(document)]
 874            word_ngrams = [" ".join(words[i : i + n]) for i in range(len(words) - n + 1)]
 875            freq_word_ngrams: Dict[str, int] = {}
 876            for word_ngram in word_ngrams:
 877                freq_word_ngrams[word_ngram] = freq_word_ngrams.get(word_ngram, 0) + 1
 878            return freq_word_ngrams
 879
 880        freq_word_ngrams_dict = get_freq_word_ngrams(document, word_repetition_length)
 881        if len(freq_word_ngrams_dict) == 0:
 882            return 0
 883        freq_word_ngrams = list(freq_word_ngrams_dict.values())
 884        word_repetition_ratio = sum(freq for freq in freq_word_ngrams if freq > 1) / sum(
 885            freq_word_ngrams
 886        )
 887
 888        return word_repetition_ratio
 889
 890
 891class DiscardTooManySpecialToken(Filter):
 892    """
 893    [!CAUTION] This filter requires `emoji` package. Please install it
 894    by `pip install 'hojichar[all]'`.
 895
 896    句読点を含む記号、空白、絵文字、その他特殊な文字を一定の割合以上含むような文書を取り除くためのフィルタ
 897    元実装: BigScience https://github.com/bigscience-workshop/data-preparation/blob/9d0588419073cc5bf0fb92b58f37f2a1016572c3/preprocessing/training/01b_oscar_cleaning_and_filtering/parameters_filtering.py#L5-L16  # noqa: E501
 898    """
 899
 900    def __init__(self, threshold: float = 0.4, *args: Any, **kwargs: Any) -> None:
 901        """
 902
 903        Args:
 904            threshold: document whose special token ratio is higher than this value will be discarded
 905            *args:
 906            **kwargs:
 907        """  # noqa: E501
 908        super().__init__(*args, **kwargs)
 909
 910        # digits are not regarded as special tokens
 911        # otherwise many false positives are made, i.e., good documents discarded
 912        main_special_characters = string.punctuation + string.whitespace  # + string.digits
 913        other_special_characters = (
 914            "’ “— ™ – •‘œ    ˜ ‚ƒ„’“”–▬…✦�­£​•€«»°·═"
 915            "×士^˘⇓()§″′´¿−±∈¢ø‚„½¼¾¹²³―⁃,ˌ¸‹›ʺˈʻ¦‐⠀‰……‑≤≥‖"
 916            "◆●■►▼▲▴∆▻¡★☆✱ːº。¯˜¥ɪ≈†:⁄♡✓⊕․.⋅÷1‟;،、¨ाাी्े◦˚"
 917            "゜ʼ≖ʼ¤℃√!?【】‿∞➤~πه۩☛₨➩☻๑٪♥ıॽ《‘©﴿٬?▷Г♫∟™ª₪®「—❖"
 918            "」﴾》�"
 919        )
 920
 921        en_emoji = emoji.EMOJI_DATA.keys()
 922
 923        special_characters_default = set(main_special_characters + other_special_characters)
 924        special_characters_default.update(en_emoji)
 925        self.special_characters = special_characters_default
 926
 927        self.threshold = threshold
 928
 929    def _compute_special_characters_ratio(self, text: str) -> float:
 930        if len(text) == 0:
 931            return 0
 932
 933        special_characters_ratio = len(
 934            [char for char in text if char in self.special_characters]
 935        ) / len(text)
 936        return special_characters_ratio
 937
 938    def apply(self, doc: Document) -> Document:
 939        special_characters_ratio = self._compute_special_characters_ratio(doc.text)
 940
 941        if special_characters_ratio > self.threshold:
 942            doc.is_rejected = True
 943        return doc
 944
 945
 946class SingleCharacterRepetitionFilter(Filter):
 947    """
 948    単一文字が大量に繰り返されているような文書を取り除くためのフィルタ
 949    そのような文書はノイズである可能性が高いため
 950    参考: BigScienceプロジェクトによると、oscarデータセットの中にバックスラッシュだけを2M個含むような文書が含まれていたらしい
 951    https://github.com/bigscience-workshop/bigscience/blob/master/train/tr8-104B-wide/chronicles.md#2m-backslash-only-samples-in-our-dataset  # noqa: E501
 952    """
 953
 954    def __init__(
 955        self,
 956        threshold: int = 200,
 957        *args: Any,
 958        **kwargs: Any,
 959    ) -> None:
 960        """
 961        Args:
 962            threshold: The document is removed if character is repeated for this value or more
 963            *args:
 964            **kwargs:
 965        """
 966        super().__init__(*args, **kwargs)
 967        self.threshold = threshold
 968
 969    def _is_repeat_contained(self, text: str) -> bool:
 970        groups = groupby(text)
 971        is_repeat_contained = any(sum(1 for _ in group) >= self.threshold for _, group in groups)
 972        return is_repeat_contained
 973
 974    def apply(self, doc: Document) -> Document:
 975        if self._is_repeat_contained(doc.text):
 976            doc.is_rejected = True
 977        return doc
 978
 979
 980class DiscardTooManyEndingEllipsis(Filter):
 981    """
 982    ellipsisで終わるような行が大量に含まれるような文書を取り除くためのフィルタです.
 983    ellipsisとしては ... と … を用いている
 984    同様のフィルタが RedPajama v2で用いられています.
 985
 986    例として, 以下のような文書を検知します.
 987    ```
 988    ペアーズは女性、という驚愕の過食が出ているのをごアラサーですか。時代から付...
 989    バツイチアラフォー 婚活ち女性の特徴と子持な付...
 990    ```
 991
 992    デフォルトではしきい値を0.7としているが, これはC4から0.1%を削るような設定であり、
 993    precisionを重視した設定です.
 994    """
 995
 996    def __init__(
 997        self,
 998        threshold: float = 0.7,
 999        *args: Any,
1000        **kwargs: Any,
1001    ) -> None:
1002        """
1003        Args:
1004            threshold: The document is removed if ratio of lines ending with ellipsis is higher than this value
1005            *args:
1006            **kwargs:
1007        """  # noqa: E501
1008        super().__init__(*args, **kwargs)
1009        self.threshold = threshold
1010        self.ellipsis_pattern = re.compile(r"(\.{3}|…)\n")  # matches ...\n and …\n
1011
1012    def apply(self, doc: Document) -> Document:
1013        ellipsis_count = len(self.ellipsis_pattern.findall(doc.text))
1014        newline_count = max(doc.text.count("\n"), 1)  # avoid zero division
1015        ellipsis_ratio = ellipsis_count / newline_count
1016
1017        if ellipsis_ratio > self.threshold:
1018            doc.is_rejected = True
1019        return doc
1020
1021
1022class DiscardTooShortLines(Filter):
1023    """
1024    短い行を大量に含む文書を捨てるためのフィルタです.
1025
1026    メニューバーやパンくずリストのような要素を大量に含む文書を取り除くのに有効です.
1027    """
1028
1029    def __init__(self, threshold: float = 0.5, *args: Any, **kwargs: Any) -> None:
1030        """
1031        Args:
1032            threshold: The document is removed if the ratio of short (<10 chars) lines are more than this value.
1033            *args:
1034            **kwargs:
1035        """  # noqa: E501
1036        super().__init__(*args, **kwargs)
1037        self.threshold = threshold
1038        # この値は適当に決め打ち
1039        self.minimum_line_length = 10
1040
1041    def apply(self, doc: Document) -> Document:
1042        lines = [len(x) for x in doc.text.split("\n")]
1043        short_lines = [x for x in lines if x <= self.minimum_line_length]
1044        if (len(short_lines) / len(lines)) > self.threshold:
1045            doc.is_rejected = True
1046        return doc
class ExampleHojiChar(hojichar.core.filter_interface.Filter):
32class ExampleHojiChar(Filter):
33    """基本的なフィルタの実装例です. 末尾に'<hojichar>'を追加します."""
34
35    def apply(self, document: Document) -> Document:
36        """
37        >>> ExampleHojiChar()("hello, world")
38        'hello, world<hojichar>'
39        """
40        document.text += "<hojichar>"
41        return document

基本的なフィルタの実装例です. 末尾に''を追加します.

def apply( self, document: hojichar.core.models.Document) -> hojichar.core.models.Document:
35    def apply(self, document: Document) -> Document:
36        """
37        >>> ExampleHojiChar()("hello, world")
38        'hello, world<hojichar>'
39        """
40        document.text += "<hojichar>"
41        return document
>>> ExampleHojiChar()("hello, world")
'hello, world<hojichar>'
class ExampleDiscardDocumentContainKeyword(hojichar.core.filter_interface.Filter):
44class ExampleDiscardDocumentContainKeyword(Filter):
45    """特定のキーワードを持つドキュメントを破棄するようなフィルタの実装例です."""
46
47    def __init__(self, keyword: str, *args: Any, **kwargs: Any) -> None:
48        super().__init__(*args, **kwargs)
49        self.keyword = keyword
50
51    def apply(self, document: Document) -> Document:
52        """
53        >>> ExampleDiscardDocumentContainKeyword("バカ").apply(Document("あいつはバカだ")).is_rejected
54        True
55        """
56        if self.keyword in document.text:
57            document.is_rejected = True
58        return document

特定のキーワードを持つドキュメントを破棄するようなフィルタの実装例です.

ExampleDiscardDocumentContainKeyword(keyword: str, *args: Any, **kwargs: Any)
47    def __init__(self, keyword: str, *args: Any, **kwargs: Any) -> None:
48        super().__init__(*args, **kwargs)
49        self.keyword = keyword

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, document: hojichar.core.models.Document) -> hojichar.core.models.Document:
51    def apply(self, document: Document) -> Document:
52        """
53        >>> ExampleDiscardDocumentContainKeyword("バカ").apply(Document("あいつはバカだ")).is_rejected
54        True
55        """
56        if self.keyword in document.text:
57            document.is_rejected = True
58        return document
>>> ExampleDiscardDocumentContainKeyword("バカ").apply(Document("あいつはバカだ")).is_rejected
True
class Identity(hojichar.core.filter_interface.Filter):
61class Identity(Filter):
62    """何も変化を加えないフィルタです. テスト・デバッグに用いられます."""
63
64    def apply(self, document: Document) -> Document:
65        return document

何も変化を加えないフィルタです. テスト・デバッグに用いられます.

def apply( self, document: hojichar.core.models.Document) -> hojichar.core.models.Document:
64    def apply(self, document: Document) -> Document:
65        return document

Definition of filter behavior.

In this method, the filter will modify document.text, or set document.is_rejected = True to discard the document.

Do not define a filter that changes both document.text and document.token

Parameters

document : Document Input document

Returns

Document Processed Document

class DiscardAll(hojichar.core.filter_interface.Filter):
68class DiscardAll(Filter):
69    """
70    すべてのドキュメントを破棄するフィルタです.
71    テスト・デバッグに用いられます.
72    """
73
74    def apply(self, document: Document) -> Document:
75        document.is_rejected = True
76        return document

すべてのドキュメントを破棄するフィルタです. テスト・デバッグに用いられます.

def apply( self, document: hojichar.core.models.Document) -> hojichar.core.models.Document:
74    def apply(self, document: Document) -> Document:
75        document.is_rejected = True
76        return document

Definition of filter behavior.

In this method, the filter will modify document.text, or set document.is_rejected = True to discard the document.

Do not define a filter that changes both document.text and document.token

Parameters

document : Document Input document

Returns

Document Processed Document

class ApplyDiscard(hojichar.core.filter_interface.Filter):
 79class ApplyDiscard(Filter):
 80    """
 81    上流フィルタで破棄された`Document`を空文字列にします.
 82
 83    `Document.is_rejected=True` の ドキュメントは無視されるため,
 84    このフィルタを `Compose` のコンストラクタに渡しても動作しません.
 85    このフィルタは主に`Compose` 内部や, `discard_filtered=False` を指定
 86    したデバッグ時などに利用されます.
 87    """
 88
 89    def __init__(self, *args: Any, **kwargs: Any) -> None:
 90        super().__init__(*args, **kwargs)
 91
 92    def apply(self, document: Document) -> Document:
 93        """
 94        >>> ApplyDiscard().apply(Document(text="hello", is_rejected=True)).text
 95        ''
 96        """
 97        if document.is_rejected:
 98            document.text = ""
 99
100        return document

上流フィルタで破棄されたDocumentを空文字列にします.

Document.is_rejected=True の ドキュメントは無視されるため, このフィルタを Compose のコンストラクタに渡しても動作しません. このフィルタは主にCompose 内部や, discard_filtered=False を指定 したデバッグ時などに利用されます.

ApplyDiscard(*args: Any, **kwargs: Any)
89    def __init__(self, *args: Any, **kwargs: Any) -> None:
90        super().__init__(*args, **kwargs)

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, document: hojichar.core.models.Document) -> hojichar.core.models.Document:
 92    def apply(self, document: Document) -> Document:
 93        """
 94        >>> ApplyDiscard().apply(Document(text="hello", is_rejected=True)).text
 95        ''
 96        """
 97        if document.is_rejected:
 98            document.text = ""
 99
100        return document
>>> ApplyDiscard().apply(Document(text="hello", is_rejected=True)).text
''
class Sleep(hojichar.core.filter_interface.Filter):
103class Sleep(Filter):
104    """
105    デバッグ用のフィルタです. 指定秒スリープします.
106    """
107
108    def __init__(self, time: float = 1.0, *args: Any, **kwargs: Any) -> None:
109        super().__init__(*args, **kwargs)
110        self.time = time
111
112    def apply(self, document: Document) -> Document:
113        """
114        >>> Sleep(0.1)('hello')  # After 0.1 seconds,
115        'hello'
116        """
117        time.sleep(self.time)
118        return document

デバッグ用のフィルタです. 指定秒スリープします.

Sleep(time: float = 1.0, *args: Any, **kwargs: Any)
108    def __init__(self, time: float = 1.0, *args: Any, **kwargs: Any) -> None:
109        super().__init__(*args, **kwargs)
110        self.time = time

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, document: hojichar.core.models.Document) -> hojichar.core.models.Document:
112    def apply(self, document: Document) -> Document:
113        """
114        >>> Sleep(0.1)('hello')  # After 0.1 seconds,
115        'hello'
116        """
117        time.sleep(self.time)
118        return document
>>> Sleep(0.1)('hello')  # After 0.1 seconds,
'hello'
class DocumentNormalizer(hojichar.core.filter_interface.Filter):
121class DocumentNormalizer(Filter):
122    """
123    Unicode の正規化をします.
124    """
125
126    def __init__(self, *args: Any, **kwargs: Any) -> None:
127        super().__init__(*args, **kwargs)
128
129    def apply(self, document: Document) -> Document:
130        document.text = unicodedata.normalize("NFKC", document.text)
131        return document

Unicode の正規化をします.

DocumentNormalizer(*args: Any, **kwargs: Any)
126    def __init__(self, *args: Any, **kwargs: Any) -> None:
127        super().__init__(*args, **kwargs)

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, document: hojichar.core.models.Document) -> hojichar.core.models.Document:
129    def apply(self, document: Document) -> Document:
130        document.text = unicodedata.normalize("NFKC", document.text)
131        return document

Definition of filter behavior.

In this method, the filter will modify document.text, or set document.is_rejected = True to discard the document.

Do not define a filter that changes both document.text and document.token

Parameters

document : Document Input document

Returns

Document Processed Document

class JSONLoader(hojichar.core.filter_interface.Filter):
134class JSONLoader(Filter):
135    """
136    テキストを Json として解釈し, `key` で指定した要素を文字列として
137    doument に格納します.デフォルトの `key` は 'text' です.
138
139    Json の読み込み, あるいは `key` の読み込みに失敗した際には例外を送出します.
140    これらを無視する場合は, `ignore=True` にします. その際, 読み込みに失敗
141    したドキュメントは破棄されます.
142    """
143
144    def __init__(
145        self,
146        key: str = "text",
147        ignore: bool = False,
148        extra_keys: Optional[List[str]] = None,
149        *args: Any,
150        **kwargs: Any,
151    ) -> None:
152        super().__init__(*args, **kwargs)
153        self.key = key
154        self.ignore = ignore
155        self.extra_keys = extra_keys
156
157    def apply(self, document: Document) -> Document:
158        """
159        >>> JSONLoader()( '{"text": "hello, world", "words": 2}' )
160        'hello, world'
161
162        >>> JSONLoader()( '{"text": hello, world ....' ) # Broken JSON
163        Traceback (most recent call last):
164            ...
165        json.decoder.JSONDecodeError: Expecting value: line 1 column 10 (char 9)
166
167        >>> JSONLoader()( '{"words": 2}' )
168        Traceback (most recent call last):
169            ...
170        KeyError: 'text'
171
172        >>> JSONLoader(ignore=True).apply(Document('{"text": hello, world ....' )).is_rejected
173        True
174        """
175        try:
176            data = json.loads(document.text)
177            document.text = str(data[self.key])
178            if self.extra_keys is not None:
179                document.extras = {key: data[key] for key in self.extra_keys if key in data}
180        except Exception as e:
181            logger.error(f"Failed to parsing in JSONLoader. Input document: \n{document.text}")
182            if self.ignore:
183                document.is_rejected = True
184                return document
185            else:
186                raise e
187
188        return document

テキストを Json として解釈し, key で指定した要素を文字列として doument に格納します.デフォルトの key は 'text' です.

Json の読み込み, あるいは key の読み込みに失敗した際には例外を送出します. これらを無視する場合は, ignore=True にします. その際, 読み込みに失敗 したドキュメントは破棄されます.

JSONLoader( key: str = 'text', ignore: bool = False, extra_keys: Optional[List[str]] = None, *args: Any, **kwargs: Any)
144    def __init__(
145        self,
146        key: str = "text",
147        ignore: bool = False,
148        extra_keys: Optional[List[str]] = None,
149        *args: Any,
150        **kwargs: Any,
151    ) -> None:
152        super().__init__(*args, **kwargs)
153        self.key = key
154        self.ignore = ignore
155        self.extra_keys = extra_keys

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, document: hojichar.core.models.Document) -> hojichar.core.models.Document:
157    def apply(self, document: Document) -> Document:
158        """
159        >>> JSONLoader()( '{"text": "hello, world", "words": 2}' )
160        'hello, world'
161
162        >>> JSONLoader()( '{"text": hello, world ....' ) # Broken JSON
163        Traceback (most recent call last):
164            ...
165        json.decoder.JSONDecodeError: Expecting value: line 1 column 10 (char 9)
166
167        >>> JSONLoader()( '{"words": 2}' )
168        Traceback (most recent call last):
169            ...
170        KeyError: 'text'
171
172        >>> JSONLoader(ignore=True).apply(Document('{"text": hello, world ....' )).is_rejected
173        True
174        """
175        try:
176            data = json.loads(document.text)
177            document.text = str(data[self.key])
178            if self.extra_keys is not None:
179                document.extras = {key: data[key] for key in self.extra_keys if key in data}
180        except Exception as e:
181            logger.error(f"Failed to parsing in JSONLoader. Input document: \n{document.text}")
182            if self.ignore:
183                document.is_rejected = True
184                return document
185            else:
186                raise e
187
188        return document
>>> JSONLoader()( '{"text": "hello, world", "words": 2}' )
'hello, world'
>>> JSONLoader()( '{"text": hello, world ....' ) # Broken JSON
Traceback (most recent call last):
    ...
json.decoder.JSONDecodeError: Expecting value: line 1 column 10 (char 9)
>>> JSONLoader()( '{"words": 2}' )
Traceback (most recent call last):
    ...
KeyError: 'text'
>>> JSONLoader(ignore=True).apply(Document('{"text": hello, world ....' )).is_rejected
True
class JSONDumper(hojichar.core.filter_interface.Filter):
191class JSONDumper(Filter):
192    """
193    Document.text の文字列を json に変換します.
194    必要に応じ Document のメタデータを付与します. これはドキュメントの破棄事由が含まれ、偽陽性の分析に有効です。
195    デフォルトで `skip_rejected` が `False` にセットされており、Document の破棄フラグにかかわらず
196    処理されます。
197    """
198
199    def __init__(
200        self,
201        dump_reason: bool = False,
202        p: float = 1,
203        skip_rejected: bool = False,
204        *args: Any,
205        **kwargs: Any,
206    ) -> None:
207        """
208        Args:
209            dump_reason (bool, optional): `is_rejected`, `reason` エントリをダンプします. Defaults to False.
210            p (float, optional): Apply probability. Defaults to 1.
211            skip_rejected (bool, optional): 破棄済みサンプルを排除しません.
212        """
213        super().__init__(p, skip_rejected, *args, **kwargs)
214        self.dump_reason = dump_reason
215
216    def apply(self, document: Document) -> Document:
217        """
218        >>> JSONDumper()("hojichar")
219        '{"text": "hojichar"}'
220        """
221        text = document.text
222        if self.dump_reason:
223            document.text = json.dumps(
224                {
225                    "text": text,
226                    "is_rejected": document.is_rejected,
227                    "reason": document.reject_reason,
228                },
229                ensure_ascii=False,
230            )
231        else:
232            document.text = json.dumps({"text": text}, ensure_ascii=False)
233        return document

Document.text の文字列を json に変換します. 必要に応じ Document のメタデータを付与します. これはドキュメントの破棄事由が含まれ、偽陽性の分析に有効です。 デフォルトで skip_rejectedFalse にセットされており、Document の破棄フラグにかかわらず 処理されます。

JSONDumper( dump_reason: bool = False, p: float = 1, skip_rejected: bool = False, *args: Any, **kwargs: Any)
199    def __init__(
200        self,
201        dump_reason: bool = False,
202        p: float = 1,
203        skip_rejected: bool = False,
204        *args: Any,
205        **kwargs: Any,
206    ) -> None:
207        """
208        Args:
209            dump_reason (bool, optional): `is_rejected`, `reason` エントリをダンプします. Defaults to False.
210            p (float, optional): Apply probability. Defaults to 1.
211            skip_rejected (bool, optional): 破棄済みサンプルを排除しません.
212        """
213        super().__init__(p, skip_rejected, *args, **kwargs)
214        self.dump_reason = dump_reason

Args: dump_reason (bool, optional): is_rejected, reason エントリをダンプします. Defaults to False. p (float, optional): Apply probability. Defaults to 1. skip_rejected (bool, optional): 破棄済みサンプルを排除しません.

def apply( self, document: hojichar.core.models.Document) -> hojichar.core.models.Document:
216    def apply(self, document: Document) -> Document:
217        """
218        >>> JSONDumper()("hojichar")
219        '{"text": "hojichar"}'
220        """
221        text = document.text
222        if self.dump_reason:
223            document.text = json.dumps(
224                {
225                    "text": text,
226                    "is_rejected": document.is_rejected,
227                    "reason": document.reject_reason,
228                },
229                ensure_ascii=False,
230            )
231        else:
232            document.text = json.dumps({"text": text}, ensure_ascii=False)
233        return document
>>> JSONDumper()("hojichar")
'{"text": "hojichar"}'
class DocumentLengthFilter(hojichar.core.filter_interface.Filter):
236class DocumentLengthFilter(Filter):
237    """
238    `min_doc_len`, `max_doc_len` で指定した上限・下限の範囲内にないドキュメントを破棄します.
239    デフォルトでは 200字 以上 50000字以内のテキストが受理されます.
240    """
241
242    def __init__(
243        self,
244        min_doc_len: Optional[int] = None,
245        max_doc_len: Optional[int] = None,
246        *args: Any,
247        **kwargs: Any,
248    ) -> None:
249        super().__init__(*args, **kwargs)
250
251        self.min_doc_len = min_doc_len
252        self.max_doc_len = max_doc_len
253
254    def apply(self, doc: Document) -> Document:
255        """
256        >>> DocumentLengthFilter(min_doc_len=5).apply(Document("1234")).is_rejected
257        True
258        """
259        doc_len = len(doc.text)
260        if self.min_doc_len is not None:
261            if doc_len < self.min_doc_len:
262                doc.is_rejected = True
263        if self.max_doc_len is not None:
264            if self.max_doc_len < doc_len:
265                doc.is_rejected = True
266        return doc

min_doc_len, max_doc_len で指定した上限・下限の範囲内にないドキュメントを破棄します. デフォルトでは 200字 以上 50000字以内のテキストが受理されます.

DocumentLengthFilter( min_doc_len: Optional[int] = None, max_doc_len: Optional[int] = None, *args: Any, **kwargs: Any)
242    def __init__(
243        self,
244        min_doc_len: Optional[int] = None,
245        max_doc_len: Optional[int] = None,
246        *args: Any,
247        **kwargs: Any,
248    ) -> None:
249        super().__init__(*args, **kwargs)
250
251        self.min_doc_len = min_doc_len
252        self.max_doc_len = max_doc_len

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
254    def apply(self, doc: Document) -> Document:
255        """
256        >>> DocumentLengthFilter(min_doc_len=5).apply(Document("1234")).is_rejected
257        True
258        """
259        doc_len = len(doc.text)
260        if self.min_doc_len is not None:
261            if doc_len < self.min_doc_len:
262                doc.is_rejected = True
263        if self.max_doc_len is not None:
264            if self.max_doc_len < doc_len:
265                doc.is_rejected = True
266        return doc
>>> DocumentLengthFilter(min_doc_len=5).apply(Document("1234")).is_rejected
True
class NgWordsFilterJa(hojichar.core.filter_interface.Filter):
269class NgWordsFilterJa(Filter):
270    """
271    日本語のNGワード(および不適切語)を含む文書を破棄します.
272    `dict_path` で指定したファイルから, キーワードのリストを得ます.
273    ファイルは単語が改行で羅列されたテキストファイルです.
274
275    `ignore_confused` を `True` にすると,
276    偽陽性を軽減するために, カタカナのNGワードは前後にカタカナが無い場合のみNG判定されます.
277    デフォルト値は `False` です.
278    """
279
280    def __init__(
281        self,
282        dict_path: Union[str, PathLike],
283        ignore_confused: bool = False,
284        *args: Any,
285        **kwargs: Any,
286    ) -> None:
287        super().__init__(*args, **kwargs)
288
289        with open(dict_path, encoding="utf-8") as fp:
290            ng_words = fp.read().split("\n")
291        ng_words = [w.strip() for w in ng_words if not len(w) == 0]
292
293        if ignore_confused:
294            words_katakana = []
295            words_not_katakana = []
296            for w in ng_words:
297                if re.fullmatch(r"[ァ-ヴー]+", w):
298                    words_katakana.append(re.escape(w))
299                else:
300                    words_not_katakana.append(re.escape(w))
301            katakana_pat = "|".join(words_katakana)
302            katakana_pat = rf"(?<![ァ-ヴー])({katakana_pat})(?![ァ-ヴー])"
303            pat = "|".join(words_not_katakana) + "|" + katakana_pat
304            self.keyword_pat = re.compile(pat)
305        else:
306            ng_words = [re.escape(w) for w in ng_words]
307            pat = "|".join(ng_words)
308            self.keyword_pat = re.compile(pat)
309
310    def apply(self, doc: Document) -> Document:
311        regex_match = self.keyword_pat.search(doc.text)
312        if regex_match:
313            doc.is_rejected = True
314            self.matched_text = regex_match.group()
315            self.matched_text_neighbor = doc.text[
316                regex_match.start() - 20 : regex_match.end() + 20
317            ]
318
319        return doc

日本語のNGワード(および不適切語)を含む文書を破棄します. dict_path で指定したファイルから, キーワードのリストを得ます. ファイルは単語が改行で羅列されたテキストファイルです.

ignore_confusedTrue にすると, 偽陽性を軽減するために, カタカナのNGワードは前後にカタカナが無い場合のみNG判定されます. デフォルト値は False です.

NgWordsFilterJa( dict_path: Union[str, os.PathLike], ignore_confused: bool = False, *args: Any, **kwargs: Any)
280    def __init__(
281        self,
282        dict_path: Union[str, PathLike],
283        ignore_confused: bool = False,
284        *args: Any,
285        **kwargs: Any,
286    ) -> None:
287        super().__init__(*args, **kwargs)
288
289        with open(dict_path, encoding="utf-8") as fp:
290            ng_words = fp.read().split("\n")
291        ng_words = [w.strip() for w in ng_words if not len(w) == 0]
292
293        if ignore_confused:
294            words_katakana = []
295            words_not_katakana = []
296            for w in ng_words:
297                if re.fullmatch(r"[ァ-ヴー]+", w):
298                    words_katakana.append(re.escape(w))
299                else:
300                    words_not_katakana.append(re.escape(w))
301            katakana_pat = "|".join(words_katakana)
302            katakana_pat = rf"(?<![ァ-ヴー])({katakana_pat})(?![ァ-ヴー])"
303            pat = "|".join(words_not_katakana) + "|" + katakana_pat
304            self.keyword_pat = re.compile(pat)
305        else:
306            ng_words = [re.escape(w) for w in ng_words]
307            pat = "|".join(ng_words)
308            self.keyword_pat = re.compile(pat)

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
310    def apply(self, doc: Document) -> Document:
311        regex_match = self.keyword_pat.search(doc.text)
312        if regex_match:
313            doc.is_rejected = True
314            self.matched_text = regex_match.group()
315            self.matched_text_neighbor = doc.text[
316                regex_match.start() - 20 : regex_match.end() + 20
317            ]
318
319        return doc

Definition of filter behavior.

In this method, the filter will modify document.text, or set document.is_rejected = True to discard the document.

Do not define a filter that changes both document.text and document.token

Parameters

document : Document Input document

Returns

Document Processed Document

class NgWordsFilterEn(hojichar.core.filter_interface.Filter):
322class NgWordsFilterEn(Filter):
323    """
324    英語のNGワード(および不適切語)を含む文書を破棄します.
325    `dict_path` で指定したファイルから, キーワードのリストを得ます.
326    ファイルは単語が改行で羅列されたテキストファイルです.
327    """
328
329    def __init__(self, dict_path: Union[str, PathLike], *args: Any, **kwargs: Any) -> None:
330        super().__init__(*args, **kwargs)
331
332        with open(dict_path, encoding="utf-8") as fp:
333            ng_words = fp.read().split("\n")
334        ng_words = [re.escape(w.strip()) for w in ng_words if not len(w) == 0]
335        pat = "|".join(ng_words)
336        # 英語のパターンにマッチするようにしている, \s[単語]\s や [単語]. [単語], などにマッチ.
337        self.keyword_pat = re.compile(rf"(?:^| )({pat})(?:( |,|\.)|$)", re.IGNORECASE)
338
339    def apply(self, doc: Document) -> Document:
340        if self.keyword_pat.search(doc.text):
341            doc.is_rejected = True
342        return doc

英語のNGワード(および不適切語)を含む文書を破棄します. dict_path で指定したファイルから, キーワードのリストを得ます. ファイルは単語が改行で羅列されたテキストファイルです.

NgWordsFilterEn(dict_path: Union[str, os.PathLike], *args: Any, **kwargs: Any)
329    def __init__(self, dict_path: Union[str, PathLike], *args: Any, **kwargs: Any) -> None:
330        super().__init__(*args, **kwargs)
331
332        with open(dict_path, encoding="utf-8") as fp:
333            ng_words = fp.read().split("\n")
334        ng_words = [re.escape(w.strip()) for w in ng_words if not len(w) == 0]
335        pat = "|".join(ng_words)
336        # 英語のパターンにマッチするようにしている, \s[単語]\s や [単語]. [単語], などにマッチ.
337        self.keyword_pat = re.compile(rf"(?:^| )({pat})(?:( |,|\.)|$)", re.IGNORECASE)

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
339    def apply(self, doc: Document) -> Document:
340        if self.keyword_pat.search(doc.text):
341            doc.is_rejected = True
342        return doc

Definition of filter behavior.

In this method, the filter will modify document.text, or set document.is_rejected = True to discard the document.

Do not define a filter that changes both document.text and document.token

Parameters

document : Document Input document

Returns

Document Processed Document

class DiscardAdultContentJa(NgWordsFilterJa):
345class DiscardAdultContentJa(NgWordsFilterJa):
346    """
347    日本語のアダルトキーワード(および不適切語)を含む文書を破棄します.
348    `dict_path` で指定したファイルから, キーワードのリストを得ます.
349    ファイルは単語が改行で羅列されたテキストファイルです.
350    デフォルトの`dict_path` は /hojichar/dict/adult_keywords_ja.txt です.
351    """
352
353    def __init__(
354        self,
355        dict_path: Union[str, PathLike] = BASE_PATH / "dict/adult_keywords_ja.txt",
356        *args: Any,
357        **kwargs: Any,
358    ) -> None:
359        super().__init__(dict_path, *args, **kwargs)
360
361    def apply(self, doc: Document) -> Document:
362        """
363        >>> DiscardAdultContentJa().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected
364        True
365
366        >>> DiscardAdultContentJa().apply(Document("ほうじ茶")).is_rejected
367        False
368
369        挙動は正しいが誤検知しているケース. 他にも, サック in リュックサック,
370        >>> DiscardAdultContentJa().apply(Document("アスパラガス")).is_rejected \
371        # Matching with NG keyword "アス"
372        True
373        """
374        return super().apply(doc)

日本語のアダルトキーワード(および不適切語)を含む文書を破棄します. dict_path で指定したファイルから, キーワードのリストを得ます. ファイルは単語が改行で羅列されたテキストファイルです. デフォルトのdict_path は /hojichar/dict/adult_keywords_ja.txt です.

DiscardAdultContentJa( dict_path: Union[str, os.PathLike] = PosixPath('/home/runner/work/HojiChar/HojiChar/hojichar/dict/adult_keywords_ja.txt'), *args: Any, **kwargs: Any)
353    def __init__(
354        self,
355        dict_path: Union[str, PathLike] = BASE_PATH / "dict/adult_keywords_ja.txt",
356        *args: Any,
357        **kwargs: Any,
358    ) -> None:
359        super().__init__(dict_path, *args, **kwargs)

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
361    def apply(self, doc: Document) -> Document:
362        """
363        >>> DiscardAdultContentJa().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected
364        True
365
366        >>> DiscardAdultContentJa().apply(Document("ほうじ茶")).is_rejected
367        False
368
369        挙動は正しいが誤検知しているケース. 他にも, サック in リュックサック,
370        >>> DiscardAdultContentJa().apply(Document("アスパラガス")).is_rejected \
371        # Matching with NG keyword "アス"
372        True
373        """
374        return super().apply(doc)
>>> DiscardAdultContentJa().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected
True
>>> DiscardAdultContentJa().apply(Document("ほうじ茶")).is_rejected
False

挙動は正しいが誤検知しているケース. 他にも, サック in リュックサック,

>>> DiscardAdultContentJa().apply(Document("アスパラガス")).is_rejected         # Matching with NG keyword "アス"
True
class DiscardAdultContentEn(NgWordsFilterEn):
377class DiscardAdultContentEn(NgWordsFilterEn):
378    """
379    英語のアダルトキーワード(および不適切語)を含む文書を破棄します.
380    `dict_path` で指定したファイルから, キーワードのリストを得ます.
381    ファイルは単語が改行で羅列されたテキストファイルです.
382    デフォルトの`dict_path` は /hojichar/dict/adult_keywords_en.txt です.
383    """
384
385    def __init__(
386        self,
387        dict_path: Union[str, PathLike] = BASE_PATH / "dict/adult_keywords_en.txt",
388        *args: Any,
389        **kwargs: Any,
390    ) -> None:
391        super().__init__(dict_path, *args, **kwargs)
392
393    def apply(self, doc: Document) -> Document:
394        """
395        >>> DiscardAdultContentEn().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected
396        True
397
398        >>> DiscardAdultContentEn().apply(Document("hojichar")).is_rejected
399        False
400        """
401        return super().apply(doc)

英語のアダルトキーワード(および不適切語)を含む文書を破棄します. dict_path で指定したファイルから, キーワードのリストを得ます. ファイルは単語が改行で羅列されたテキストファイルです. デフォルトのdict_path は /hojichar/dict/adult_keywords_en.txt です.

DiscardAdultContentEn( dict_path: Union[str, os.PathLike] = PosixPath('/home/runner/work/HojiChar/HojiChar/hojichar/dict/adult_keywords_en.txt'), *args: Any, **kwargs: Any)
385    def __init__(
386        self,
387        dict_path: Union[str, PathLike] = BASE_PATH / "dict/adult_keywords_en.txt",
388        *args: Any,
389        **kwargs: Any,
390    ) -> None:
391        super().__init__(dict_path, *args, **kwargs)

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
393    def apply(self, doc: Document) -> Document:
394        """
395        >>> DiscardAdultContentEn().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected
396        True
397
398        >>> DiscardAdultContentEn().apply(Document("hojichar")).is_rejected
399        False
400        """
401        return super().apply(doc)
>>> DiscardAdultContentEn().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected
True
>>> DiscardAdultContentEn().apply(Document("hojichar")).is_rejected
False
class DiscardDiscriminationContentJa(NgWordsFilterJa):
404class DiscardDiscriminationContentJa(NgWordsFilterJa):
405    """
406    日本語の差別キーワード(および不適切語)を含む文書を破棄します.
407    `dict_path` で指定したファイルから, キーワードのリストを得ます.
408    ファイルは単語が改行で羅列されたテキストファイルです.
409    デフォルトの`dict_path` は /hojichar/dict/discrimination_keywords_ja.txt です.
410    """
411
412    def __init__(
413        self,
414        dict_path: Union[str, PathLike] = BASE_PATH / "dict/discrimination_keywords_ja.txt",
415        *args: Any,
416        **kwargs: Any,
417    ):
418        super().__init__(dict_path, *args, **kwargs)
419
420    def apply(self, doc: Document) -> Document:
421        """
422        >>> DiscardDiscriminationContentJa().\
423            apply(Document("<TEST_STRING_OF_DISCRIMINATION_KEYWORD>")).is_rejected
424        True
425
426        >>> DiscardDiscriminationContentJa().apply(Document("ほうじ茶")).is_rejected
427        False
428        """
429        return super().apply(doc)

日本語の差別キーワード(および不適切語)を含む文書を破棄します. dict_path で指定したファイルから, キーワードのリストを得ます. ファイルは単語が改行で羅列されたテキストファイルです. デフォルトのdict_path は /hojichar/dict/discrimination_keywords_ja.txt です.

DiscardDiscriminationContentJa( dict_path: Union[str, os.PathLike] = PosixPath('/home/runner/work/HojiChar/HojiChar/hojichar/dict/discrimination_keywords_ja.txt'), *args: Any, **kwargs: Any)
412    def __init__(
413        self,
414        dict_path: Union[str, PathLike] = BASE_PATH / "dict/discrimination_keywords_ja.txt",
415        *args: Any,
416        **kwargs: Any,
417    ):
418        super().__init__(dict_path, *args, **kwargs)

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
420    def apply(self, doc: Document) -> Document:
421        """
422        >>> DiscardDiscriminationContentJa().\
423            apply(Document("<TEST_STRING_OF_DISCRIMINATION_KEYWORD>")).is_rejected
424        True
425
426        >>> DiscardDiscriminationContentJa().apply(Document("ほうじ茶")).is_rejected
427        False
428        """
429        return super().apply(doc)
>>> DiscardDiscriminationContentJa().            apply(Document("<TEST_STRING_OF_DISCRIMINATION_KEYWORD>")).is_rejected
True
>>> DiscardDiscriminationContentJa().apply(Document("ほうじ茶")).is_rejected
False
class DiscardViolenceContentJa(NgWordsFilterJa):
432class DiscardViolenceContentJa(NgWordsFilterJa):
433    """
434    日本語の暴力・脅迫を示唆するキーワードを含む文書を破棄します.
435    `dict_path` で指定したファイルから, キーワードのリストを得ます.
436    ファイルは単語が改行で羅列されたテキストファイルです.
437    デフォルトの`dict_path` は /hojichar/dict/violence_keywords_ja.txt です.
438    """
439
440    def __init__(
441        self,
442        dict_path: Union[str, PathLike] = BASE_PATH / "dict/violence_keywords_ja.txt",
443        *args: Any,
444        **kwargs: Any,
445    ) -> None:
446        super().__init__(dict_path, *args, **kwargs)
447
448    def apply(self, doc: Document) -> Document:
449        """
450        >>> DiscardViolenceContentJa()\
451            .apply(Document("<TEST_STRING_OF_VIOLENCE_KEYWORD>")).is_rejected
452        True
453
454        >>> DiscardViolenceContentJa().apply(Document("ほうじ茶")).is_rejected
455        False
456        """
457        return super().apply(doc)

日本語の暴力・脅迫を示唆するキーワードを含む文書を破棄します. dict_path で指定したファイルから, キーワードのリストを得ます. ファイルは単語が改行で羅列されたテキストファイルです. デフォルトのdict_path は /hojichar/dict/violence_keywords_ja.txt です.

DiscardViolenceContentJa( dict_path: Union[str, os.PathLike] = PosixPath('/home/runner/work/HojiChar/HojiChar/hojichar/dict/violence_keywords_ja.txt'), *args: Any, **kwargs: Any)
440    def __init__(
441        self,
442        dict_path: Union[str, PathLike] = BASE_PATH / "dict/violence_keywords_ja.txt",
443        *args: Any,
444        **kwargs: Any,
445    ) -> None:
446        super().__init__(dict_path, *args, **kwargs)

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
448    def apply(self, doc: Document) -> Document:
449        """
450        >>> DiscardViolenceContentJa()\
451            .apply(Document("<TEST_STRING_OF_VIOLENCE_KEYWORD>")).is_rejected
452        True
453
454        >>> DiscardViolenceContentJa().apply(Document("ほうじ茶")).is_rejected
455        False
456        """
457        return super().apply(doc)
>>> DiscardViolenceContentJa()            .apply(Document("<TEST_STRING_OF_VIOLENCE_KEYWORD>")).is_rejected
True
>>> DiscardViolenceContentJa().apply(Document("ほうじ茶")).is_rejected
False
class DiscardBBSComments(hojichar.core.filter_interface.Filter):
460class DiscardBBSComments(Filter):
461    """
462    正規表現 "BBS Pattern" に `max_allow_num` 回よりたくさんマッチする文書を破棄します.
463    `max_allow_num` のデフォルト値は14です.
464    正規表現 "BBS Pattern" は下記のリンクで検証可能です.
465    https://regex101.com/r/ybQvL2/1
466    """
467
468    def __init__(self, max_allowed_num: int = 14, *args: Any, **kwargs: Any) -> None:
469        super().__init__(*args, **kwargs)
470
471        self.max_allowed_num = max_allowed_num
472        self.keyword_pat = re.compile(
473            r"\d{4}[年\.\-\/][\ ]*\d{1,2}[月\.\-\/][\ ]*\d{1,2}[日]*|コメント|SOLD OUT|レビュー|投稿|ページ|\([月火水木金土日]\)|質問|\d+話|楽天市場|-"  # noqa
474        )
475
476    def apply(self, doc: Document) -> Document:
477        """
478        >>> DiscardBBSComments().apply(Document("楽天市場 質問 投稿 コメント レビュー "*3)).is_rejected
479        True
480
481        >>> DiscardBBSComments().apply(Document("鏡餅")).is_rejected
482        False
483        """
484        bbs_factor = self.keyword_pat.findall(doc.text)
485        if len(bbs_factor) > self.max_allowed_num:
486            doc.is_rejected = True
487        return doc

正規表現 "BBS Pattern" に max_allow_num 回よりたくさんマッチする文書を破棄します. max_allow_num のデフォルト値は14です. 正規表現 "BBS Pattern" は下記のリンクで検証可能です. https://regex101.com/r/ybQvL2/1

DiscardBBSComments(max_allowed_num: int = 14, *args: Any, **kwargs: Any)
468    def __init__(self, max_allowed_num: int = 14, *args: Any, **kwargs: Any) -> None:
469        super().__init__(*args, **kwargs)
470
471        self.max_allowed_num = max_allowed_num
472        self.keyword_pat = re.compile(
473            r"\d{4}[年\.\-\/][\ ]*\d{1,2}[月\.\-\/][\ ]*\d{1,2}[日]*|コメント|SOLD OUT|レビュー|投稿|ページ|\([月火水木金土日]\)|質問|\d+話|楽天市場|-"  # noqa
474        )

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
476    def apply(self, doc: Document) -> Document:
477        """
478        >>> DiscardBBSComments().apply(Document("楽天市場 質問 投稿 コメント レビュー "*3)).is_rejected
479        True
480
481        >>> DiscardBBSComments().apply(Document("鏡餅")).is_rejected
482        False
483        """
484        bbs_factor = self.keyword_pat.findall(doc.text)
485        if len(bbs_factor) > self.max_allowed_num:
486            doc.is_rejected = True
487        return doc
>>> DiscardBBSComments().apply(Document("楽天市場 質問 投稿 コメント レビュー "*3)).is_rejected
True
>>> DiscardBBSComments().apply(Document("鏡餅")).is_rejected
False
class DiscardAds(hojichar.core.filter_interface.Filter):
490class DiscardAds(Filter):
491    """
492    主に広告キーワードを`max_allow_num`より多く含む文書を破棄します.
493    デフォルトで`max_allow_num` は14です.
494    `dict_path` で指定したファイルから, 広告キーワードのリストを得ます.
495    ファイルは単語が改行で羅列されたテキストファイルです.
496    デフォルトの`dict_path` は /hojichar/dict/advertisement_keywords_ja.txt です.
497    """
498
499    def __init__(
500        self,
501        dict_path: Union[str, PathLike] = BASE_PATH / "dict/advertisement_keywords_ja.txt",
502        max_allowed_num: int = 14,
503        *args: Any,
504        **kwargs: Any,
505    ):
506        super().__init__(*args, **kwargs)
507
508        self.max_allow_num = max_allowed_num
509        with open(dict_path, encoding="utf-8") as fp:
510            ng_words = fp.read().split("\n")
511        ng_words = [re.escape(w.strip()) for w in ng_words if not len(w) == 0]
512        pat = r"|".join(ng_words)
513        self.keyword_pat = re.compile(pat)
514
515    def apply(self, doc: Document) -> Document:
516        """
517        >>> DiscardAds().apply(Document("お問い合わせください 営業時間 よくある質問"*5)).is_rejected
518        True
519
520        >>> DiscardAds().apply(Document("おはよう")).is_rejected
521        False
522        """
523        ads_factor = self.keyword_pat.findall(doc.text)
524        if len(ads_factor) > self.max_allow_num:
525            doc.is_rejected = True
526        return doc

主に広告キーワードをmax_allow_numより多く含む文書を破棄します. デフォルトでmax_allow_num は14です. dict_path で指定したファイルから, 広告キーワードのリストを得ます. ファイルは単語が改行で羅列されたテキストファイルです. デフォルトのdict_path は /hojichar/dict/advertisement_keywords_ja.txt です.

DiscardAds( dict_path: Union[str, os.PathLike] = PosixPath('/home/runner/work/HojiChar/HojiChar/hojichar/dict/advertisement_keywords_ja.txt'), max_allowed_num: int = 14, *args: Any, **kwargs: Any)
499    def __init__(
500        self,
501        dict_path: Union[str, PathLike] = BASE_PATH / "dict/advertisement_keywords_ja.txt",
502        max_allowed_num: int = 14,
503        *args: Any,
504        **kwargs: Any,
505    ):
506        super().__init__(*args, **kwargs)
507
508        self.max_allow_num = max_allowed_num
509        with open(dict_path, encoding="utf-8") as fp:
510            ng_words = fp.read().split("\n")
511        ng_words = [re.escape(w.strip()) for w in ng_words if not len(w) == 0]
512        pat = r"|".join(ng_words)
513        self.keyword_pat = re.compile(pat)

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
515    def apply(self, doc: Document) -> Document:
516        """
517        >>> DiscardAds().apply(Document("お問い合わせください 営業時間 よくある質問"*5)).is_rejected
518        True
519
520        >>> DiscardAds().apply(Document("おはよう")).is_rejected
521        False
522        """
523        ads_factor = self.keyword_pat.findall(doc.text)
524        if len(ads_factor) > self.max_allow_num:
525            doc.is_rejected = True
526        return doc
>>> DiscardAds().apply(Document("お問い合わせください 営業時間 よくある質問"*5)).is_rejected
True
>>> DiscardAds().apply(Document("おはよう")).is_rejected
False
class AcceptJapanese(hojichar.core.filter_interface.Filter):
529class AcceptJapanese(Filter):
530    """
531    日本語でないドキュメントを破棄します. 日本語判定は次の手順で行われます.
532        1. テキストを左から`lookup_size` (デフォルトで50字) 参照し,
533        ひらがな・カタカナが存在すれば日本語と判定する.
534    """
535
536    def __init__(self, lookup_size: int = 50, *args: Any, **kwargs: Any) -> None:
537        super().__init__(*args, **kwargs)
538
539        self.lookup_size = lookup_size
540        self.hiragana_katakana_pat = re.compile(r"[ぁ-んァ-ン]")
541
542    def apply(self, doc: Document) -> Document:
543        """
544        >>> AcceptJapanese().apply(Document("This is English document")).is_rejected
545        True
546
547        >>> AcceptJapanese().apply(Document("a"*50 + "あ")).is_rejected
548        True
549
550        >>> AcceptJapanese().apply(Document("ほうじ茶")).is_rejected
551        False
552        """
553        if not self.hiragana_katakana_pat.search(doc.text[: self.lookup_size]):
554            doc.is_rejected = True
555        return doc

日本語でないドキュメントを破棄します. 日本語判定は次の手順で行われます. 1. テキストを左からlookup_size (デフォルトで50字) 参照し, ひらがな・カタカナが存在すれば日本語と判定する.

AcceptJapanese(lookup_size: int = 50, *args: Any, **kwargs: Any)
536    def __init__(self, lookup_size: int = 50, *args: Any, **kwargs: Any) -> None:
537        super().__init__(*args, **kwargs)
538
539        self.lookup_size = lookup_size
540        self.hiragana_katakana_pat = re.compile(r"[ぁ-んァ-ン]")

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
542    def apply(self, doc: Document) -> Document:
543        """
544        >>> AcceptJapanese().apply(Document("This is English document")).is_rejected
545        True
546
547        >>> AcceptJapanese().apply(Document("a"*50 + "あ")).is_rejected
548        True
549
550        >>> AcceptJapanese().apply(Document("ほうじ茶")).is_rejected
551        False
552        """
553        if not self.hiragana_katakana_pat.search(doc.text[: self.lookup_size]):
554            doc.is_rejected = True
555        return doc
>>> AcceptJapanese().apply(Document("This is English document")).is_rejected
True
>>> AcceptJapanese().apply(Document("a"*50 + "あ")).is_rejected
True
>>> AcceptJapanese().apply(Document("ほうじ茶")).is_rejected
False
class DiscardRareKuten(hojichar.core.filter_interface.Filter):
558class DiscardRareKuten(Filter):
559    """
560    日本語でないドキュメントを破棄します. 日本語判定は次の手順で行われます
561    ドキュメントを句点"。"で区切り, 平均文長が
562    `max_avarage_sentence_length` より長い場合は破棄します.
563    `max_avarage_sentence_length` のデフォルト値は100です.
564    このフィルタは, 文章中の句点の割合が少なすぎるドキュメントを破棄します.
565    """
566
567    def __init__(self, max_average_sentence_length: int = 100, *args: Any, **kwargs: Any) -> None:
568        super().__init__(*args, **kwargs)
569
570        self.max_average_sentence_length = max_average_sentence_length
571        self.kuten_pat = re.compile(r"。")
572
573    def apply(self, doc: Document) -> Document:
574        """
575        >>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよ。")).is_rejected
576        False
577        >>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよう。")).is_rejected
578        True
579        """
580        kuten_lst = self.kuten_pat.findall(doc.text)
581        min_kuten_num = len(doc.text) / self.max_average_sentence_length
582        if len(kuten_lst) < min_kuten_num:
583            doc.is_rejected = True
584        return doc

日本語でないドキュメントを破棄します. 日本語判定は次の手順で行われます ドキュメントを句点"。"で区切り, 平均文長が max_avarage_sentence_length より長い場合は破棄します. max_avarage_sentence_length のデフォルト値は100です. このフィルタは, 文章中の句点の割合が少なすぎるドキュメントを破棄します.

DiscardRareKuten(max_average_sentence_length: int = 100, *args: Any, **kwargs: Any)
567    def __init__(self, max_average_sentence_length: int = 100, *args: Any, **kwargs: Any) -> None:
568        super().__init__(*args, **kwargs)
569
570        self.max_average_sentence_length = max_average_sentence_length
571        self.kuten_pat = re.compile(r"。")

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
573    def apply(self, doc: Document) -> Document:
574        """
575        >>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよ。")).is_rejected
576        False
577        >>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよう。")).is_rejected
578        True
579        """
580        kuten_lst = self.kuten_pat.findall(doc.text)
581        min_kuten_num = len(doc.text) / self.max_average_sentence_length
582        if len(kuten_lst) < min_kuten_num:
583            doc.is_rejected = True
584        return doc
>>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよ。")).is_rejected
False
>>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよう。")).is_rejected
True
class HeaderFooterTagsRemover(hojichar.core.filter_interface.Filter):
587class HeaderFooterTagsRemover(Filter):
588    """
589    ドキュメントの冒頭・末尾のトークンを調査し, ヘッダー・フッダー的な
590    タグが存在していた場合, そのトークンを除去します.
591
592    このフィルタを通す前に, 事前にセンテンスレベルにトーカナイズしておいてください.
593    このフィルタでは Document.token にのみ変更が加えられるので, 出力前 あるいは 下流フィルタで
594    Document.text に変更を加える前にトークンをマージしておいてください.
595    """
596
597    def __init__(
598        self,
599        dict_path: Union[str, PathLike] = BASE_PATH / "dict/header_footer_keywords_ja.txt",
600        *args: Any,
601        **kwargs: Any,
602    ) -> None:
603        super().__init__(*args, **kwargs)
604
605        with open(dict_path) as fp:
606            keywords = fp.read().split("\n")
607        keywords = [re.escape(w.strip()) for w in keywords if not len(w) == 0]
608        self.keyword_pat = re.compile(r"|".join(keywords))
609
610    def apply(self, doc: Document) -> Document:
611        if len(doc.tokens) == 0:
612            return doc
613
614        lookup_size = 0
615        if 1 <= len(doc.tokens) < 4:
616            lookup_size = 1
617        elif 4 <= len(doc.tokens) < 6:
618            lookup_size = 2
619        elif 6 <= len(doc.tokens):
620            lookup_size = 3
621
622        for i in range(lookup_size):
623            if self.should_drop_token(doc.tokens[i]):
624                doc.tokens[i].is_rejected = True
625            if self.should_drop_token(doc.tokens[-(i + 1)]):
626                doc.tokens[i].is_rejected = True
627
628        return doc
629
630    def should_drop_token(self, token: Token) -> bool:
631        """
632        >>> HeaderFooterTagsRemover().should_drop_token(Token("<TEST_STRING_OF_KEYWORD>"))
633        True
634
635        >>> HeaderFooterTagsRemover().should_drop_token(Token("ほうじ茶"))
636        False
637
638        Comment.
639        Original legacy code removed a pattern r"« _ | Main | _ »" .
640        In the pattern, "|" is not escaped, so **ANY** string was eliminated.
641        It seems unintended behavior, so I fix this.
642        """
643        if self.keyword_pat.match(token.text):
644            return True
645        else:
646            return False

ドキュメントの冒頭・末尾のトークンを調査し, ヘッダー・フッダー的な タグが存在していた場合, そのトークンを除去します.

このフィルタを通す前に, 事前にセンテンスレベルにトーカナイズしておいてください. このフィルタでは Document.token にのみ変更が加えられるので, 出力前 あるいは 下流フィルタで Document.text に変更を加える前にトークンをマージしておいてください.

HeaderFooterTagsRemover( dict_path: Union[str, os.PathLike] = PosixPath('/home/runner/work/HojiChar/HojiChar/hojichar/dict/header_footer_keywords_ja.txt'), *args: Any, **kwargs: Any)
597    def __init__(
598        self,
599        dict_path: Union[str, PathLike] = BASE_PATH / "dict/header_footer_keywords_ja.txt",
600        *args: Any,
601        **kwargs: Any,
602    ) -> None:
603        super().__init__(*args, **kwargs)
604
605        with open(dict_path) as fp:
606            keywords = fp.read().split("\n")
607        keywords = [re.escape(w.strip()) for w in keywords if not len(w) == 0]
608        self.keyword_pat = re.compile(r"|".join(keywords))

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
610    def apply(self, doc: Document) -> Document:
611        if len(doc.tokens) == 0:
612            return doc
613
614        lookup_size = 0
615        if 1 <= len(doc.tokens) < 4:
616            lookup_size = 1
617        elif 4 <= len(doc.tokens) < 6:
618            lookup_size = 2
619        elif 6 <= len(doc.tokens):
620            lookup_size = 3
621
622        for i in range(lookup_size):
623            if self.should_drop_token(doc.tokens[i]):
624                doc.tokens[i].is_rejected = True
625            if self.should_drop_token(doc.tokens[-(i + 1)]):
626                doc.tokens[i].is_rejected = True
627
628        return doc

Definition of filter behavior.

In this method, the filter will modify document.text, or set document.is_rejected = True to discard the document.

Do not define a filter that changes both document.text and document.token

Parameters

document : Document Input document

Returns

Document Processed Document

def should_drop_token(self, token: hojichar.core.models.Token) -> bool:
630    def should_drop_token(self, token: Token) -> bool:
631        """
632        >>> HeaderFooterTagsRemover().should_drop_token(Token("<TEST_STRING_OF_KEYWORD>"))
633        True
634
635        >>> HeaderFooterTagsRemover().should_drop_token(Token("ほうじ茶"))
636        False
637
638        Comment.
639        Original legacy code removed a pattern r"« _ | Main | _ »" .
640        In the pattern, "|" is not escaped, so **ANY** string was eliminated.
641        It seems unintended behavior, so I fix this.
642        """
643        if self.keyword_pat.match(token.text):
644            return True
645        else:
646            return False
>>> HeaderFooterTagsRemover().should_drop_token(Token("<TEST_STRING_OF_KEYWORD>"))
True
>>> HeaderFooterTagsRemover().should_drop_token(Token("ほうじ茶"))
False

Comment. Original legacy code removed a pattern r"« _ | Main | _ »" . In the pattern, "|" is not escaped, so ANY string was eliminated. It seems unintended behavior, so I fix this.

class MaskPersonalInformation(hojichar.core.filter_interface.Filter):
649class MaskPersonalInformation(Filter):
650    """
651    ドキュメントに含まれる電話番号・電子メールアドレスを一部マスキングします.
652    """
653
654    def __init__(self, *args: Any, **kwargs: Any) -> None:
655        super().__init__(*args, **kwargs)
656
657        self.phone_pat = re.compile(
658            r"((0|\+\d{1,3}[- ]?)(\d{2}[- ]?\d{4}[- ]?|\d[- ]?\d{4}[- ]?|\d{2}[- ]?\d{3}[- ]?|\d{3}[- ]?\d{2}[- ]?|\d{4}[- ]?\d{1}[- ]?))\d{4}"  # noqa
659        )
660        self.email_pat = re.compile(
661            r"[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~.]+@[A-Za-z0-9!#$%&'*+\-/=?^_`{|}~.]+(\.[A-Za-z0-9\-]+)"  # noqa
662        )
663
664    def apply(self, doc: Document) -> Document:
665        """
666        >>> MaskPersonalInformation()('06-1234-5678')
667        '06-1234-XXXX'
668        >>> MaskPersonalInformation()('075-123-4567')
669        '075-123-XXXX'
670        >>> MaskPersonalInformation()('0166-12-3456')
671        '0166-12-XXXX'
672        >>> MaskPersonalInformation()('09808-1-2345')
673        '09808-1-XXXX'
674        >>> MaskPersonalInformation()('090-1234-5678')
675        '090-1234-XXXX'
676        >>> MaskPersonalInformation()('0751234567')
677        '075123XXXX'
678        >>> MaskPersonalInformation()('08012345678')
679        '0801234XXXX'
680        >>> MaskPersonalInformation()('連絡は075-123-4567 まで')
681        '連絡は075-123-XXXX まで'
682        >>> MaskPersonalInformation()('+81-80-1234-5678')
683        '+81-80-1234-XXXX'
684        >>> MaskPersonalInformation()('+818012345678')
685        '+81801234XXXX'
686        >>> MaskPersonalInformation()('hogehoge@example.com')
687        'xxxx@yyy.com'
688        >>> MaskPersonalInformation()('何かあれば hogehoge@example.ne.jp まで連絡')
689        '何かあれば xxxx@yyy.jp まで連絡'
690        """
691        text = self.phone_pat.sub(r"\1XXXX", doc.text)
692        text = self.email_pat.sub(r"xxxx@yyy\1", text)
693        doc.text = text
694        return doc

ドキュメントに含まれる電話番号・電子メールアドレスを一部マスキングします.

MaskPersonalInformation(*args: Any, **kwargs: Any)
654    def __init__(self, *args: Any, **kwargs: Any) -> None:
655        super().__init__(*args, **kwargs)
656
657        self.phone_pat = re.compile(
658            r"((0|\+\d{1,3}[- ]?)(\d{2}[- ]?\d{4}[- ]?|\d[- ]?\d{4}[- ]?|\d{2}[- ]?\d{3}[- ]?|\d{3}[- ]?\d{2}[- ]?|\d{4}[- ]?\d{1}[- ]?))\d{4}"  # noqa
659        )
660        self.email_pat = re.compile(
661            r"[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~.]+@[A-Za-z0-9!#$%&'*+\-/=?^_`{|}~.]+(\.[A-Za-z0-9\-]+)"  # noqa
662        )

Parameters

p : float, optional Probability that this filter will be applied. Default=1

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
664    def apply(self, doc: Document) -> Document:
665        """
666        >>> MaskPersonalInformation()('06-1234-5678')
667        '06-1234-XXXX'
668        >>> MaskPersonalInformation()('075-123-4567')
669        '075-123-XXXX'
670        >>> MaskPersonalInformation()('0166-12-3456')
671        '0166-12-XXXX'
672        >>> MaskPersonalInformation()('09808-1-2345')
673        '09808-1-XXXX'
674        >>> MaskPersonalInformation()('090-1234-5678')
675        '090-1234-XXXX'
676        >>> MaskPersonalInformation()('0751234567')
677        '075123XXXX'
678        >>> MaskPersonalInformation()('08012345678')
679        '0801234XXXX'
680        >>> MaskPersonalInformation()('連絡は075-123-4567 まで')
681        '連絡は075-123-XXXX まで'
682        >>> MaskPersonalInformation()('+81-80-1234-5678')
683        '+81-80-1234-XXXX'
684        >>> MaskPersonalInformation()('+818012345678')
685        '+81801234XXXX'
686        >>> MaskPersonalInformation()('hogehoge@example.com')
687        'xxxx@yyy.com'
688        >>> MaskPersonalInformation()('何かあれば hogehoge@example.ne.jp まで連絡')
689        '何かあれば xxxx@yyy.jp まで連絡'
690        """
691        text = self.phone_pat.sub(r"\1XXXX", doc.text)
692        text = self.email_pat.sub(r"xxxx@yyy\1", text)
693        doc.text = text
694        return doc
>>> MaskPersonalInformation()('06-1234-5678')
'06-1234-XXXX'
>>> MaskPersonalInformation()('075-123-4567')
'075-123-XXXX'
>>> MaskPersonalInformation()('0166-12-3456')
'0166-12-XXXX'
>>> MaskPersonalInformation()('09808-1-2345')
'09808-1-XXXX'
>>> MaskPersonalInformation()('090-1234-5678')
'090-1234-XXXX'
>>> MaskPersonalInformation()('0751234567')
'075123XXXX'
>>> MaskPersonalInformation()('08012345678')
'0801234XXXX'
>>> MaskPersonalInformation()('連絡は075-123-4567 まで')
'連絡は075-123-XXXX まで'
>>> MaskPersonalInformation()('+81-80-1234-5678')
'+81-80-1234-XXXX'
>>> MaskPersonalInformation()('+818012345678')
'+81801234XXXX'
>>> MaskPersonalInformation()('hogehoge@example.com')
'xxxx@yyy.com'
>>> MaskPersonalInformation()('何かあれば hogehoge@example.ne.jp まで連絡')
'何かあれば xxxx@yyy.jp まで連絡'
class DiscardTooManyNouns(hojichar.core.filter_interface.Filter):
697class DiscardTooManyNouns(Filter):
698    """
699    [!CAUTION] This filter requires `fugashi` package. Please install it
700    by `pip install 'hojichar[all]'`.
701
702    A filter that removes document with too many nouns in Japanese i.e.,
703    documents such as advertisement, word salad, etc ...
704    """
705
706    def __init__(self, threshold: float = 0.80, *args: Any, **kwargs: Any) -> None:
707        """
708        Args:
709            threshold: document whose noun ratio is higher than this value will be discarded
710            *args:
711            **kwargs:
712        """
713        super().__init__(*args, **kwargs)
714        assert (
715            is_loaded_extras
716        ), "fugashi is required for this filter. Try pip install 'hojichar[all]'"
717
718        self.threshold = threshold
719        self.tagger = Tagger("-Owakati")
720        assert (
721            "unidic" in self.tagger.dictionary_info[0]["filename"]
722        ), "MeCab dictionary must be unidic"
723
724    def apply(self, doc: Document) -> Document:
725        """
726        >>> DiscardTooManyNouns().apply(Document("自然言語処理大好き!")).is_rejected
727        False
728        >>> DiscardTooManyNouns().apply(Document("リンゴ・オレンジ・ミカン・バナナ セール中")).is_rejected
729        True
730        >>> DiscardTooManyNouns().apply(Document("今日の仙台朝市ではリンゴがセール中")).is_rejected
731        False
732        """
733        # remove "補助記号" from part-of-speech statistics
734        # because they often decrease the noun ratio,
735        # e.g., the sentence "リンゴ・オレンジ・バナナ・" has 補助記号 ratio of 0.5
736        # however, we don't want such sentence
737        pos_count = Counter(
738            w.feature.pos1 for w in self.tagger(doc.text) if w.feature.pos1 != "補助記号"
739        )
740        try:
741            noun_ratio = pos_count["名詞"] / sum(pos_count.values())
742        except ZeroDivisionError:
743            noun_ratio = 0.0
744        if noun_ratio >= self.threshold:
745            doc.is_rejected = True
746        return doc

[!CAUTION] This filter requires fugashi package. Please install it by pip install 'hojichar[all]'.

A filter that removes document with too many nouns in Japanese i.e., documents such as advertisement, word salad, etc ...

DiscardTooManyNouns(threshold: float = 0.8, *args: Any, **kwargs: Any)
706    def __init__(self, threshold: float = 0.80, *args: Any, **kwargs: Any) -> None:
707        """
708        Args:
709            threshold: document whose noun ratio is higher than this value will be discarded
710            *args:
711            **kwargs:
712        """
713        super().__init__(*args, **kwargs)
714        assert (
715            is_loaded_extras
716        ), "fugashi is required for this filter. Try pip install 'hojichar[all]'"
717
718        self.threshold = threshold
719        self.tagger = Tagger("-Owakati")
720        assert (
721            "unidic" in self.tagger.dictionary_info[0]["filename"]
722        ), "MeCab dictionary must be unidic"

Args: threshold: document whose noun ratio is higher than this value will be discarded args: *kwargs:

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
724    def apply(self, doc: Document) -> Document:
725        """
726        >>> DiscardTooManyNouns().apply(Document("自然言語処理大好き!")).is_rejected
727        False
728        >>> DiscardTooManyNouns().apply(Document("リンゴ・オレンジ・ミカン・バナナ セール中")).is_rejected
729        True
730        >>> DiscardTooManyNouns().apply(Document("今日の仙台朝市ではリンゴがセール中")).is_rejected
731        False
732        """
733        # remove "補助記号" from part-of-speech statistics
734        # because they often decrease the noun ratio,
735        # e.g., the sentence "リンゴ・オレンジ・バナナ・" has 補助記号 ratio of 0.5
736        # however, we don't want such sentence
737        pos_count = Counter(
738            w.feature.pos1 for w in self.tagger(doc.text) if w.feature.pos1 != "補助記号"
739        )
740        try:
741            noun_ratio = pos_count["名詞"] / sum(pos_count.values())
742        except ZeroDivisionError:
743            noun_ratio = 0.0
744        if noun_ratio >= self.threshold:
745            doc.is_rejected = True
746        return doc
>>> DiscardTooManyNouns().apply(Document("自然言語処理大好き!")).is_rejected
False
>>> DiscardTooManyNouns().apply(Document("リンゴ・オレンジ・ミカン・バナナ セール中")).is_rejected
True
>>> DiscardTooManyNouns().apply(Document("今日の仙台朝市ではリンゴがセール中")).is_rejected
False
class CharRepetitionRatioFilter(hojichar.core.filter_interface.Filter):
749class CharRepetitionRatioFilter(Filter):
750    """
751    文字Ngramの重なり率(文書中で高頻度文字Ngramが占める割合)を計算して, 重なりの大きいものを除去します.
752    名詞の連続からなるような広告テキストを取り除くのに有効です.
753
754    実装は, BigScience で採用されていた前処理を参考にしています.
755    元実装: https://github.com/bigscience-workshop/data-preparation/blob/9d0588419073cc5bf0fb92b58f37f2a1016572c3/preprocessing/training/01b_oscar_cleaning_and_filtering/filtering.py#L425-L453  # noqa: E501
756
757    「高頻度文字Ngram」は、sqrt(ユニークなNgramの総数)によって求めていますが,
758    これは文書長の影響を軽減するためだとされています.
759
760    掲示板のテキストが引っかかりやすい傾向があります.
761    13: 名無しさん@実況で競馬板アウト 2019/08/18(日) 15:28:46.10 ID:eBvZg8h+0
762    的なものが高頻度で登場するため、文字Ngramの重なり率も高くなってしまう
763    """
764
765    def __init__(
766        self, threshold: float = 0.33, ngram_size: int = 5, *args: Any, **kwargs: Any
767    ) -> None:
768        """
769
770        Args:
771            threshold: document with character repetition ratio higher than this value will be discarded
772            ngram_size: character ngram size. Larger value will decrease the false positive of long documents
773            *args:
774            **kwargs:
775        """  # noqa: E501
776
777        super().__init__(*args, **kwargs)
778        self.threshold = threshold
779        self.ngram_size = ngram_size
780
781    def apply(self, doc: Document) -> Document:
782        ratio = self.compute_character_repetition_ratio(doc.text, self.ngram_size)
783        if ratio >= self.threshold:
784            doc.is_rejected = True
785        return doc
786
787    @staticmethod
788    def compute_character_repetition_ratio(
789        document: str, character_repetition_length: int
790    ) -> float:
791        def get_freq_character_ngrams(document: str, n: int) -> Dict[str, int]:
792            character_ngrams: List[str] = [
793                document[i : i + n] for i in range(len(document) - n + 1)
794            ]
795            freq_character_ngrams_dict: Dict[str, int] = {}
796            for character_ngram in character_ngrams:
797                freq_character_ngrams_dict[character_ngram] = (
798                    freq_character_ngrams_dict.get(character_ngram, 0) + 1
799                )
800            return freq_character_ngrams_dict
801
802        freq_character_ngrams_dict = get_freq_character_ngrams(
803            document, character_repetition_length
804        )
805        if len(freq_character_ngrams_dict) == 0:
806            return 0.0
807        freq_character_ngrams: List[int] = list(freq_character_ngrams_dict.values())
808        freq_character_ngrams = sorted(freq_character_ngrams, reverse=True)
809        val_one = len([el for el in freq_character_ngrams if el == 1])
810        num_rep_character_ngrams = min(
811            int(np.sqrt(len(freq_character_ngrams))),
812            len(freq_character_ngrams) - val_one,
813        )
814        character_repetition_ratio = sum(freq_character_ngrams[:num_rep_character_ngrams]) / sum(
815            freq_character_ngrams
816        )
817        return character_repetition_ratio

文字Ngramの重なり率(文書中で高頻度文字Ngramが占める割合)を計算して, 重なりの大きいものを除去します. 名詞の連続からなるような広告テキストを取り除くのに有効です.

実装は, BigScience で採用されていた前処理を参考にしています. 元実装: https://github.com/bigscience-workshop/data-preparation/blob/9d0588419073cc5bf0fb92b58f37f2a1016572c3/preprocessing/training/01b_oscar_cleaning_and_filtering/filtering.py#L425-L453 # noqa: E501

「高頻度文字Ngram」は、sqrt(ユニークなNgramの総数)によって求めていますが, これは文書長の影響を軽減するためだとされています.

掲示板のテキストが引っかかりやすい傾向があります. 13: 名無しさん@実況で競馬板アウト 2019/08/18(日) 15:28:46.10 ID:eBvZg8h+0 的なものが高頻度で登場するため、文字Ngramの重なり率も高くなってしまう

CharRepetitionRatioFilter( threshold: float = 0.33, ngram_size: int = 5, *args: Any, **kwargs: Any)
765    def __init__(
766        self, threshold: float = 0.33, ngram_size: int = 5, *args: Any, **kwargs: Any
767    ) -> None:
768        """
769
770        Args:
771            threshold: document with character repetition ratio higher than this value will be discarded
772            ngram_size: character ngram size. Larger value will decrease the false positive of long documents
773            *args:
774            **kwargs:
775        """  # noqa: E501
776
777        super().__init__(*args, **kwargs)
778        self.threshold = threshold
779        self.ngram_size = ngram_size

Args: threshold: document with character repetition ratio higher than this value will be discarded ngram_size: character ngram size. Larger value will decrease the false positive of long documents args: *kwargs:

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
781    def apply(self, doc: Document) -> Document:
782        ratio = self.compute_character_repetition_ratio(doc.text, self.ngram_size)
783        if ratio >= self.threshold:
784            doc.is_rejected = True
785        return doc

Definition of filter behavior.

In this method, the filter will modify document.text, or set document.is_rejected = True to discard the document.

Do not define a filter that changes both document.text and document.token

Parameters

document : Document Input document

Returns

Document Processed Document

@staticmethod
def compute_character_repetition_ratio(document: str, character_repetition_length: int) -> float:
787    @staticmethod
788    def compute_character_repetition_ratio(
789        document: str, character_repetition_length: int
790    ) -> float:
791        def get_freq_character_ngrams(document: str, n: int) -> Dict[str, int]:
792            character_ngrams: List[str] = [
793                document[i : i + n] for i in range(len(document) - n + 1)
794            ]
795            freq_character_ngrams_dict: Dict[str, int] = {}
796            for character_ngram in character_ngrams:
797                freq_character_ngrams_dict[character_ngram] = (
798                    freq_character_ngrams_dict.get(character_ngram, 0) + 1
799                )
800            return freq_character_ngrams_dict
801
802        freq_character_ngrams_dict = get_freq_character_ngrams(
803            document, character_repetition_length
804        )
805        if len(freq_character_ngrams_dict) == 0:
806            return 0.0
807        freq_character_ngrams: List[int] = list(freq_character_ngrams_dict.values())
808        freq_character_ngrams = sorted(freq_character_ngrams, reverse=True)
809        val_one = len([el for el in freq_character_ngrams if el == 1])
810        num_rep_character_ngrams = min(
811            int(np.sqrt(len(freq_character_ngrams))),
812            len(freq_character_ngrams) - val_one,
813        )
814        character_repetition_ratio = sum(freq_character_ngrams[:num_rep_character_ngrams]) / sum(
815            freq_character_ngrams
816        )
817        return character_repetition_ratio
class WordRepetitionRatioFilter(hojichar.core.filter_interface.Filter):
820class WordRepetitionRatioFilter(Filter):
821    """
822    [!CAUTION] This filter requires `fugashi` package. Please install it
823    by `pip install 'hojichar[all]'`.
824
825    単語Ngramの重なり率(文書中で重複する単語Ngramが占める割合)を計算して、重なりの大きいものを弾くためのフィルタ.
826    BigScienceで採用されていた前処理を参考にしている.
827
828    名詞が連打されているような広告テキストを取り除くのに有効な様子
829    まともな文書がたまたま2回繰り返されている場合もあり、これを取り除いて良いのかは分からない
830    例:
831    "ウェブ\n本文: ニコンの上昇率16%超える、今3月期は経常76%の大幅増益見込む(ニコン) 2013年05月10日[minkabu PRESS] - みんなの株式 (みんかぶ)\n2013/05/10(10:57)
832    ニコン<7731.T>が急騰、寄り付き直後に前日比355円高の2537円まで買い上げ
833    られ、上昇率は16%を超えた。外国為替市場で円が1ドル100円台、1ユーロ131円台に入るなど急速に円安が進み、輸出株が軒並み高になる
834    なか、9日取引終了後に発表した前年3月期決算で、今3月期は2ケタ近い増収で大幅増益を見込んだことが買い気を強めさせた。連結売上
835    高は前期比9.8%増の1兆1100億円、経常利益75.8%増の850億円を予想。前期は半導体、電子部品の低迷が足かせになり、2ケタ増収ながら
836    経常46%の大幅減益になったが、レンズ交換式デジタルカメラの拡大や液晶ディスプレイの回復で収益が急回復する。ニコンの株価は10時
837    56分現在2491円(△309円)出所:株経通信(株式会社みんかぶ)\n2013/05/10 - ニコン(7731) の関連ニュース。 ニコン<7731.T>が急騰、寄
838    り付き直後に前日比355円高の2537円まで買い上げられ、上昇率は16%を超えた。外国為替市場で円が1ドル100円台、1ユーロ131円台に入
839    るなど急速に円安が進み、輸出株が軒並み高になるなか、9日取引終了後に発表した前年3月期決算で、今3月期は2ケタ近い増収で大幅増
840    益を見込んだことが買い気を強めさせた。連結売上高は前期比9.8%増の1兆1100億円、経常利益75.8%増の850億円を予想。前期は半導体、
841    電子部品の低迷が足かせになり、2ケタ増収ながら経常46%の大幅減益になったが、レンズ交換式デジタルカメラの拡大や液晶ディスプレ
842    イの回復で収益が急回"
843    """  # noqa: E501
844
845    def __init__(
846        self, threshold: float = 0.40, ngram_size: int = 7, *args: Any, **kwargs: Any
847    ) -> None:
848        """
849
850        Args:
851            threshold: document whose character repetition ratio is higher than this value will be discarded
852            ngram_size: character ngram size. Larger value will decrease the false positive of long documents
853            *args:
854            **kwargs:
855        """  # noqa: E501
856        super().__init__(*args, **kwargs)
857        assert (
858            is_loaded_extras
859        ), "fugashi is required for this filter. Try pip install 'hojichar[all]'"
860
861        self.threshold = threshold
862        self.ngram_size = ngram_size
863        self.tagger = Tagger("-Owakati")
864
865    def apply(self, doc: Document) -> Document:
866        ratio = self.compute_word_repetition_ratio(doc.text, self.ngram_size)
867        if ratio >= self.threshold:
868            doc.is_rejected = True
869        return doc
870
871    def compute_word_repetition_ratio(self, document: str, word_repetition_length: int) -> float:
872        def get_freq_word_ngrams(document: str, n: int) -> Dict[str, int]:
873            # tokenizing given document
874            words = [w.surface for w in self.tagger(document)]
875            word_ngrams = [" ".join(words[i : i + n]) for i in range(len(words) - n + 1)]
876            freq_word_ngrams: Dict[str, int] = {}
877            for word_ngram in word_ngrams:
878                freq_word_ngrams[word_ngram] = freq_word_ngrams.get(word_ngram, 0) + 1
879            return freq_word_ngrams
880
881        freq_word_ngrams_dict = get_freq_word_ngrams(document, word_repetition_length)
882        if len(freq_word_ngrams_dict) == 0:
883            return 0
884        freq_word_ngrams = list(freq_word_ngrams_dict.values())
885        word_repetition_ratio = sum(freq for freq in freq_word_ngrams if freq > 1) / sum(
886            freq_word_ngrams
887        )
888
889        return word_repetition_ratio

[!CAUTION] This filter requires fugashi package. Please install it by pip install 'hojichar[all]'.

単語Ngramの重なり率(文書中で重複する単語Ngramが占める割合)を計算して、重なりの大きいものを弾くためのフィルタ.
BigScienceで採用されていた前処理を参考にしている.

名詞が連打されているような広告テキストを取り除くのに有効な様子
まともな文書がたまたま2回繰り返されている場合もあり、これを取り除いて良いのかは分からない
例:
"ウェブ

本文: ニコンの上昇率16%超える、今3月期は経常76%の大幅増益見込む(ニコン) 2013年05月10日[minkabu PRESS] - みんなの株式 (みんかぶ) 2013/05/10(10:57) ニコン<7731.T>が急騰、寄り付き直後に前日比355円高の2537円まで買い上げ られ、上昇率は16%を超えた。外国為替市場で円が1ドル100円台、1ユーロ131円台に入るなど急速に円安が進み、輸出株が軒並み高になる なか、9日取引終了後に発表した前年3月期決算で、今3月期は2ケタ近い増収で大幅増益を見込んだことが買い気を強めさせた。連結売上 高は前期比9.8%増の1兆1100億円、経常利益75.8%増の850億円を予想。前期は半導体、電子部品の低迷が足かせになり、2ケタ増収ながら 経常46%の大幅減益になったが、レンズ交換式デジタルカメラの拡大や液晶ディスプレイの回復で収益が急回復する。ニコンの株価は10時 56分現在2491円(△309円)出所:株経通信(株式会社みんかぶ) 2013/05/10 - ニコン(7731) の関連ニュース。 ニコン<7731.T>が急騰、寄 り付き直後に前日比355円高の2537円まで買い上げられ、上昇率は16%を超えた。外国為替市場で円が1ドル100円台、1ユーロ131円台に入 るなど急速に円安が進み、輸出株が軒並み高になるなか、9日取引終了後に発表した前年3月期決算で、今3月期は2ケタ近い増収で大幅増 益を見込んだことが買い気を強めさせた。連結売上高は前期比9.8%増の1兆1100億円、経常利益75.8%増の850億円を予想。前期は半導体、 電子部品の低迷が足かせになり、2ケタ増収ながら経常46%の大幅減益になったが、レンズ交換式デジタルカメラの拡大や液晶ディスプレ イの回復で収益が急回"

WordRepetitionRatioFilter( threshold: float = 0.4, ngram_size: int = 7, *args: Any, **kwargs: Any)
845    def __init__(
846        self, threshold: float = 0.40, ngram_size: int = 7, *args: Any, **kwargs: Any
847    ) -> None:
848        """
849
850        Args:
851            threshold: document whose character repetition ratio is higher than this value will be discarded
852            ngram_size: character ngram size. Larger value will decrease the false positive of long documents
853            *args:
854            **kwargs:
855        """  # noqa: E501
856        super().__init__(*args, **kwargs)
857        assert (
858            is_loaded_extras
859        ), "fugashi is required for this filter. Try pip install 'hojichar[all]'"
860
861        self.threshold = threshold
862        self.ngram_size = ngram_size
863        self.tagger = Tagger("-Owakati")

Args: threshold: document whose character repetition ratio is higher than this value will be discarded ngram_size: character ngram size. Larger value will decrease the false positive of long documents args: *kwargs:

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
865    def apply(self, doc: Document) -> Document:
866        ratio = self.compute_word_repetition_ratio(doc.text, self.ngram_size)
867        if ratio >= self.threshold:
868            doc.is_rejected = True
869        return doc

Definition of filter behavior.

In this method, the filter will modify document.text, or set document.is_rejected = True to discard the document.

Do not define a filter that changes both document.text and document.token

Parameters

document : Document Input document

Returns

Document Processed Document

def compute_word_repetition_ratio(self, document: str, word_repetition_length: int) -> float:
871    def compute_word_repetition_ratio(self, document: str, word_repetition_length: int) -> float:
872        def get_freq_word_ngrams(document: str, n: int) -> Dict[str, int]:
873            # tokenizing given document
874            words = [w.surface for w in self.tagger(document)]
875            word_ngrams = [" ".join(words[i : i + n]) for i in range(len(words) - n + 1)]
876            freq_word_ngrams: Dict[str, int] = {}
877            for word_ngram in word_ngrams:
878                freq_word_ngrams[word_ngram] = freq_word_ngrams.get(word_ngram, 0) + 1
879            return freq_word_ngrams
880
881        freq_word_ngrams_dict = get_freq_word_ngrams(document, word_repetition_length)
882        if len(freq_word_ngrams_dict) == 0:
883            return 0
884        freq_word_ngrams = list(freq_word_ngrams_dict.values())
885        word_repetition_ratio = sum(freq for freq in freq_word_ngrams if freq > 1) / sum(
886            freq_word_ngrams
887        )
888
889        return word_repetition_ratio
class DiscardTooManySpecialToken(hojichar.core.filter_interface.Filter):
892class DiscardTooManySpecialToken(Filter):
893    """
894    [!CAUTION] This filter requires `emoji` package. Please install it
895    by `pip install 'hojichar[all]'`.
896
897    句読点を含む記号、空白、絵文字、その他特殊な文字を一定の割合以上含むような文書を取り除くためのフィルタ
898    元実装: BigScience https://github.com/bigscience-workshop/data-preparation/blob/9d0588419073cc5bf0fb92b58f37f2a1016572c3/preprocessing/training/01b_oscar_cleaning_and_filtering/parameters_filtering.py#L5-L16  # noqa: E501
899    """
900
901    def __init__(self, threshold: float = 0.4, *args: Any, **kwargs: Any) -> None:
902        """
903
904        Args:
905            threshold: document whose special token ratio is higher than this value will be discarded
906            *args:
907            **kwargs:
908        """  # noqa: E501
909        super().__init__(*args, **kwargs)
910
911        # digits are not regarded as special tokens
912        # otherwise many false positives are made, i.e., good documents discarded
913        main_special_characters = string.punctuation + string.whitespace  # + string.digits
914        other_special_characters = (
915            "’ “— ™ – •‘œ    ˜ ‚ƒ„’“”–▬…✦�­£​•€«»°·═"
916            "×士^˘⇓()§″′´¿−±∈¢ø‚„½¼¾¹²³―⁃,ˌ¸‹›ʺˈʻ¦‐⠀‰……‑≤≥‖"
917            "◆●■►▼▲▴∆▻¡★☆✱ːº。¯˜¥ɪ≈†:⁄♡✓⊕․.⋅÷1‟;،、¨ाাी्े◦˚"
918            "゜ʼ≖ʼ¤℃√!?【】‿∞➤~πه۩☛₨➩☻๑٪♥ıॽ《‘©﴿٬?▷Г♫∟™ª₪®「—❖"
919            "」﴾》�"
920        )
921
922        en_emoji = emoji.EMOJI_DATA.keys()
923
924        special_characters_default = set(main_special_characters + other_special_characters)
925        special_characters_default.update(en_emoji)
926        self.special_characters = special_characters_default
927
928        self.threshold = threshold
929
930    def _compute_special_characters_ratio(self, text: str) -> float:
931        if len(text) == 0:
932            return 0
933
934        special_characters_ratio = len(
935            [char for char in text if char in self.special_characters]
936        ) / len(text)
937        return special_characters_ratio
938
939    def apply(self, doc: Document) -> Document:
940        special_characters_ratio = self._compute_special_characters_ratio(doc.text)
941
942        if special_characters_ratio > self.threshold:
943            doc.is_rejected = True
944        return doc

[!CAUTION] This filter requires emoji package. Please install it by pip install 'hojichar[all]'.

句読点を含む記号、空白、絵文字、その他特殊な文字を一定の割合以上含むような文書を取り除くためのフィルタ 元実装: BigScience https://github.com/bigscience-workshop/data-preparation/blob/9d0588419073cc5bf0fb92b58f37f2a1016572c3/preprocessing/training/01b_oscar_cleaning_and_filtering/parameters_filtering.py#L5-L16 # noqa: E501

DiscardTooManySpecialToken(threshold: float = 0.4, *args: Any, **kwargs: Any)
901    def __init__(self, threshold: float = 0.4, *args: Any, **kwargs: Any) -> None:
902        """
903
904        Args:
905            threshold: document whose special token ratio is higher than this value will be discarded
906            *args:
907            **kwargs:
908        """  # noqa: E501
909        super().__init__(*args, **kwargs)
910
911        # digits are not regarded as special tokens
912        # otherwise many false positives are made, i.e., good documents discarded
913        main_special_characters = string.punctuation + string.whitespace  # + string.digits
914        other_special_characters = (
915            "’ “— ™ – •‘œ    ˜ ‚ƒ„’“”–▬…✦�­£​•€«»°·═"
916            "×士^˘⇓()§″′´¿−±∈¢ø‚„½¼¾¹²³―⁃,ˌ¸‹›ʺˈʻ¦‐⠀‰……‑≤≥‖"
917            "◆●■►▼▲▴∆▻¡★☆✱ːº。¯˜¥ɪ≈†:⁄♡✓⊕․.⋅÷1‟;،、¨ाাी्े◦˚"
918            "゜ʼ≖ʼ¤℃√!?【】‿∞➤~πه۩☛₨➩☻๑٪♥ıॽ《‘©﴿٬?▷Г♫∟™ª₪®「—❖"
919            "」﴾》�"
920        )
921
922        en_emoji = emoji.EMOJI_DATA.keys()
923
924        special_characters_default = set(main_special_characters + other_special_characters)
925        special_characters_default.update(en_emoji)
926        self.special_characters = special_characters_default
927
928        self.threshold = threshold

Args: threshold: document whose special token ratio is higher than this value will be discarded args: *kwargs:

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
939    def apply(self, doc: Document) -> Document:
940        special_characters_ratio = self._compute_special_characters_ratio(doc.text)
941
942        if special_characters_ratio > self.threshold:
943            doc.is_rejected = True
944        return doc

Definition of filter behavior.

In this method, the filter will modify document.text, or set document.is_rejected = True to discard the document.

Do not define a filter that changes both document.text and document.token

Parameters

document : Document Input document

Returns

Document Processed Document

class SingleCharacterRepetitionFilter(hojichar.core.filter_interface.Filter):
947class SingleCharacterRepetitionFilter(Filter):
948    """
949    単一文字が大量に繰り返されているような文書を取り除くためのフィルタ
950    そのような文書はノイズである可能性が高いため
951    参考: BigScienceプロジェクトによると、oscarデータセットの中にバックスラッシュだけを2M個含むような文書が含まれていたらしい
952    https://github.com/bigscience-workshop/bigscience/blob/master/train/tr8-104B-wide/chronicles.md#2m-backslash-only-samples-in-our-dataset  # noqa: E501
953    """
954
955    def __init__(
956        self,
957        threshold: int = 200,
958        *args: Any,
959        **kwargs: Any,
960    ) -> None:
961        """
962        Args:
963            threshold: The document is removed if character is repeated for this value or more
964            *args:
965            **kwargs:
966        """
967        super().__init__(*args, **kwargs)
968        self.threshold = threshold
969
970    def _is_repeat_contained(self, text: str) -> bool:
971        groups = groupby(text)
972        is_repeat_contained = any(sum(1 for _ in group) >= self.threshold for _, group in groups)
973        return is_repeat_contained
974
975    def apply(self, doc: Document) -> Document:
976        if self._is_repeat_contained(doc.text):
977            doc.is_rejected = True
978        return doc

単一文字が大量に繰り返されているような文書を取り除くためのフィルタ そのような文書はノイズである可能性が高いため 参考: BigScienceプロジェクトによると、oscarデータセットの中にバックスラッシュだけを2M個含むような文書が含まれていたらしい https://github.com/bigscience-workshop/bigscience/blob/master/train/tr8-104B-wide/chronicles.md#2m-backslash-only-samples-in-our-dataset # noqa: E501

SingleCharacterRepetitionFilter(threshold: int = 200, *args: Any, **kwargs: Any)
955    def __init__(
956        self,
957        threshold: int = 200,
958        *args: Any,
959        **kwargs: Any,
960    ) -> None:
961        """
962        Args:
963            threshold: The document is removed if character is repeated for this value or more
964            *args:
965            **kwargs:
966        """
967        super().__init__(*args, **kwargs)
968        self.threshold = threshold

Args: threshold: The document is removed if character is repeated for this value or more args: *kwargs:

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
975    def apply(self, doc: Document) -> Document:
976        if self._is_repeat_contained(doc.text):
977            doc.is_rejected = True
978        return doc

Definition of filter behavior.

In this method, the filter will modify document.text, or set document.is_rejected = True to discard the document.

Do not define a filter that changes both document.text and document.token

Parameters

document : Document Input document

Returns

Document Processed Document

class DiscardTooManyEndingEllipsis(hojichar.core.filter_interface.Filter):
 981class DiscardTooManyEndingEllipsis(Filter):
 982    """
 983    ellipsisで終わるような行が大量に含まれるような文書を取り除くためのフィルタです.
 984    ellipsisとしては ... と … を用いている
 985    同様のフィルタが RedPajama v2で用いられています.
 986
 987    例として, 以下のような文書を検知します.
 988    ```
 989    ペアーズは女性、という驚愕の過食が出ているのをごアラサーですか。時代から付...
 990    バツイチアラフォー 婚活ち女性の特徴と子持な付...
 991    ```
 992
 993    デフォルトではしきい値を0.7としているが, これはC4から0.1%を削るような設定であり、
 994    precisionを重視した設定です.
 995    """
 996
 997    def __init__(
 998        self,
 999        threshold: float = 0.7,
1000        *args: Any,
1001        **kwargs: Any,
1002    ) -> None:
1003        """
1004        Args:
1005            threshold: The document is removed if ratio of lines ending with ellipsis is higher than this value
1006            *args:
1007            **kwargs:
1008        """  # noqa: E501
1009        super().__init__(*args, **kwargs)
1010        self.threshold = threshold
1011        self.ellipsis_pattern = re.compile(r"(\.{3}|…)\n")  # matches ...\n and …\n
1012
1013    def apply(self, doc: Document) -> Document:
1014        ellipsis_count = len(self.ellipsis_pattern.findall(doc.text))
1015        newline_count = max(doc.text.count("\n"), 1)  # avoid zero division
1016        ellipsis_ratio = ellipsis_count / newline_count
1017
1018        if ellipsis_ratio > self.threshold:
1019            doc.is_rejected = True
1020        return doc

ellipsisで終わるような行が大量に含まれるような文書を取り除くためのフィルタです. ellipsisとしては ... と … を用いている 同様のフィルタが RedPajama v2で用いられています.

例として, 以下のような文書を検知します.

ペアーズは女性、という驚愕の過食が出ているのをごアラサーですか。時代から付...
バツイチアラフォー 婚活ち女性の特徴と子持な付...

デフォルトではしきい値を0.7としているが, これはC4から0.1%を削るような設定であり、 precisionを重視した設定です.

DiscardTooManyEndingEllipsis(threshold: float = 0.7, *args: Any, **kwargs: Any)
 997    def __init__(
 998        self,
 999        threshold: float = 0.7,
1000        *args: Any,
1001        **kwargs: Any,
1002    ) -> None:
1003        """
1004        Args:
1005            threshold: The document is removed if ratio of lines ending with ellipsis is higher than this value
1006            *args:
1007            **kwargs:
1008        """  # noqa: E501
1009        super().__init__(*args, **kwargs)
1010        self.threshold = threshold
1011        self.ellipsis_pattern = re.compile(r"(\.{3}|…)\n")  # matches ...\n and …\n

Args: threshold: The document is removed if ratio of lines ending with ellipsis is higher than this value args: *kwargs:

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
1013    def apply(self, doc: Document) -> Document:
1014        ellipsis_count = len(self.ellipsis_pattern.findall(doc.text))
1015        newline_count = max(doc.text.count("\n"), 1)  # avoid zero division
1016        ellipsis_ratio = ellipsis_count / newline_count
1017
1018        if ellipsis_ratio > self.threshold:
1019            doc.is_rejected = True
1020        return doc

Definition of filter behavior.

In this method, the filter will modify document.text, or set document.is_rejected = True to discard the document.

Do not define a filter that changes both document.text and document.token

Parameters

document : Document Input document

Returns

Document Processed Document

class DiscardTooShortLines(hojichar.core.filter_interface.Filter):
1023class DiscardTooShortLines(Filter):
1024    """
1025    短い行を大量に含む文書を捨てるためのフィルタです.
1026
1027    メニューバーやパンくずリストのような要素を大量に含む文書を取り除くのに有効です.
1028    """
1029
1030    def __init__(self, threshold: float = 0.5, *args: Any, **kwargs: Any) -> None:
1031        """
1032        Args:
1033            threshold: The document is removed if the ratio of short (<10 chars) lines are more than this value.
1034            *args:
1035            **kwargs:
1036        """  # noqa: E501
1037        super().__init__(*args, **kwargs)
1038        self.threshold = threshold
1039        # この値は適当に決め打ち
1040        self.minimum_line_length = 10
1041
1042    def apply(self, doc: Document) -> Document:
1043        lines = [len(x) for x in doc.text.split("\n")]
1044        short_lines = [x for x in lines if x <= self.minimum_line_length]
1045        if (len(short_lines) / len(lines)) > self.threshold:
1046            doc.is_rejected = True
1047        return doc

短い行を大量に含む文書を捨てるためのフィルタです.

メニューバーやパンくずリストのような要素を大量に含む文書を取り除くのに有効です.

DiscardTooShortLines(threshold: float = 0.5, *args: Any, **kwargs: Any)
1030    def __init__(self, threshold: float = 0.5, *args: Any, **kwargs: Any) -> None:
1031        """
1032        Args:
1033            threshold: The document is removed if the ratio of short (<10 chars) lines are more than this value.
1034            *args:
1035            **kwargs:
1036        """  # noqa: E501
1037        super().__init__(*args, **kwargs)
1038        self.threshold = threshold
1039        # この値は適当に決め打ち
1040        self.minimum_line_length = 10

Args: threshold: The document is removed if the ratio of short (<10 chars) lines are more than this value. args: *kwargs:

def apply( self, doc: hojichar.core.models.Document) -> hojichar.core.models.Document:
1042    def apply(self, doc: Document) -> Document:
1043        lines = [len(x) for x in doc.text.split("\n")]
1044        short_lines = [x for x in lines if x <= self.minimum_line_length]
1045        if (len(short_lines) / len(lines)) > self.threshold:
1046            doc.is_rejected = True
1047        return doc

Definition of filter behavior.

In this method, the filter will modify document.text, or set document.is_rejected = True to discard the document.

Do not define a filter that changes both document.text and document.token

Parameters

document : Document Input document

Returns

Document Processed Document