Filter OpenCorpora abbreviations & proper nouns from the Russian noun list
build / dawg (pull_request) Successful in 2m45s
build / dawg (pull_request) Successful in 2m45s
OpenCorpora tags indeclinable abbreviations as common nouns (Abbr+Fixd), and Stage 2 seeded the result with its whole noun lexicon, so non-words leaked into scrabble.txt: ндс, ст, ср, кпд, чп, гибдд, днк, … ru_stage2.py now drops Abbr+Fixd nouns from the OpenCorpora seed and lets the orthographic dictionary decide instead: a lowercase РАН headword whose note is a noun is kept (the lexicalised сельпо, под, ска, роно, врио, тв, фио, суперэвм), everything else is dropped. Function words (CONJ/PRCL, e.g. зато) and OpenCorpora inflected forms (ан = род. мн. от «ана»; proper «Ан») are excluded too. --trace WORD now prints a detailed per-signal audit (all.txt, OpenCorpora POS / Abbr / Fixd / CONJ/PRCL, libmorph, РАН note + classify, outcome) for auditing a word or repeating the analysis; tools/README.md documents the rule and method. Net: 179 words removed from scrabble.txt (83385 -> 83206) and erudit.txt (83343 -> 83164); verified to remove exactly those 179 and add none. DAWGs rebuild clean.
This commit is contained in:
+105
-13
@@ -38,6 +38,8 @@ LIBMORPH_BIN = os.path.join(HERE, "libmorph_check")
|
||||
ALPHABET = "абвгдеёжзийклмнопрстуфхцчшщъыьэюя"
|
||||
ORDER = {c: i for i, c in enumerate(ALPHABET)}
|
||||
PROPER = {"Name", "Surn", "Patr", "Geox", "Orgn", "Trad"}
|
||||
ABBR_NOUN = {"Abbr", "Fixd"} # indeclinable abbreviation marker (НДС, КПД, сельпо, …)
|
||||
FUNCTION_POS = {"CONJ", "PRCL"} # conjunction / particle: a function word, never a Scrabble noun
|
||||
LIBMORPH_NOUN_CODES = set(range(7, 22)) | {24} # 7..21 plus 24 (pluralia tantum)
|
||||
ADJ_END = {"ая", "яя", "ое", "ее", "ье", "ья", "ьи"}
|
||||
VERB3 = ("ет", "ёт", "ит", "ют", "ут", "ает", "яет", "ует", "уют", "нет", "жет", "чет")
|
||||
@@ -72,7 +74,10 @@ D = M._dawg_dict
|
||||
|
||||
|
||||
def oc_noun_lemmas():
|
||||
"""Every common-noun lemma (nom. sing. / pluralia tantum) in OpenCorpora's words.dawg."""
|
||||
"""Every common-noun lemma (nom. sing. / pluralia tantum) in OpenCorpora's words.dawg,
|
||||
excluding indeclinable abbreviations (Abbr+Fixd, e.g. НДС, КПД). Such words are admitted
|
||||
only when the orthographic dictionary attests them as nouns (see oc_abbr_fixd_only and the
|
||||
Stage-2 loop); abbreviations carried by OpenCorpora alone (ст, ср, кпд, …) are dropped."""
|
||||
gp, pt = D.get_paradigm, D.parse_tag_string
|
||||
para0, tagc = {}, {}
|
||||
|
||||
@@ -104,6 +109,8 @@ def oc_noun_lemmas():
|
||||
pre0, suf0, gr0 = g0(pid)
|
||||
if (PROPER & gr) or (PROPER & gr0):
|
||||
continue
|
||||
if ABBR_NOUN <= gr0: # indeclinable abbreviation: not seeded; the loop decides via the note
|
||||
continue
|
||||
stem = word[len(pre):len(word) - len(suf)] if suf else word[len(pre):]
|
||||
out.add(pre0 + stem + suf0)
|
||||
return {w for w in out if cyr_ok(w)}
|
||||
@@ -126,6 +133,43 @@ def oc_status(word):
|
||||
return False, True
|
||||
|
||||
|
||||
def oc_abbr_fixd_only(word):
|
||||
"""True when OpenCorpora's only common-noun reading of word is an indeclinable
|
||||
abbreviation (Abbr+Fixd) — e.g. сельпо, ндс, ан. Such a word is not admitted as a noun on
|
||||
OpenCorpora's say-so; the Stage-2 loop hands it to the orthographic note (and the function-
|
||||
word check) instead, so lexicalised nouns (сельпо, под, ска) survive while bare letter-
|
||||
abbreviations (ндс, кпд) and proper/служебные homographs (ан, зато) do not."""
|
||||
parses = D.get_word_parses(word)
|
||||
if not parses:
|
||||
return False
|
||||
gp, pt = D.get_paradigm, D.parse_tag_string
|
||||
has_noun = has_plain = False
|
||||
for pid, idx in parses:
|
||||
suf, tag, pre = gp(pid, idx)
|
||||
pos, gr = pt(tag)
|
||||
if pos != "NOUN":
|
||||
continue
|
||||
_, tag0, _ = gp(pid, 0)
|
||||
_, gr0 = pt(tag0)
|
||||
if PROPER & gr or PROPER & gr0:
|
||||
continue
|
||||
has_noun = True
|
||||
if not (ABBR_NOUN <= gr0):
|
||||
has_plain = True
|
||||
return has_noun and not has_plain
|
||||
|
||||
|
||||
def oc_function_word(word):
|
||||
"""True when OpenCorpora reads word as a conjunction or particle — a function word that
|
||||
shares a spelling with an abbreviation/proper noun (ан «ан нет», зато). It is never a
|
||||
Scrabble noun even if the orthographic note looks noun-like (ан → «самолёт Антонова»)."""
|
||||
gp, pt = D.get_paradigm, D.parse_tag_string
|
||||
for pid, idx in (D.get_word_parses(word) or []):
|
||||
if pt(gp(pid, idx)[1])[0] in FUNCTION_POS:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def libmorph_analyze(words):
|
||||
"""Map each word to (known, noun_lemma, codes) per libmorph; noun_lemma is None when it
|
||||
is not a common noun there. Empty result if the helper binary is not built."""
|
||||
@@ -240,9 +284,27 @@ def build():
|
||||
scrabble = set(oc)
|
||||
adj, verb, amb = [], [], []
|
||||
for w in pdf:
|
||||
if oc_abbr_fixd_only(w):
|
||||
# Indeclinable abbreviation in OpenCorpora: do not trust OC's noun verdict. Drop it
|
||||
# if OC also reads it as a function word (ан, зато — служебное/пропер); otherwise let
|
||||
# the orthographic note decide, keeping lexicalised nouns (сельпо, под, ска) and
|
||||
# dropping the rest (кпд, чп, гор, …).
|
||||
if oc_function_word(w):
|
||||
fate[w] = "отброшено: служебное слово (CONJ/PRCL в OpenCorpora)"
|
||||
elif classify(w, hmap.get(w)) == "noun":
|
||||
s = to_singular(w)
|
||||
scrabble.add(s)
|
||||
fate[w] = "scrabble: несклон. аббрев.-сущ. по помете орфословаря" + ("" if s == w else f" → {s}")
|
||||
else:
|
||||
fate[w] = "отброшено: аббревиатура без подтверждающей пометы орфословаря"
|
||||
continue
|
||||
oc_noun, oc_known = oc_status(w)
|
||||
if oc_noun:
|
||||
fate[w] = "scrabble: сущ. по OpenCorpora"
|
||||
# A common-noun reading already put w in the seed (scrabble = set(oc)) — unless its
|
||||
# lemma is a different word, i.e. w is an inflected form (ан = род. мн. от «ана»);
|
||||
# such a form is not a headword and stays out.
|
||||
fate[w] = ("scrabble: сущ. по OpenCorpora" if w in scrabble
|
||||
else "отброшено: словоформа OpenCorpora (лемма — другое слово)")
|
||||
continue
|
||||
lm_known, lm_lemma, _ = lm.get(w, (False, None, frozenset()))
|
||||
if lm_lemma is not None:
|
||||
@@ -295,21 +357,51 @@ def build():
|
||||
return {
|
||||
"oc": oc, "scrabble": scrabble, "undefined": undefined,
|
||||
"adjectives": adj, "verbs": verb, "singulars": ed_nouns,
|
||||
"fate": fate, "all": set(all_words),
|
||||
"fate": fate, "all": set(all_words), "hmap": hmap,
|
||||
}
|
||||
|
||||
|
||||
def trace(word, r):
|
||||
"""A detailed, per-signal audit of how WORD did or did not reach the dictionary: its
|
||||
Stage-1 (all.txt) membership, every OpenCorpora reading, libmorph, the orthographic note
|
||||
with its classify verdict, and the final outcome. Used by `--trace WORD`."""
|
||||
w = destress(word)
|
||||
if w in r["fate"]:
|
||||
return r["fate"][w]
|
||||
if w in r["scrabble"]:
|
||||
return "scrabble: лексикон OpenCorpora" if w in r["oc"] else "scrabble: производная/лемма"
|
||||
if w not in r["all"]:
|
||||
return "нет в russian_all (не извлечено на Stage 1 — нет в .pdf, либо имя собств./дефис/форма)"
|
||||
if not cyr_ok(w):
|
||||
return "отсеяно: длина или символы вне диапазона (2–15 кириллица)"
|
||||
return "не определено"
|
||||
note = r["hmap"].get(w)
|
||||
parses = D.get_word_parses(w) or []
|
||||
gp, pt = D.get_paradigm, D.parse_tag_string
|
||||
pos = sorted({pt(gp(pid, idx)[1])[0] for pid, idx in parses})
|
||||
oc_noun, _ = oc_status(w)
|
||||
|
||||
out = [f"{word} → {'В СЛОВАРЕ' if w in r['scrabble'] else 'НЕ в словаре'}"]
|
||||
out.append(f" Stage-1 / all.txt (строчный заголовок РАН): {'да' if w in r['all'] else 'нет'}")
|
||||
if parses:
|
||||
kind = ("только несклон. аббрев. (Abbr+Fixd)" if oc_abbr_fixd_only(w) else
|
||||
"обычное сущ." if oc_noun else
|
||||
"не существительное")
|
||||
out.append(f" OpenCorpora: части речи {pos}; как сущ. — {kind}; "
|
||||
f"служебное (CONJ/PRCL): {'да' if oc_function_word(w) else 'нет'}")
|
||||
else:
|
||||
out.append(" OpenCorpora: нет в словаре")
|
||||
lm = libmorph_analyze([w]).get(w)
|
||||
if lm:
|
||||
known, lemma, _ = lm
|
||||
out.append(" libmorph: " + (f"сущ. → {lemma}" if lemma else ("известно, не сущ." if known else "нет")))
|
||||
else:
|
||||
out.append(" libmorph: helper отсутствует — в анализе не участвует")
|
||||
out.append(" Орфословарь РАН: " + (f"помета «{note}» → classify={classify(w, note)}"
|
||||
if note is not None else "нет заголовка"))
|
||||
reason = r["fate"].get(w)
|
||||
if reason is None:
|
||||
if w in r["scrabble"]:
|
||||
reason = "лексикон OpenCorpora (обычное сущ.)"
|
||||
elif oc_abbr_fixd_only(w) and w not in r["all"]:
|
||||
reason = "исключено из посева: несклон. аббрев. (Abbr+Fixd), нет в all.txt"
|
||||
elif not parses and w not in r["all"]:
|
||||
reason = "нет ни в OpenCorpora, ни в all.txt"
|
||||
else:
|
||||
reason = "не прошло ни один путь отбора"
|
||||
out.append(f" ИТОГ: {reason}")
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -320,7 +412,7 @@ def main():
|
||||
|
||||
r = build()
|
||||
if args.trace:
|
||||
print(f"{args.trace}: {trace(args.trace, r)}")
|
||||
print(trace(args.trace, r))
|
||||
return
|
||||
|
||||
write(os.path.join(OUT_DIR, "scrabble.txt"), r["scrabble"])
|
||||
|
||||
Reference in New Issue
Block a user