tidigare idag, jag sa en arbetskollega som re.finditer () i Python är vanligtvis det föredragna sättet att tokenisera inmatning, snarare än att handrullera en lexer från en iterabel av tecken. Han sa att det kräver att man läser hela filen i minnet. På plats, jag sheepishly sa att du förmodligen kan läsa den rad för rad och ringa re.finditer () upprepade gånger om token regexp inte korsar linje gräns.
det är sant, men inte ett bra svar. Det visar sig att konstruera en kedjad re.finditer () från en iterable av strängar är möjligt men inte helt enkelt.
det första försöket här visar huvudtanken: eftersom re.finditer () matcher är icke-överlappande, efter alla matcher är uttömda, nästa re.finditer () bör återupptas från resten av den oöverträffade strängen.
def finditer_chained_bad(pattern, strings, flags=0): last = '' for s in strings: m = None for m in re.finditer(pattern, last + s, flags): yield m if not m: last = '' ##1 continue # m is the last match object. last = s
raden med kommentaren ##1
kan vara en stridspunkt. Om re.finditer () hade inte gett någon match, genomförandebeslutet här är att kassera s helt förutsatt att ingenting i det någonsin skulle matcha, men man kan hävda att en potentiell matchning skulle kunna korsa inmatningssträngsgränsen. Om detta ändrades till last += s
, skulle det fungera om den potentiella matchningen passerar ingångsgränsen, på bekostnad av obegränsad minnestillväxt om en lång spännvidd av ingången aldrig gav någon matchning. Den obegränsade tillväxten är nödvändig eftersom det betyder att en potentiell matchning är större än ingångsstorleken, så för att få hela matchen måste vi dynamiskt öka bitstorleken, vilket är vad vi gör här.
ett annat problem är att den sista matchen kan vara en partiell match som fortsätter in i nästa sträng. Om vi ger det naivt, den som ringer skulle få en trasig partiell match. För att illustrera detta, anta att ingången är chunked i 8 teckensträngar:
def main(): for m in finditer_chained_bad(r'\w+', ): print m.group(0)
den skriver ut:
helloworld
ordet ”Värld” bryts av misstag i två matcher på grund av stränggränsen. För att åtgärda detta, vi behöver bara skjuta upp den sista matchen och starta om nästa re.finditer () från början av den sista matchen. Den fullständiga arbetskoden blir:
def finditer_chained(pattern, strings, flags=0): last_s = '' for s in strings: last_m = None last_s += s for m in re.finditer(pattern, last_s, flags): if last_m: yield last_m last_m = m if not last_m: continue assert last_m is m last_s = s if last_m: yield last_m
och den skriver korrekt ut:
helloworld
den resulterande finditer_chained() accepterar ett filliknande objekt eftersom en fil är en iterable av linjer, som om re.finditer () tillämpas på hela filen. Och det fungerar utan att läsa hela filen i minnet.