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

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

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

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:
218    def apply(self, document: Document) -> Document:
219        """
220        >>> JSONDumper()("hojichar")
221        '{"text": "hojichar"}'
222        """
223        text = document.text
224        if self.dump_reason:
225            if self.export_extras:
226                document.text = json.dumps(
227                    {
228                        "text": text,
229                        "is_rejected": document.is_rejected,
230                        "reason": document.reject_reason,
231                        "extras": document.extras,
232                    },
233                    ensure_ascii=False,
234                )
235            else:
236                document.text = json.dumps(
237                    {
238                        "text": text,
239                        "is_rejected": document.is_rejected,
240                        "reason": document.reject_reason,
241                    },
242                    ensure_ascii=False,
243                )
244        else:
245            if self.export_extras:
246                document.text = json.dumps(
247                    {
248                        "text": text,
249                        "extras": document.extras,
250                    },
251                    ensure_ascii=False,
252                )
253            else:
254                document.text = json.dumps({"text": text}, ensure_ascii=False)
255        return document
>>> JSONDumper()("hojichar")
'{"text": "hojichar"}'
class DocumentLengthFilter(hojichar.core.filter_interface.Filter):
258class DocumentLengthFilter(Filter):
259    """
260    `min_doc_len`, `max_doc_len` で指定した上限・下限の範囲内にないドキュメントを破棄します.
261    デフォルトでは 200字 以上 50000字以内のテキストが受理されます.
262    """
263
264    def __init__(
265        self,
266        min_doc_len: Optional[int] = None,
267        max_doc_len: Optional[int] = None,
268        *args: Any,
269        **kwargs: Any,
270    ) -> None:
271        super().__init__(*args, **kwargs)
272
273        self.min_doc_len = min_doc_len
274        self.max_doc_len = max_doc_len
275
276    def apply(self, doc: Document) -> Document:
277        """
278        >>> DocumentLengthFilter(min_doc_len=5).apply(Document("1234")).is_rejected
279        True
280        """
281        doc_len = len(doc.text)
282        if self.min_doc_len is not None:
283            if doc_len < self.min_doc_len:
284                doc.is_rejected = True
285        if self.max_doc_len is not None:
286            if self.max_doc_len < doc_len:
287                doc.is_rejected = True
288        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)
264    def __init__(
265        self,
266        min_doc_len: Optional[int] = None,
267        max_doc_len: Optional[int] = None,
268        *args: Any,
269        **kwargs: Any,
270    ) -> None:
271        super().__init__(*args, **kwargs)
272
273        self.min_doc_len = min_doc_len
274        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:
276    def apply(self, doc: Document) -> Document:
277        """
278        >>> DocumentLengthFilter(min_doc_len=5).apply(Document("1234")).is_rejected
279        True
280        """
281        doc_len = len(doc.text)
282        if self.min_doc_len is not None:
283            if doc_len < self.min_doc_len:
284                doc.is_rejected = True
285        if self.max_doc_len is not None:
286            if self.max_doc_len < doc_len:
287                doc.is_rejected = True
288        return doc
>>> DocumentLengthFilter(min_doc_len=5).apply(Document("1234")).is_rejected
True
class NgWordsFilterJa(hojichar.core.filter_interface.Filter):
291class NgWordsFilterJa(Filter):
292    """
293    日本語のNGワード(および不適切語)を含む文書を破棄します.
294    `dict_path` で指定したファイルから, キーワードのリストを得ます.
295    ファイルは単語が改行で羅列されたテキストファイルです.
296
297    `ignore_confused` を `True` にすると,
298    偽陽性を軽減するために, カタカナのNGワードは前後にカタカナが無い場合のみNG判定されます.
299    デフォルト値は `False` です.
300    """
301
302    def __init__(
303        self,
304        dict_path: Union[str, PathLike],
305        ignore_confused: bool = False,
306        *args: Any,
307        **kwargs: Any,
308    ) -> None:
309        super().__init__(*args, **kwargs)
310
311        with open(dict_path, encoding="utf-8") as fp:
312            ng_words = fp.read().split("\n")
313        ng_words = [w.strip() for w in ng_words if not len(w) == 0]
314
315        if ignore_confused:
316            words_katakana = []
317            words_not_katakana = []
318            for w in ng_words:
319                if re.fullmatch(r"[ァ-ヴー]+", w):
320                    words_katakana.append(re.escape(w))
321                else:
322                    words_not_katakana.append(re.escape(w))
323            katakana_pat = "|".join(words_katakana)
324            katakana_pat = rf"(?<![ァ-ヴー])({katakana_pat})(?![ァ-ヴー])"
325            pat = "|".join(words_not_katakana) + "|" + katakana_pat
326            self.keyword_pat = re.compile(pat)
327        else:
328            ng_words = [re.escape(w) for w in ng_words]
329            pat = "|".join(ng_words)
330            self.keyword_pat = re.compile(pat)
331
332    def apply(self, doc: Document) -> Document:
333        regex_match = self.keyword_pat.search(doc.text)
334        if regex_match:
335            doc.is_rejected = True
336            self.matched_text = regex_match.group()
337            self.matched_text_neighbor = doc.text[
338                regex_match.start() - 20 : regex_match.end() + 20
339            ]
340
341        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)
302    def __init__(
303        self,
304        dict_path: Union[str, PathLike],
305        ignore_confused: bool = False,
306        *args: Any,
307        **kwargs: Any,
308    ) -> None:
309        super().__init__(*args, **kwargs)
310
311        with open(dict_path, encoding="utf-8") as fp:
312            ng_words = fp.read().split("\n")
313        ng_words = [w.strip() for w in ng_words if not len(w) == 0]
314
315        if ignore_confused:
316            words_katakana = []
317            words_not_katakana = []
318            for w in ng_words:
319                if re.fullmatch(r"[ァ-ヴー]+", w):
320                    words_katakana.append(re.escape(w))
321                else:
322                    words_not_katakana.append(re.escape(w))
323            katakana_pat = "|".join(words_katakana)
324            katakana_pat = rf"(?<![ァ-ヴー])({katakana_pat})(?![ァ-ヴー])"
325            pat = "|".join(words_not_katakana) + "|" + katakana_pat
326            self.keyword_pat = re.compile(pat)
327        else:
328            ng_words = [re.escape(w) for w in ng_words]
329            pat = "|".join(ng_words)
330            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:
332    def apply(self, doc: Document) -> Document:
333        regex_match = self.keyword_pat.search(doc.text)
334        if regex_match:
335            doc.is_rejected = True
336            self.matched_text = regex_match.group()
337            self.matched_text_neighbor = doc.text[
338                regex_match.start() - 20 : regex_match.end() + 20
339            ]
340
341        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):
344class NgWordsFilterEn(Filter):
345    """
346    英語のNGワード(および不適切語)を含む文書を破棄します.
347    `dict_path` で指定したファイルから, キーワードのリストを得ます.
348    ファイルは単語が改行で羅列されたテキストファイルです.
349    """
350
351    def __init__(self, dict_path: Union[str, PathLike], *args: Any, **kwargs: Any) -> None:
352        super().__init__(*args, **kwargs)
353
354        with open(dict_path, encoding="utf-8") as fp:
355            ng_words = fp.read().split("\n")
356        ng_words = [re.escape(w.strip()) for w in ng_words if not len(w) == 0]
357        pat = "|".join(ng_words)
358        # 英語のパターンにマッチするようにしている, \s[単語]\s や [単語]. [単語], などにマッチ.
359        self.keyword_pat = re.compile(rf"(?:^| )({pat})(?:( |,|\.)|$)", re.IGNORECASE)
360
361    def apply(self, doc: Document) -> Document:
362        if self.keyword_pat.search(doc.text):
363            doc.is_rejected = True
364        return doc

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

