This commit is contained in:
2026-07-19 23:52:25 +04:00
commit 99b06b0468
155 changed files with 3808611 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
"""Phase 2.3 — clean every raw bucket → data/cpt_clean/<bucket>.jsonl
Pipeline order (plan §2.3), applied per doc, then a global cross-bucket dedup:
1. ftfy unicode fix (NFC)
2. fastText language id — drop if top lang != ru or p(ru) < 0.65
3. mixed-script rejection — drop docs with homoglyph-contaminated words
4. boilerplate/length — strip HTML, collapse whitespace, drop <200 or >50k chars,
drop docs with >30% non-letter chars
5. MinHash-LSH near-dup removal across ALL buckets (the biggest quality lever)
6. light PII scrub (phones/emails) — logs bucket has real data
Usage: python corpus_clean.py # all buckets present in data/cpt_raw/
python corpus_clean.py wiki synth # specific buckets
"""
import sys
import re
import json
import html
import ftfy
from datasketch import MinHash, MinHashLSH
from corpus_common import RAW, CLEAN, has_mixed_script, lang_prob
MIN_CHARS, MAX_CHARS = 200, 50_000
NON_LETTER_MAX = 0.30
LID_MIN = 0.65
LSH_THRESHOLD = 0.8
NUM_PERM = 128
_TAG = re.compile(r"<[^>]+>")
_WS = re.compile(r"[ \t ]+")
_NL = re.compile(r"\n{3,}")
_LETTER = re.compile(r"[^\W\d_]", re.UNICODE)
_EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+")
_PHONE = re.compile(r"(?<!\d)(?:\+7|8)[\s(-]*\d{3}[\s)-]*\d{3}[\s-]*\d{2}[\s-]*\d{2}(?!\d)")
def normalize(text: str) -> str:
text = ftfy.fix_text(text)
text = html.unescape(text)
text = _TAG.sub(" ", text)
text = _WS.sub(" ", text)
text = _NL.sub("\n\n", text)
return text.strip()
def non_letter_ratio(text: str) -> float:
letters = len(_LETTER.findall(text))
return 1.0 - (letters / len(text)) if text else 1.0
def scrub_pii(text: str) -> str:
text = _EMAIL.sub("<email>", text)
text = _PHONE.sub("<phone>", text)
return text
def doc_minhash(text: str) -> MinHash:
m = MinHash(num_perm=NUM_PERM)
for tok in text.lower().split():
m.update(tok.encode("utf-8"))
return m
def clean_bucket(bucket: str, lsh: MinHashLSH, seen: dict) -> int:
src = RAW / bucket / "shard-000.jsonl"
if not src.exists():
print(f"[{bucket}] no raw shard at {src}; skip")
return 0
out = CLEAN / f"{bucket}.jsonl"
kept = dropped = 0
with open(src, encoding="utf-8") as fin, open(out, "w", encoding="utf-8") as fout:
for i, line in enumerate(fin):
try:
text = json.loads(line)["text"]
except (json.JSONDecodeError, KeyError):
dropped += 1
continue
text = normalize(text)
# 4. length / boilerplate
if not (MIN_CHARS <= len(text) <= MAX_CHARS) or non_letter_ratio(text) > NON_LETTER_MAX:
dropped += 1
continue
# 3. mixed script
if has_mixed_script(text):
dropped += 1
continue
# 2. language id
try:
lang, p = lang_prob(text)
if lang != "ru" or p < LID_MIN:
dropped += 1
continue
except FileNotFoundError:
raise SystemExit("lid.176.bin missing — see corpus_common.LID_PATH")
# 5. dedup
m = doc_minhash(text)
if lsh.query(m):
dropped += 1
continue
key = f"{bucket}:{i}"
lsh.insert(key, m)
seen[key] = True
# 6. pii
text = scrub_pii(text)
fout.write(json.dumps({"text": text}, ensure_ascii=False) + "\n")
kept += 1
if (kept + dropped) % 5000 == 0:
print(f"[{bucket}] kept {kept} dropped {dropped}")
print(f"[{bucket}] DONE kept {kept} dropped {dropped} -> {out}")
return kept
def main():
buckets = sys.argv[1:] or [p.name for p in sorted(RAW.iterdir()) if p.is_dir()]
# one shared LSH across all buckets = global dedup (plan §2.3 rule 5)
lsh = MinHashLSH(threshold=LSH_THRESHOLD, num_perm=NUM_PERM)
seen = {}
total = 0
for b in buckets:
total += clean_bucket(b, lsh, seen)
print(f"[clean] TOTAL kept {total} docs across {len(buckets)} buckets")
if __name__ == "__main__":
main()