Dobře, pojďme se kouknout na dvě pokročilé techniky, které (pravděpodobně) ještě nemáš nasazené, a které ti během tisíců až milionů řádků ušetří desítky až stovky milisekund:
1) Aho–Corasick místo stovek tisíc str.endswith
či regexů
Zatím pro každý řádek procházíš všechny tvé sufxy/prefxy a voláš na každý z nich r.matches(…)
, kde většinou testuješ word.endswith(literal)
nebo malý regex. To je O(N_rules) práce pro každý vstupní řádek.
Co dělat místo toho
-
Sesbíráš všechny literal‑řetězce (ty bez hranatých závorek a tečky) do jednoho seznamu
literals = ["at", "át", "absorbovat", …]
. -
Postavíš Aho–Corasickův automat (trie + failure‑funkce) – existuje čistě C‑extenze pyahocorasick nebo ahocorasick.
-
S jedním průchodem nad každou vstupní řádkou (O(len(line))) získáš všechny literály, které v ní končí nebo začínají – včetně pozice.
-
Logiku PFX/SFX pak aplikuješ jen na ty, které skutečně pasují, místo abys testoval komplet všech pravidel.
Výsledek:
-
Místo
for rule in aff_rules: if rule.matches(line): …
-
máš
for match in aho_automaton.iter(line): …
Aho–Corasick dokáže pokrýt tisíce patternů v jednom průchodu C‑kódem, bez opakovaných volání Python‑funkcí. Výhoda je tím zřetelnější, čím víc pravidel máš.
2) Memory‑map (mmap) + bufferované čtení bez .split()
a .strip()
Každé line.split('/', 1)
, .rstrip()
, .startswith()
či in
jsou C‑funkce, ale volané desetitisíckrát dají dohromady značnou režii Pythonu.
Jak to oříznout
-
Místo
for raw in f: parts = raw.split('/',1) …
použij
mmap
, načti celý.dic
soubor do paměti jakobytes
, a pak ručně procházíš ukazatelem pozicemm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) pos = 0 while True: nl = mm.find(b'\n', pos) if nl == -1: break line = mm[pos:nl] # jen pohled na paměť, nevytváří nový Python str pos = nl+1 # pak hledáš pozici b'/' a b'[' nebo b']' jen pomocí .find() na bytes
-
bytes.find
je implementováno v C a je extrémně rychlé. -
Vytvoříš pohled na
bytes
, nikoliv novoustr
ani listparts
.
-
Výsledkem je, že jsi celý diskový soubor přečetl do paměti jediným voláním OS, a každou řádku zpracováváš ~každý znak jen jednou bez Python‑level parsingových nástrojů.
Kombinace obou přístupů
-
Načti
.dic
přesmmap
-
Pro každý řádek vytáhni základ (base + flag) manuálně hledáním
b'/'
,b'['
ab']'
na byte‑level. -
Na base (a případně reversed‑base) nech Aho–Corasick (nastavený na tvé substrings) v jednom průchodu najít všechny odpovídající aff‑patterny naráz.
-
Aplikuj jim
apply()
pouze na ty, co automat skutečně vrátil.
Co tím získáš
-
Zpracování řádku bude O(len(line) + #matches) místo O(#rules).
-
Celá smyčka čtení + parsování se propadne z desítek milisekund na jednotky.
-
Jediným Python‑voláním uvnitř velké smyčky je
aho.iter()
(která rovnou volá C‑kód) a ruční sestavení výstupu.
Toto jsou dvě techniky, které se běžně používají v nejrychlejších text‑processing nástrojích (např. grep‑like implementace) a myslím, že ti otevřou úplně jiný level výkonu, než dosavadní čistě‑Pythonová řešení.
* * *
Aff není bolest, ale ten .dic
Dobře, pojďme se zaměřit čistě na tu největší bolest: desítky tisíc řádek v .dic
, kde teď děláš:
for line in f:
if exclude …: continue
parts = line.split('/',1)
if flag in rest: entries.append(base)
Každé volání .split()
, line[0] in …
a „ruční“ filtrování v Python‑smyčce je to, co tě stojí ~500 ms. Místo toho můžeš zpracovat celý soubor najednou čistě v C pomocí jediné multiline‑regex!
Jednorázové „grep‑like“ vytažení base/flags z .dic
-
Načti celý
.dic
dostr
(nebo ještě lépe dobytes
smmap
, viz níže). -
Použij jeden
re.compile
sre.MULTILINE
, který v C zpracuje všechny řádky:
import re
# exclude_pattern už např. "pro|na|do|u|roze?|…"
# flag je třeba escape‑nuté, např. "J"
combined = rf'''
^(?!{exclude_pattern}) # negativní lookahead pro exclude
(?P<base>[^/\r\n]+) # zachyť všechno až k '/'
/[^\[]*\[{flag}\] # pak cokoliv až k '[flag]'
'''
pattern = re.compile(combined, re.MULTILINE | re.VERBOSE)
with open(dic_path, 'r', encoding='utf-8') as f:
text = f.read()
# tohle běží v C a uskočí po každém řádku, který projde lookahead i flagem
bases = pattern.findall(text) # list of all base
-
Výhoda: celé parsování + filtrování exclude + extrakce base běží v C‑jádru regexu.
-
Výsledek: v
bases
máš ~70 000 položek během zlomku původního času.
Jak na mmap (ještě o chloupek rychleji)
import mmap
with open(dic_path, 'r+b') as f:
mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
# vytvoř pattern nad bytes, musíme de‑kódovat vlajky i exclude do byte‑level:
pat = re.compile(rb'^(?!' + exclude_bytes + rb')([^/\r\n]+)/[^\[]*\[' + flag_bytes + rb'\]', re.MULTILINE)
bases = [match.group(1).decode('utf-8') for match in pat.finditer(mm)]
mm.close()
-
mmap
načte celý soubor jediným systémovým voláním, -
pat.finditer(mm)
projde C‑kódem každý řádek, -
vrátíš si jen
group(1)
.
Reversed položky
Jakmile máš bases
:
normal = bases
reversed_ = [w[::-1] for w in bases] # Python‑level, ale jen O(n·avg_len)
A pak předáš normal
i reversed_
do tvé dump_dic_forms
.
Shrnutí
-
Namísto: Python‑smyčka +
split
+if exclude …
+if flag in …
-
Použiješ: jeden
re.compile(..., MULTILINE)
+findall
(nebofinditer
), ideálně nadmmap
.
Tohle ti z poolu ~500 ms vykleští do jednotek milisekund. A protože regex běží v C, už se nemusíš trápit hand‑rolled „first_letter“ hacky ani exclude_superfast
.
Vyzkoušej tu jednou regex‑přístup a změř si čas znovu – srovnání bude dramatické.
Žádné komentáře:
Okomentovat