NgWordsFilterEn(dict_path: Union[str, os.PathLike], *args: Any, **kwargs: Any)
351    def __init__(self, dict_path: Union[str, PathLike], *args: Any, **kwargs: Any) -> None:
352        super().__init__(*args, **kwargs)
353
354        with open(dict_path, encoding="utf-8") as fp:
355            ng_words = fp.read().split("\n")
356        ng_words = [re.escape(w.strip()) for w in ng_words if not len(w) == 0]
357        pat = "|".join(ng_words)
358        # 英語のパターンにマッチするようにしている, \s[単語]\s や [単語]. [単語], などにマッチ.
359        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:
361    def apply(self, doc: Document) -> Document:
362        if self.keyword_pat.search(doc.text):
363            doc.is_rejected = True
364        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):
367class DiscardAdultContentJa(NgWordsFilterJa):
368    """
369    日本語のアダルトキーワード(および不適切語)を含む文書を破棄します.
370    `dict_path` で指定したファイルから, キーワードのリストを得ます.
371    ファイルは単語が改行で羅列されたテキストファイルです.
372    デフォルトの`dict_path` は /hojichar/dict/adult_keywords_ja.txt です.
373    """
374
375    def __init__(
376        self,
377        dict_path: Union[str, PathLike] = BASE_PATH / "dict/adult_keywords_ja.txt",
378        *args: Any,
379        **kwargs: Any,
380    ) -> None:
381        super().__init__(dict_path, *args, **kwargs)
382
383    def apply(self, doc: Document) -> Document:
384        """
385        >>> DiscardAdultContentJa().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected
386        True
387
388        >>> DiscardAdultContentJa().apply(Document("ほうじ茶")).is_rejected
389        False
390
391        挙動は正しいが誤検知しているケース. 他にも, サック in リュックサック,
392        >>> DiscardAdultContentJa().apply(Document("アスパラガス")).is_rejected \
393        # Matching with NG keyword "アス"
394        True
395        """
396        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)
375    def __init__(
376        self,
377        dict_path: Union[str, PathLike] = BASE_PATH / "dict/adult_keywords_ja.txt",
378        *args: Any,
379        **kwargs: Any,
380    ) -> None:
381        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:
383    def apply(self, doc: Document) -> Document:
384        """
385        >>> DiscardAdultContentJa().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected
386        True
387
388        >>> DiscardAdultContentJa().apply(Document("ほうじ茶")).is_rejected
389        False
390
391        挙動は正しいが誤検知しているケース. 他にも, サック in リュックサック,
392        >>> DiscardAdultContentJa().apply(Document("アスパラガス")).is_rejected \
393        # Matching with NG keyword "アス"
394        True
395        """
396        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):
399class DiscardAdultContentEn(NgWordsFilterEn):
400    """
401    英語のアダルトキーワード(および不適切語)を含む文書を破棄します.
402    `dict_path` で指定したファイルから, キーワードのリストを得ます.
403    ファイルは単語が改行で羅列されたテキストファイルです.
404    デフォルトの`dict_path` は /hojichar/dict/adult_keywords_en.txt です.
405    """
406
407    def __init__(
408        self,
409        dict_path: Union[str, PathLike] = BASE_PATH / "dict/adult_keywords_en.txt",
410        *args: Any,
411        **kwargs: Any,
412    ) -> None:
413        super().__init__(dict_path, *args, **kwargs)
414
415    def apply(self, doc: Document) -> Document:
416        """
417        >>> DiscardAdultContentEn().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected
418        True
419
420        >>> DiscardAdultContentEn().apply(Document("hojichar")).is_rejected
421        False
422        """
423        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)
407    def __init__(
408        self,
409        dict_path: Union[str, PathLike] = BASE_PATH / "dict/adult_keywords_en.txt",
410        *args: Any,
411        **kwargs: Any,
412    ) -> None:
413        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:
415    def apply(self, doc: Document) -> Document:
416        """
417        >>> DiscardAdultContentEn().apply(Document("<TEST_STRING_OF_ADULT_KEYWORD>")).is_rejected
418        True
419
420        >>> DiscardAdultContentEn().apply(Document("hojichar")).is_rejected
421        False
422        """
423        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):
426class DiscardDiscriminationContentJa(NgWordsFilterJa):
427    """
428    日本語の差別キーワード(および不適切語)を含む文書を破棄します.
429    `dict_path` で指定したファイルから, キーワードのリストを得ます.
430    ファイルは単語が改行で羅列されたテキストファイルです.
431    デフォルトの`dict_path` は /hojichar/dict/discrimination_keywords_ja.txt です.
432    """
433
434    def __init__(
435        self,
436        dict_path: Union[str, PathLike] = BASE_PATH / "dict/discrimination_keywords_ja.txt",
437        *args: Any,
438        **kwargs: Any,
439    ):
440        super().__init__(dict_path, *args, **kwargs)
441
442    def apply(self, doc: Document) -> Document:
443        """
444        >>> DiscardDiscriminationContentJa().\
445            apply(Document("<TEST_STRING_OF_DISCRIMINATION_KEYWORD>")).is_rejected
446        True
447
448        >>> DiscardDiscriminationContentJa().apply(Document("ほうじ茶")).is_rejected
449        False
450        """
451        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)
434    def __init__(
435        self,
436        dict_path: Union[str, PathLike] = BASE_PATH / "dict/discrimination_keywords_ja.txt",
437        *args: Any,
438        **kwargs: Any,
439    ):
440        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:
442    def apply(self, doc: Document) -> Document:
443        """
444        >>> DiscardDiscriminationContentJa().\
445            apply(Document("<TEST_STRING_OF_DISCRIMINATION_KEYWORD>")).is_rejected
446        True
447
448        >>> DiscardDiscriminationContentJa().apply(Document("ほうじ茶")).is_rejected
449        False
450        """
451        return super().apply(doc)
>>> DiscardDiscriminationContentJa().            apply(Document("<TEST_STRING_OF_DISCRIMINATION_KEYWORD>")).is_rejected
True
>>> DiscardDiscriminationContentJa().apply(Document("ほうじ茶")).is_rejected
False
class DiscardViolenceContentJa(NgWordsFilterJa):
454class DiscardViolenceContentJa(NgWordsFilterJa):
455    """
456    日本語の暴力・脅迫を示唆するキーワードを含む文書を破棄します.
457    `dict_path` で指定したファイルから, キーワードのリストを得ます.
458    ファイルは単語が改行で羅列されたテキストファイルです.
459    デフォルトの`dict_path` は /hojichar/dict/violence_keywords_ja.txt です.
460    """
461
462    def __init__(
463        self,
464        dict_path: Union[str, PathLike] = BASE_PATH / "dict/violence_keywords_ja.txt",
465        *args: Any,
466        **kwargs: Any,
467    ) -> None:
468        super().__init__(dict_path, *args, **kwargs)
469
470    def apply(self, doc: Document) -> Document:
471        """
472        >>> DiscardViolenceContentJa()\
473            .apply(Document("<TEST_STRING_OF_VIOLENCE_KEYWORD>")).is_rejected
474        True
475
476        >>> DiscardViolenceContentJa().apply(Document("ほうじ茶")).is_rejected
477        False
478        """
479        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)
462    def __init__(
463        self,
464        dict_path: Union[str, PathLike] = BASE_PATH / "dict/violence_keywords_ja.txt",
465        *args: Any,
466        **kwargs: Any,
467    ) -> None:
468        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:
470    def apply(self, doc: Document) -> Document:
471        """
472        >>> DiscardViolenceContentJa()\
473            .apply(Document("<TEST_STRING_OF_VIOLENCE_KEYWORD>")).is_rejected
474        True
475
476        >>> DiscardViolenceContentJa().apply(Document("ほうじ茶")).is_rejected
477        False
478        """
479        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):
482class DiscardBBSComments(Filter):
483    """
484    正規表現 "BBS Pattern" に `max_allow_num` 回よりたくさんマッチする文書を破棄します.
485    `max_allow_num` のデフォルト値は14です.
486    正規表現 "BBS Pattern" は下記のリンクで検証可能です.
487    https://regex101.com/r/ybQvL2/1
488    """
489
490    def __init__(self, max_allowed_num: int = 14, *args: Any, **kwargs: Any) -> None:
491        super().__init__(*args, **kwargs)
492
493        self.max_allowed_num = max_allowed_num
494        self.keyword_pat = re.compile(
495            r"\d{4}[年\.\-\/][\ ]*\d{1,2}[月\.\-\/][\ ]*\d{1,2}[日]*|コメント|SOLD OUT|レビュー|投稿|ページ|\([月火水木金土日]\)|質問|\d+話|楽天市場|-"  # noqa
496        )
497
498    def apply(self, doc: Document) -> Document:
499        """
500        >>> DiscardBBSComments().apply(Document("楽天市場 質問 投稿 コメント レビュー "*3)).is_rejected
501        True
502
503        >>> DiscardBBSComments().apply(Document("鏡餅")).is_rejected
504        False
505        """
506        bbs_factor = self.keyword_pat.findall(doc.text)
507        if len(bbs_factor) > self.max_allowed_num:
508            doc.is_rejected = True
509        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)
490    def __init__(self, max_allowed_num: int = 14, *args: Any, **kwargs: Any) -> None:
491        super().__init__(*args, **kwargs)
492
493        self.max_allowed_num = max_allowed_num
494        self.keyword_pat = re.compile(
495            r"\d{4}[年\.\-\/][\ ]*\d{1,2}[月\.\-\/][\ ]*\d{1,2}[日]*|コメント|SOLD OUT|レビュー|投稿|ページ|\([月火水木金土日]\)|質問|\d+話|楽天市場|-"  # noqa
496        )

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:
498    def apply(self, doc: Document) -> Document:
499        """
500        >>> DiscardBBSComments().apply(Document("楽天市場 質問 投稿 コメント レビュー "*3)).is_rejected
501        True
502
503        >>> DiscardBBSComments().apply(Document("鏡餅")).is_rejected
504        False
505        """
506        bbs_factor = self.keyword_pat.findall(doc.text)
507        if len(bbs_factor) > self.max_allowed_num:
508            doc.is_rejected = True
509        return doc
>>> DiscardBBSComments().apply(Document("楽天市場 質問 投稿 コメント レビュー "*3)).is_rejected
True
>>> DiscardBBSComments().apply(Document("鏡餅")).is_rejected
False
class DiscardAds(hojichar.core.filter_interface.Filter):
512class DiscardAds(Filter):
513    """
514    主に広告キーワードを`max_allow_num`より多く含む文書を破棄します.
515    デフォルトで`max_allow_num` は14です.
516    `dict_path` で指定したファイルから, 広告キーワードのリストを得ます.
517    ファイルは単語が改行で羅列されたテキストファイルです.
518    デフォルトの`dict_path` は /hojichar/dict/advertisement_keywords_ja.txt です.
519    """
520
521    def __init__(
522        self,
523        dict_path: Union[str, PathLike] = BASE_PATH / "dict/advertisement_keywords_ja.txt",
524        max_allowed_num: int = 14,
525        *args: Any,
526        **kwargs: Any,
527    ):
528        super().__init__(*args, **kwargs)
529
530        self.max_allow_num = max_allowed_num
531        with open(dict_path, encoding="utf-8") as fp:
532            ng_words = fp.read().split("\n")
533        ng_words = [re.escape(w.strip()) for w in ng_words if not len(w) == 0]
534        pat = r"|".join(ng_words)
535        self.keyword_pat = re.compile(pat)
536
537    def apply(self, doc: Document) -> Document:
538        """
539        >>> DiscardAds().apply(Document("お問い合わせください 営業時間 よくある質問"*5)).is_rejected
540        True
541
542        >>> DiscardAds().apply(Document("おはよう")).is_rejected
543        False
544        """
545        ads_factor = self.keyword_pat.findall(doc.text)
546        if len(ads_factor) > self.max_allow_num:
547            doc.is_rejected = True
548        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)
521    def __init__(
522        self,
523        dict_path: Union[str, PathLike] = BASE_PATH / "dict/advertisement_keywords_ja.txt",
524        max_allowed_num: int = 14,
525        *args: Any,
526        **kwargs: Any,
527    ):
528        super().__init__(*args, **kwargs)
529
530        self.max_allow_num = max_allowed_num
531        with open(dict_path, encoding="utf-8") as fp:
532            ng_words = fp.read().split("\n")
533        ng_words = [re.escape(w.strip()) for w in ng_words if not len(w) == 0]
534        pat = r"|".join(ng_words)
535        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:
537    def apply(self, doc: Document) -> Document:
538        """
539        >>> DiscardAds().apply(Document("お問い合わせください 営業時間 よくある質問"*5)).is_rejected
540        True
541
542        >>> DiscardAds().apply(Document("おはよう")).is_rejected
543        False
544        """
545        ads_factor = self.keyword_pat.findall(doc.text)
546        if len(ads_factor) > self.max_allow_num:
547            doc.is_rejected = True
548        return doc
>>> DiscardAds().apply(Document("お問い合わせください 営業時間 よくある質問"*5)).is_rejected
True
>>> DiscardAds().apply(Document("おはよう")).is_rejected
False
class AcceptJapanese(hojichar.core.filter_interface.Filter):
551class AcceptJapanese(Filter):
552    """
553    日本語でないドキュメントを破棄します. 日本語判定は次の手順で行われます.
554        1. テキストを左から`lookup_size` (デフォルトで50字) 参照し,
555        ひらがな・カタカナが存在すれば日本語と判定する.
556    """
557
558    def __init__(self, lookup_size: int = 50, *args: Any, **kwargs: Any) -> None:
559        super().__init__(*args, **kwargs)
560
561        self.lookup_size = lookup_size
562        self.hiragana_katakana_pat = re.compile(r"[ぁ-んァ-ン]")
563
564    def apply(self, doc: Document) -> Document:
565        """
566        >>> AcceptJapanese().apply(Document("This is English document")).is_rejected
567        True
568
569        >>> AcceptJapanese().apply(Document("a"*50 + "あ")).is_rejected
570        True
571
572        >>> AcceptJapanese().apply(Document("ほうじ茶")).is_rejected
573        False
574        """
575        if not self.hiragana_katakana_pat.search(doc.text[: self.lookup_size]):
576            doc.is_rejected = True
577        return doc

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

AcceptJapanese(lookup_size: int = 50, *args: Any, **kwargs: Any)
558    def __init__(self, lookup_size: int = 50, *args: Any, **kwargs: Any) -> None:
559        super().__init__(*args, **kwargs)
560
561        self.lookup_size = lookup_size
562        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:
564    def apply(self, doc: Document) -> Document:
565        """
566        >>> AcceptJapanese().apply(Document("This is English document")).is_rejected
567        True
568
569        >>> AcceptJapanese().apply(Document("a"*50 + "あ")).is_rejected
570        True
571
572        >>> AcceptJapanese().apply(Document("ほうじ茶")).is_rejected
573        False
574        """
575        if not self.hiragana_katakana_pat.search(doc.text[: self.lookup_size]):
576            doc.is_rejected = True
577        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):
580class DiscardRareKuten(Filter):
581    """
582    日本語でないドキュメントを破棄します. 日本語判定は次の手順で行われます
583    ドキュメントを句点"。"で区切り, 平均文長が
584    `max_avarage_sentence_length` より長い場合は破棄します.
585    `max_avarage_sentence_length` のデフォルト値は100です.
586    このフィルタは, 文章中の句点の割合が少なすぎるドキュメントを破棄します.
587    """
588
589    def __init__(self, max_average_sentence_length: int = 100, *args: Any, **kwargs: Any) -> None:
590        super().__init__(*args, **kwargs)
591
592        self.max_average_sentence_length = max_average_sentence_length
593        self.kuten_pat = re.compile(r"。")
594
595    def apply(self, doc: Document) -> Document:
596        """
597        >>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよ。")).is_rejected
598        False
599        >>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよう。")).is_rejected
600        True
601        """
602        kuten_lst = self.kuten_pat.findall(doc.text)
603        min_kuten_num = len(doc.text) / self.max_average_sentence_length
604        if len(kuten_lst) < min_kuten_num:
605            doc.is_rejected = True
606        return doc

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

DiscardRareKuten(max_average_sentence_length: int = 100, *args: Any, **kwargs: Any)
589    def __init__(self, max_average_sentence_length: int = 100, *args: Any, **kwargs: Any) -> None:
590        super().__init__(*args, **kwargs)
591
592        self.max_average_sentence_length = max_average_sentence_length
593        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:
595    def apply(self, doc: Document) -> Document:
596        """
597        >>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよ。")).is_rejected
598        False
599        >>> DiscardRareKuten(max_average_sentence_length=4).apply(Document("おはよう。")).is_rejected
600        True
601        """
602        kuten_lst = self.kuten_pat.findall(doc.text)
603        min_kuten_num = len(doc.text) / self.max_average_sentence_length
604        if len(kuten_lst) < min_kuten_num:
605            doc.is_rejected = True
606        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):
609class HeaderFooterTagsRemover(Filter):
610    """
611    ドキュメントの冒頭・末尾のトークンを調査し, ヘッダー・フッダー的な
612    タグが存在していた場合, そのトークンを除去します.
613
614    このフィルタを通す前に, 事前にセンテンスレベルにトーカナイズしておいてください.
615    このフィルタでは Document.token にのみ変更が加えられるので, 出力前 あるいは 下流フィルタで
616    Document.text に変更を加える前にトークンをマージしておいてください.
617    """
618
619    def __init__(
620        self,
621        dict_path: Union[str, PathLike] = BASE_PATH / "dict/header_footer_keywords_ja.txt",
622        *args: Any,
623        **kwargs: Any,
624    ) -> None:
625        super().__init__(*args, **kwargs)
626
627        with open(dict_path) as fp:
628            keywords = fp.read().split("\n")
629        keywords = [re.escape(w.strip()) for w in keywords if not len(w) == 0]
630        self.keyword_pat = re.compile(r"|".join(keywords))
631
632    def apply(self, doc: Document) -> Document:
633        if len(doc.tokens) == 0:
634            return doc
635
636        lookup_size = 0
637        if 1 <= len(doc.tokens) < 4:
638            lookup_size = 1
639        elif 4 <= len(doc.tokens) < 6:
640            lookup_size = 2
641        elif 6 <= len(doc.tokens):
642            lookup_size = 3
643
644        for i in range(lookup_size):
645            if self.should_drop_token(doc.tokens[i]):
646                doc.tokens[i].is_rejected = True
647            if self.should_drop_token(doc.tokens[-(i + 1)]):
648                doc.tokens[i].is_rejected = True
649
650        return doc
651
652    def should_drop_token(self, token: Token) -> bool:
653        """
654        >>> HeaderFooterTagsRemover().should_drop_token(Token("<TEST_STRING_OF_KEYWORD>"))
655        True
656
657        >>> HeaderFooterTagsRemover().should_drop_token(Token("ほうじ茶"))
658        False
659
660        Comment.
661        Original legacy code removed a pattern r"« _ | Main | _ »" .
662        In the pattern, "|" is not escaped, so **ANY** string was eliminated.
663        It seems unintended behavior, so I fix this.
664        """
665        if self.keyword_pat.match(token.text):
666            return True
667        else:
668            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)
619    def __init__(
620        self,
621        dict_path: Union[str, PathLike] = BASE_PATH / "dict/header_footer_keywords_ja.txt",
622        *args: Any,
623        **kwargs: Any,
624    ) -> None:
625        super().__init__(*args, **kwargs)
626
627        with open(dict_path) as fp:
628            keywords = fp.read().split("\n")
629        keywords = [re.escape(w.strip()) for w in keywords if not len(w) == 0]
630        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:
632    def apply(self, doc: Document) -> Document:
633        if len(doc.tokens) == 0:
634            return doc
635
636        lookup_size = 0
637        if 1 <= len(doc.tokens) < 4:
638            lookup_size = 1
639        elif 4 <= len(doc.tokens) < 6:
640            lookup_size = 2
641        elif 6 <= len(doc.tokens):
642            lookup_size = 3
643
644        for i in range(lookup_size):
645            if self.should_drop_token(doc.tokens[i]):
646                doc.tokens[i].is_rejected = True
647            if self.should_drop_token(doc.tokens[-(i + 1)]):
648                doc.tokens[i].is_rejected = True
649
650        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:
652    def should_drop_token(self, token: Token) -> bool:
653        """
654        >>> HeaderFooterTagsRemover().should_drop_token(Token("<TEST_STRING_OF_KEYWORD>"))
655        True
656
657        >>> HeaderFooterTagsRemover().should_drop_token(Token("ほうじ茶"))
658        False
659
660        Comment.
661        Original legacy code removed a pattern r"« _ | Main | _ »" .
662        In the pattern, "|" is not escaped, so **ANY** string was eliminated.
663        It seems unintended behavior, so I fix this.
664        """
665        if self.keyword_pat.match(token.text):
666            return True
667        else:
668            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):
671class MaskPersonalInformation(Filter):
672    """
673    ドキュメントに含まれる電話番号・電子メールアドレスを一部マスキングします.
674    """
675
676    def __init__(self, *args: Any, **kwargs: Any) -> None:
677        super().__init__(*args, **kwargs)
678
679        self.phone_pat = re.compile(
680            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
681        )
682        self.email_pat = re.compile(
683            r"[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~.]+@[A-Za-z0-9!#$%&'*+\-/=?^_`{|}~.]+(\.[A-Za-z0-9\-]+)"  # noqa
684        )
685
686    def apply(self, doc: Document) -> Document:
687        """
688        >>> MaskPersonalInformation()('06-1234-5678')
689        '06-1234-XXXX'
690        >>> MaskPersonalInformation()('075-123-4567')
691        '075-123-XXXX'
692        >>> MaskPersonalInformation()('0166-12-3456')
693        '0166-12-XXXX'
694        >>> MaskPersonalInformation()('09808-1-2345')
695        '09808-1-XXXX'
696        >>> MaskPersonalInformation()('090-1234-5678')
697        '090-1234-XXXX'
698        >>> MaskPersonalInformation()('0751234567')
699        '075123XXXX'
700        >>> MaskPersonalInformation()('08012345678')
701        '0801234XXXX'
702        >>> MaskPersonalInformation()('連絡は075-123-4567 まで')
703        '連絡は075-123-XXXX まで'
704        >>> MaskPersonalInformation()('+81-80-1234-5678')
705        '+81-80-1234-XXXX'
706        >>> MaskPersonalInformation()('+818012345678')
707        '+81801234XXXX'
708        >>> MaskPersonalInformation()('hogehoge@example.com')
709        'xxxx@yyy.com'
710        >>> MaskPersonalInformation()('何かあれば hogehoge@example.ne.jp まで連絡')
711        '何かあれば xxxx@yyy.jp まで連絡'
712        """
713        text = self.phone_pat.sub(r"\1XXXX", doc.text)
714        text = self.email_pat.sub(r"xxxx@yyy\1", text)
715        doc.text = text
716        return doc

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

MaskPersonalInformation(*args: Any, **kwargs: Any)
676    def __init__(self, *args: Any, **kwargs: Any) -> None:
677        super().__init__(*args, **kwargs)
678
679        self.phone_pat = re.compile(
680            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
681        )
682        self.email_pat = re.compile(
683            r"[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~.]+@[A-Za-z0-9!#$%&'*+\-/=?^_`{|}~.]+(\.[A-Za-z0-9\-]+)"  # noqa
684        )

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:
686    def apply(self, doc: Document) -> Document:
687        """
688        >>> MaskPersonalInformation()('06-1234-5678')
689        '06-1234-XXXX'
690        >>> MaskPersonalInformation()('075-123-4567')
691        '075-123-XXXX'
692        >>> MaskPersonalInformation()('0166-12-3456')
693        '0166-12-XXXX'
694        >>> MaskPersonalInformation()('09808-1-2345')
695        '09808-1-XXXX'
696        >>> MaskPersonalInformation()('090-1234-5678')
697        '090-1234-XXXX'
698        >>> MaskPersonalInformation()('0751234567')
699        '075123XXXX'
700        >>> MaskPersonalInformation()('08012345678')
701        '0801234XXXX'
702        >>> MaskPersonalInformation()('連絡は075-123-4567 まで')
703        '連絡は075-123-XXXX まで'
704        >>> MaskPersonalInformation()('+81-80-1234-5678')
705        '+81-80-1234-XXXX'
706        >>> MaskPersonalInformation()('+818012345678')
707        '+81801234XXXX'
708        >>> MaskPersonalInformation()('hogehoge@example.com')
709        'xxxx@yyy.com'
710        >>> MaskPersonalInformation()('何かあれば hogehoge@example.ne.jp まで連絡')
711        '何かあれば xxxx@yyy.jp まで連絡'
712        """
713        text = self.phone_pat.sub(r"\1XXXX", doc.text)
714        text = self.email_pat.sub(r"xxxx@yyy\1", text)
715        doc.text = text
716        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):
719class DiscardTooManyNouns(Filter):
720    """
721    [!CAUTION] This filter requires `fugashi` package. Please install it
722    by `pip install 'hojichar[all]'`.
723
724    A filter that removes document with too many nouns in Japanese i.e.,
725    documents such as advertisement, word salad, etc ...
726    """
727
728    def __init__(self, threshold: float = 0.80, *args: Any, **kwargs: Any) -> None:
729        """
730        Args:
731            threshold: document whose noun ratio is higher than this value will be discarded
732            *args:
733            **kwargs:
734        """
735        super().__init__(*args, **kwargs)
736        assert (
737            is_loaded_extras
738        ), "fugashi is required for this filter. Try pip install 'hojichar[all]'"
739
740        self.threshold = threshold
741        self.tagger = Tagger("-Owakati")
742        assert (
743            "unidic" in self.tagger.dictionary_info[0]["filename"]
744        ), "MeCab dictionary must be unidic"
745
746    def apply(self, doc: Document) -> Document:
747        """
748        >>> DiscardTooManyNouns().apply(Document("自然言語処理大好き!")).is_rejected
749        False
750        >>> DiscardTooManyNouns().apply(Document("リンゴ・オレンジ・ミカン・バナナ セール中")).is_rejected
751        True
752        >>> DiscardTooManyNouns().apply(Document("今日の仙台朝市ではリンゴがセール中")).is_rejected
753        False
754        """
755        # remove "補助記号" from part-of-speech statistics
756        # because they often decrease the noun ratio,
757        # e.g., the sentence "リンゴ・オレンジ・バナナ・" has 補助記号 ratio of 0.5
758        # however, we don't want such sentence
759        pos_count = Counter(
760            w.feature.pos1 for w in self.tagger(doc.text) if w.feature.pos1 != "補助記号"
761        )
762        try:
763            noun_ratio = pos_count["名詞"] / sum(pos_count.values())
764        except ZeroDivisionError:
765            noun_ratio = 0.0
766        if noun_ratio >= self.threshold:
767            doc.is_rejected = True
768        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)
728    def __init__(self, threshold: float = 0.80, *args: Any, **kwargs: Any) -> None:
729        """
730        Args:
731            threshold: document whose noun ratio is higher than this value will be discarded
732            *args:
733            **kwargs:
734        """
735        super().__init__(*args, **kwargs)
736        assert (
737            is_loaded_extras
738        ), "fugashi is required for this filter. Try pip install 'hojichar[all]'"
739
740        self.threshold = threshold
741        self.tagger = Tagger("-Owakati")
742        assert (
743            "unidic" in self.tagger.dictionary_info[0]["filename"]
744        ), "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:
746    def apply(self, doc: Document) -> Document:
747        """
748        >>> DiscardTooManyNouns().apply(Document("自然言語処理大好き!")).is_rejected
749        False
750        >>> DiscardTooManyNouns().apply(Document("リンゴ・オレンジ・ミカン・バナナ セール中")).is_rejected
751        True
752        >>> DiscardTooManyNouns().apply(Document("今日の仙台朝市ではリンゴがセール中")).is_rejected
753        False
754        """
755        # remove "補助記号" from part-of-speech statistics
756        # because they often decrease the noun ratio,
757        # e.g., the sentence "リンゴ・オレンジ・バナナ・" has 補助記号 ratio of 0.5
758        # however, we don't want such sentence
759        pos_count = Counter(
760            w.feature.pos1 for w in self.tagger(doc.text) if w.feature.pos1 != "補助記号"
761        )
762        try:
763            noun_ratio = pos_count["名詞"] / sum(pos_count.values())
764        except ZeroDivisionError:
765            noun_ratio = 0.0
766        if noun_ratio >= self.threshold:
767            doc.is_rejected = True
768        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):
771class CharRepetitionRatioFilter(Filter):
772    """
773    文字Ngramの重なり率(文書中で高頻度文字Ngramが占める割合)を計算して, 重なりの大きいものを除去します.
774    名詞の連続からなるような広告テキストを取り除くのに有効です.
775
776    実装は, BigScience で採用されていた前処理を参考にしています.
777    元実装: https://github.com/bigscience-workshop/data-preparation/blob/9d0588419073cc5bf0fb92b58f37f2a1016572c3/preprocessing/training/01b_oscar_cleaning_and_filtering/filtering.py#L425-L453  # noqa: E501
778
779    「高頻度文字Ngram」は、sqrt(ユニークなNgramの総数)によって求めていますが,
780    これは文書長の影響を軽減するためだとされています.
781
782    掲示板のテキストが引っかかりやすい傾向があります.
783    13: 名無しさん@実況で競馬板アウト 2019/08/18(日) 15:28:46.10 ID:eBvZg8h+0
784    的なものが高頻度で登場するため、文字Ngramの重なり率も高くなってしまう
785    """
786
787    def __init__(
788        self, threshold: float = 0.33, ngram_size: int = 5, *args: Any, **kwargs: Any
789    ) -> None:
790        """
791
792        Args:
793            threshold: document with character repetition ratio higher than this value will be discarded
794            ngram_size: character ngram size. Larger value will decrease the false positive of long documents
795            *args:
796            **kwargs:
797        """  # noqa: E501
798
799        super().__init__(*args, **kwargs)
800        self.threshold = threshold
801        self.ngram_size = ngram_size
802
803    def apply(self, doc: Document) -> Document:
804        ratio = self.compute_character_repetition_ratio(doc.text, self.ngram_size)
805        if ratio >= self.threshold:
806            doc.is_rejected = True
807        return doc
808
809    @staticmethod
810    def compute_character_repetition_ratio(
811        document: str, character_repetition_length: int
812    ) -> float:
813        def get_freq_character_ngrams(document: str, n: int) -> Dict[str, int]:
814            character_ngrams: List[str] = [
815                document[i : i + n] for i in range(len(document) - n + 1)
816            ]
817            freq_character_ngrams_dict: Dict[str, int] = {}
818            for character_ngram in character_ngrams:
819                freq_character_ngrams_dict[character_ngram] = (
820                    freq_character_ngrams_dict.get(character_ngram, 0) + 1
821                )
822            return freq_character_ngrams_dict
823
824        freq_character_ngrams_dict = get_freq_character_ngrams(
825            document, character_repetition_length
826        )
827        if len(freq_character_ngrams_dict) == 0:
828            return 0.0
829        freq_character_ngrams: List[int] = list(freq_character_ngrams_dict.values())
830        freq_character_ngrams = sorted(freq_character_ngrams, reverse=True)
831        val_one = len([el for el in freq_character_ngrams if el == 1])
832        num_rep_character_ngrams = min(
833            int(np.sqrt(len(freq_character_ngrams))),
834            len(freq_character_ngrams) - val_one,
835        )
836        character_repetition_ratio = sum(freq_character_ngrams[:num_rep_character_ngrams]) / sum(
837            freq_character_ngrams
838        )
839        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)
787    def __init__(
788        self, threshold: float = 0.33, ngram_size: int = 5, *args: Any, **kwargs: Any
789    ) -> None:
790        """
791
792        Args:
793            threshold: document with character repetition ratio higher than this value will be discarded
794            ngram_size: character ngram size. Larger value will decrease the false positive of long documents
795            *args:
796            **kwargs:
797        """  # noqa: E501
798
799        super().__init__(*args, **kwargs)
800        self.threshold = threshold
801        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:
803    def apply(self, doc: Document) -> Document:
804        ratio = self.compute_character_repetition_ratio(doc.text, self.ngram_size)
805        if ratio >= self.threshold:
806            doc.is_rejected = True
807        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:
809    @staticmethod
810    def compute_character_repetition_ratio(
811        document: str, character_repetition_length: int
812    ) -> float:
813        def get_freq_character_ngrams(document: str, n: int) -> Dict[str, int]:
814            character_ngrams: List[str] = [
815                document[i : i + n] for i in range(len(document) - n + 1)
816            ]
817            freq_character_ngrams_dict: Dict[str, int] = {}
818            for character_ngram in character_ngrams:
819                freq_character_ngrams_dict[character_ngram] = (
820                    freq_character_ngrams_dict.get(character_ngram, 0) + 1
821                )
822            return freq_character_ngrams_dict
823
824        freq_character_ngrams_dict = get_freq_character_ngrams(
825            document, character_repetition_length
826        )
827        if len(freq_character_ngrams_dict) == 0:
828            return 0.0
829        freq_character_ngrams: List[int] = list(freq_character_ngrams_dict.values())
830        freq_character_ngrams = sorted(freq_character_ngrams, reverse=True)
831        val_one = len([el for el in freq_character_ngrams if el == 1])
832        num_rep_character_ngrams = min(
833            int(np.sqrt(len(freq_character_ngrams))),
834            len(freq_character_ngrams) - val_one,
835        )
836        character_repetition_ratio = sum(freq_character_ngrams[:num_rep_character_ngrams]) / sum(
837            freq_character_ngrams
838        )
839        return character_repetition_ratio
class WordRepetitionRatioFilter(hojichar.core.filter_interface.Filter):
842class WordRepetitionRatioFilter(Filter):
843    """
844    [!CAUTION] This filter requires `fugashi` package. Please install it
845    by `pip install 'hojichar[all]'`.
846
847    単語Ngramの重なり率(文書中で重複する単語Ngramが占める割合)を計算して、重なりの大きいものを弾くためのフィルタ.
848    BigScienceで採用されていた前処理を参考にしている.
849
850    名詞が連打されているような広告テキストを取り除くのに有効な様子
851    まともな文書がたまたま2回繰り返されている場合もあり、これを取り除いて良いのかは分からない
852    例:
853    "ウェブ\n本文: ニコンの上昇率16%超える、今3月期は経常76%の大幅増益見込む(ニコン) 2013年05月10日[minkabu PRESS] - みんなの株式 (みんかぶ)\n2013/05/10(10:57)
854    ニコン<7731.T>が急騰、寄り付き直後に前日比355円高の2537円まで買い上げ
855    られ、上昇率は16%を超えた。外国為替市場で円が1ドル100円台、1ユーロ131円台に入るなど急速に円安が進み、輸出株が軒並み高になる
856    なか、9日取引終了後に発表した前年3月期決算で、今3月期は2ケタ近い増収で大幅増益を見込んだことが買い気を強めさせた。連結売上
857    高は前期比9.8%増の1兆1100億円、経常利益75.8%増の850億円を予想。前期は半導体、電子部品の低迷が足かせになり、2ケタ増収ながら
858    経常46%の大幅減益になったが、レンズ交換式デジタルカメラの拡大や液晶ディスプレイの回復で収益が急回復する。ニコンの株価は10時
859    56分現在2491円(△309円)出所:株経通信(株式会社みんかぶ)\n2013/05/10 - ニコン(7731) の関連ニュース。 ニコン<7731.T>が急騰、寄
860    り付き直後に前日比355円高の2537円まで買い上げられ、上昇率は16%を超えた。外国為替市場で円が1ドル100円台、1ユーロ131円台に入
861    るなど急速に円安が進み、輸出株が軒並み高になるなか、9日取引終了後に発表した前年3月期決算で、今3月期は2ケタ近い増収で大幅増
862    益を見込んだことが買い気を強めさせた。連結売上高は前期比9.8%増の1兆1100億円、経常利益75.8%増の850億円を予想。前期は半導体、
863    電子部品の低迷が足かせになり、2ケタ増収ながら経常46%の大幅減益になったが、レンズ交換式デジタルカメラの拡大や液晶ディスプレ
864    イの回復で収益が急回"
865    """  # noqa: E501
866
867    def __init__(
868        self, threshold: float = 0.40, ngram_size: int = 7, *args: Any, **kwargs: Any
869    ) -> None:
870        """
871
872        Args:
873            threshold: document whose character repetition ratio is higher than this value will be discarded
874            ngram_size: character ngram size. Larger value will decrease the false positive of long documents
875            *args:
876            **kwargs:
877        """  # noqa: E501
878        super().__init__(*args, **kwargs)
879        assert (
880            is_loaded_extras
881        ), "fugashi is required for this filter. Try pip install 'hojichar[all]'"
882
883        self.threshold = threshold
884        self.ngram_size = ngram_size
885        self.tagger = Tagger("-Owakati")
886
887    def apply(self, doc: Document) -> Document:
888        ratio = self.compute_word_repetition_ratio(doc.text, self.ngram_size)
889        if ratio >= self.threshold:
890            doc.is_rejected = True
891        return doc
892
893    def compute_word_repetition_ratio(self, document: str, word_repetition_length: int) -> float:
894        def get_freq_word_ngrams(document: str, n: int) -> Dict[str, int]:
895            # tokenizing given document
896            words = [w.surface for w in self.tagger(document)]
897            word_ngrams = [" ".join(words[i : i + n]) for i in range(len(words) - n + 1)]
898            freq_word_ngrams: Dict[str, int] = {}
899            for word_ngram in word_ngrams:
900                freq_word_ngrams[word_ngram] = freq_word_ngrams.get(word_ngram, 0) + 1
901            return freq_word_ngrams
902
903        freq_word_ngrams_dict = get_freq_word_ngrams(document, word_repetition_length)
904        if len(freq_word_ngrams_dict) == 0:
905            return 0
906        freq_word_ngrams = list(freq_word_ngrams_dict.values())
907        word_repetition_ratio = sum(freq for freq in freq_word_ngrams if freq > 1) / sum(
908            freq_word_ngrams
909        )
910
911        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)
867    def __init__(
868        self, threshold: float = 0.40, ngram_size: int = 7, *args: Any, **kwargs: Any
869    ) -> None:
870        """
871
872        Args:
873            threshold: document whose character repetition ratio is higher than this value will be discarded
874            ngram_size: character ngram size. Larger value will decrease the false positive of long documents
875            *args:
876            **kwargs:
877        """  # noqa: E501
878        super().__init__(*args, **kwargs)
879        assert (
880            is_loaded_extras
881        ), "fugashi is required for this filter. Try pip install 'hojichar[all]'"
882
883        self.threshold = threshold
884        self.ngram_size = ngram_size
885        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:
887    def apply(self, doc: Document) -> Document:
888        ratio = self.compute_word_repetition_ratio(doc.text, self.ngram_size)
889        if ratio >= self.threshold:
890            doc.is_rejected = True
891        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:
893    def compute_word_repetition_ratio(self, document: str, word_repetition_length: int) -> float:
894        def get_freq_word_ngrams(document: str, n: int) -> Dict[str, int]:
895            # tokenizing given document
896            words = [w.surface for w in self.tagger(document)]
897            word_ngrams = [" ".join(words[i : i + n]) for i in range(len(words) - n + 1)]
898            freq_word_ngrams: Dict[str, int] = {}
899            for word_ngram in word_ngrams:
900                freq_word_ngrams[word_ngram] = freq_word_ngrams.get(word_ngram, 0) + 1
901            return freq_word_ngrams
902
903        freq_word_ngrams_dict = get_freq_word_ngrams(document, word_repetition_length)
904        if len(freq_word_ngrams_dict) == 0:
905            return 0
906        freq_word_ngrams = list(freq_word_ngrams_dict.values())
907        word_repetition_ratio = sum(freq for freq in freq_word_ngrams if freq > 1) / sum(
908            freq_word_ngrams
909        )
910
911        return word_repetition_ratio
class DiscardTooManySpecialToken(hojichar.core.filter_interface.Filter):
914class DiscardTooManySpecialToken(Filter):
915    """
916    [!CAUTION] This filter requires `emoji` package. Please install it
917    by `pip install 'hojichar[all]'`.
918
919    句読点を含む記号、空白、絵文字、その他特殊な文字を一定の割合以上含むような文書を取り除くためのフィルタ
920    元実装: BigScience https://github.com/bigscience-workshop/data-preparation/blob/9d0588419073cc5bf0fb92b58f37f2a1016572c3/preprocessing/training/01b_oscar_cleaning_and_filtering/parameters_filtering.py#L5-L16  # noqa: E501
921    """
922
923    def __init__(self, threshold: float = 0.4, *args: Any, **kwargs: Any) -> None:
924        """
925
926        Args:
927            threshold: document whose special token ratio is higher than this value will be discarded
928            *args:
929            **kwargs:
930        """  # noqa: E501
931        super().__init__(*args, **kwargs)
932
933        # digits are not regarded as special tokens
934        # otherwise many false positives are made, i.e., good documents discarded
935        main_special_characters = string.punctuation + string.whitespace  # + string.digits
936        other_special_characters = (
937            "’ “— ™ – •‘œ    ˜ ‚ƒ„’“”–▬…✦�­£​•€«»°·═"
938            "×士^˘⇓()§″′´¿−±∈¢ø‚„½¼¾¹²³―⁃,ˌ¸‹›ʺˈʻ¦‐⠀‰……‑≤≥‖"
939            "◆●■►▼▲▴∆▻¡★☆✱ːº。¯˜¥ɪ≈†:⁄♡✓⊕․.⋅÷1‟;،、¨ाাी्े◦˚"
940            "゜ʼ≖ʼ¤℃√!?【】‿∞➤~πه۩☛₨➩☻๑٪♥ıॽ《‘©﴿٬?▷Г♫∟™ª₪®「—❖"
941            "」﴾》�"
942        )
943
944        en_emoji = emoji.EMOJI_DATA.keys()
945
946        special_characters_default = set(main_special_characters + other_special_characters)
947        special_characters_default.update(en_emoji)
948        self.special_characters = special_characters_default
949
950        self.threshold = threshold
951
952    def _compute_special_characters_ratio(self, text: str) -> float:
953        if len(text) == 0:
954            return 0
955
956        special_characters_ratio = len(
957            [char for char in text if char in self.special_characters]
958        ) / len(text)
959        return special_characters_ratio
960
961    def apply(self, doc: Document) -> Document:
962        special_characters_ratio = self._compute_special_characters_ratio(doc.text)
963
964        if special_characters_ratio > self.threshold:
965            doc.is_rejected = True
966        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)
923    def __init__(self, threshold: float = 0.4, *args: Any, **kwargs: Any) -> None:
924        """
925
926        Args:
927            threshold: document whose special token ratio is higher than this value will be discarded
928            *args:
929            **kwargs:
930        """  # noqa: E501
931        super().__init__(*args, **kwargs)
932
933        # digits are not regarded as special tokens
934        # otherwise many false positives are made, i.e., good documents discarded
935        main_special_characters = string.punctuation + string.whitespace  # + string.digits
936        other_special_characters = (
937            "’ “— ™ – •‘œ    ˜ ‚ƒ„’“”–▬…✦�­£​•€«»°·═"
938            "×士^˘⇓()§″′´¿−±∈¢ø‚„½¼¾¹²³―⁃,ˌ¸‹›ʺˈʻ¦‐⠀‰……‑≤≥‖"
939            "◆●■►▼▲▴∆▻¡★☆✱ːº。¯˜¥ɪ≈†:⁄♡✓⊕․.⋅÷1‟;،、¨ाাी्े◦˚"
940            "゜ʼ≖ʼ¤℃√!?【】‿∞➤~πه۩☛₨➩☻๑٪♥ıॽ《‘©﴿٬?▷Г♫∟™ª₪®「—❖"
941            "」﴾》�"
942        )
943
944        en_emoji = emoji.EMOJI_DATA.keys()
945
946        special_characters_default = set(main_special_characters + other_special_characters)
947        special_characters_default.update(en_emoji)
948        self.special_characters = special_characters_default
949
950        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:
961    def apply(self, doc: Document) -> Document:
962        special_characters_ratio = self._compute_special_characters_ratio(doc.text)
963
964        if special_characters_ratio > self.threshold:
965            doc.is_rejected = True
966        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):
 969class SingleCharacterRepetitionFilter(Filter):
 970    """
 971    単一文字が大量に繰り返されているような文書を取り除くためのフィルタ
 972    そのような文書はノイズである可能性が高いため
 973    参考: BigScienceプロジェクトによると、oscarデータセットの中にバックスラッシュだけを2M個含むような文書が含まれていたらしい
 974    https://github.com/bigscience-workshop/bigscience/blob/master/train/tr8-104B-wide/chronicles.md#2m-backslash-only-samples-in-our-dataset  # noqa: E501
 975    """
 976
 977    def __init__(
 978        self,
 979        threshold: int = 200,
 980        *args: Any,
 981        **kwargs: Any,
 982    ) -> None:
 983        """
 984        Args:
 985            threshold: The document is removed if character is repeated for this value or more
 986            *args:
 987            **kwargs:
 988        """
 989        super().__init__(*args, **kwargs)
 990        self.threshold = threshold
 991
 992    def _is_repeat_contained(self, text: str) -> bool:
 993        groups = groupby(text)
 994        is_repeat_contained = any(sum(1 for _ in group) >= self.threshold for _, group in groups)
 995        return is_repeat_contained
 996
 997    def apply(self, doc: Document) -> Document:
 998        if self._is_repeat_contained(doc.text):
 999            doc.is_rejected = True
1000        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)
977    def __init__(
978        self,
979        threshold: int = 200,
980        *args: Any,
981        **kwargs: Any,
982    ) -> None:
983        """
984        Args:
985            threshold: The document is removed if character is repeated for this value or more
986            *args:
987            **kwargs:
988        """
989        super().__init__(*args, **kwargs)
990        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:
 997    def apply(self, doc: Document) -> Document:
 998        if self._is_repeat_contained(doc.text):
 999            doc.is_rejected = True
1000        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):
1003class DiscardTooManyEndingEllipsis(Filter):
1004    """
1005    ellipsisで終わるような行が大量に含まれるような文書を取り除くためのフィルタです.
1006    ellipsisとしては ... と … を用いている
1007    同様のフィルタが RedPajama v2で用いられています.
1008
1009    例として, 以下のような文書を検知します.
1010    ```
1011    ペアーズは女性、という驚愕の過食が出ているのをごアラサーですか。時代から付...
1012    バツイチアラフォー 婚活ち女性の特徴と子持な付...
1013    ```
1014
1015    デフォルトではしきい値を0.7としているが, これはC4から0.1%を削るような設定であり、
1016    precisionを重視した設定です.
1017    """
1018
1019    def __init__(
1020        self,
1021        threshold: float = 0.7,
1022        *args: Any,
1023        **kwargs: Any,
1024    ) -> None:
1025        """
1026        Args:
1027            threshold: The document is removed if ratio of lines ending with ellipsis is higher than this value
1028            *args:
1029            **kwargs:
1030        """  # noqa: E501
1031        super().__init__(*args, **kwargs)
1032        self.threshold = threshold
1033        self.ellipsis_pattern = re.compile(r"(\.{3}|…)\n")  # matches ...\n and …\n
1034
1035    def apply(self, doc: Document) -> Document:
1036        ellipsis_count = len(self.ellipsis_pattern.findall(doc.text))
1037        newline_count = max(doc.text.count("\n"), 1)  # avoid zero division
1038        ellipsis_ratio = ellipsis_count / newline_count
1039
1040        if ellipsis_ratio > self.threshold:
1041            doc.is_rejected = True
1042        return doc

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

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

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

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

DiscardTooManyEndingEllipsis(threshold: float = 0.7, *args: Any, **kwargs: Any)
1019    def __init__(
1020        self,
1021        threshold: float = 0.7,
1022        *args: Any,
1023        **kwargs: Any,
1024    ) -> None:
1025        """
1026        Args:
1027            threshold: The document is removed if ratio of lines ending with ellipsis is higher than this value
1028            *args:
1029            **kwargs:
1030        """  # noqa: E501
1031        super().__init__(*args, **kwargs)
1032        self.threshold = threshold
1033        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:
1035    def apply(self, doc: Document) -> Document:
1036        ellipsis_count = len(self.ellipsis_pattern.findall(doc.text))
1037        newline_count = max(doc.text.count("\n"), 1)  # avoid zero division
1038        ellipsis_ratio = ellipsis_count / newline_count
1039
1040        if ellipsis_ratio > self.threshold:
1041            doc.is_rejected = True
1042        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):
1045class DiscardTooShortLines(Filter):
1046    """
1047    短い行を大量に含む文書を捨てるためのフィルタです.
1048
1049    メニューバーやパンくずリストのような要素を大量に含む文書を取り除くのに有効です.
1050    """
1051
1052    def __init__(self, threshold: float = 0.5, *args: Any, **kwargs: Any) -> None:
1053        """
1054        Args:
1055            threshold: The document is removed if the ratio of short (<10 chars) lines are more than this value.
1056            *args:
1057            **kwargs:
1058        """  # noqa: E501
1059        super().__init__(*args, **kwargs)
1060        self.threshold = threshold
1061        # この値は適当に決め打ち
1062        self.minimum_line_length = 10
1063
1064    def apply(self, doc: Document) -> Document:
1065        lines = [len(x) for x in doc.text.split("\n")]
1066        short_lines = [x for x in lines if x <= self.minimum_line_length]
1067        if (len(short_lines) / len(lines)) > self.threshold:
1068            doc.is_rejected = True
1069        return doc

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

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

DiscardTooShortLines(threshold: float = 0.5, *args: Any, **kwargs: Any)
1052    def __init__(self, threshold: float = 0.5, *args: Any, **kwargs: Any) -> None:
1053        """
1054        Args:
1055            threshold: The document is removed if the ratio of short (<10 chars) lines are more than this value.
1056            *args:
1057            **kwargs:
1058        """  # noqa: E501
1059        super().__init__(*args, **kwargs)
1060        self.threshold = threshold
1061        # この値は適当に決め打ち
1062        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:
1064    def apply(self, doc: Document) -> Document:
1065        lines = [len(x) for x in doc.text.split("\n")]
1066        short_lines = [x for x in lines if x <= self.minimum_line_length]
1067        if (len(short_lines) / len(lines)) > self.threshold:
1068            doc.is_rejected = True
1069        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