Nome per questo tipo di parser, O perché non esiste


27

I parser convenzionali consumano tutto il loro input e producono un singolo albero di analisi. Sto cercando uno che consuma un flusso continuo e produce una foresta di analisi [ modifica: vedere la discussione nei commenti sul perché questo uso di quel termine potrebbe non essere convenzionale ]. Il mio istinto dice che non posso essere la prima persona a necessitare (o pensare di aver bisogno) di un simile parser, ma ho cercato e spento per mesi senza risultati.

Riconosco che potrei essere rapito dal problema XY. Il mio scopo finale è analizzare un flusso di testo, ignorandone la maggior parte, e produrre un flusso di alberi di analisi dalle sezioni riconosciute.

Quindi la mia domanda è condizionata: se esiste una classe di parser con queste caratteristiche, come si chiama? E se no, perché no? Qual è l'alternativa? Forse mi manca un modo per fare in modo che i parser convenzionali facciano quello che voglio.


1
Fondamentalmente il tuo parser analizza un singolo documento e produce un albero di analisi, quindi inizia immediatamente ad analizzare un altro documento, ecc. Suppongo che questa modifica del comportamento sia banale rispetto alla varietà di tecniche di analisi applicate a un singolo documento. Da qui la mancanza di un termine speciale per questo.
9000,

3
Ho fatto una ricerca su Google per "Parse Forest" e ho scoperto che Earley Parser li produce.
Robert Harvey,

7
Stai forse cercando combinatori di parser monadici , ovvero un parser più grande composto da più parser più piccoli. Sono utili per le situazioni in cui un'isola di una lingua è incorporata in un'altra. Il mio ex collega del team di progettazione C # Luke Hoban ha pubblicato un buon articolo: blogs.msdn.com/b/lukeh/archive/2007/08/19/…
Eric Lippert,

3
C'è un po 'di confusione. Vuoi dire che vuoi un albero di analisi per ogni documento nel tuo flusso e che formano insieme una foresta di analisi. Questo non è il solito significato di foresta di analisi. Una foresta di analisi è un insieme di alberi di analisi per un singolo documento ambiguo (che semplifica un po ') che può essere analizzato in diversi modi. Ed è di questo che parlano tutte le risposte. Il tuo stream è composto da molti documenti completi separati da immondizia o è un singolo documento che è stato parzialmente confuso. Il tuo documento dovrebbe essere sintatticamente corretto o no? La risposta tecnica adeguata dipende da questo.
babou,

1
Quindi dimentica tutte le risposte sulle foreste di analisi e Earley, GLR, Marpa, derivati. Apparentemente non sono ciò che vuoi a meno che non si presenti un'altra ragione. I tuoi documenti sono sintatticamente corretti? Alcune tecniche di analisi possono ricreare il contesto per documenti parzialmente confusi. Hai una sintassi precisa per questi documenti. È lo stesso per tutti? Vuoi davvero gli alberi di analisi, o saresti soddisfatto isolando i documenti e possibilmente analizzarli in un secondo momento, separatamente. Penso di sapere cosa potrebbe migliorare la tua elaborazione, ma non sono sicuro che tu possa farlo dallo scaffale.
babou,

Risposte:


48

Un parser che restituisce un risultato (parziale) prima che l'intero input sia stato consumato è chiamato parser incrementale . L'analisi incrementale può essere difficile se ci sono ambiguità locali in una grammatica che vengono decise solo più avanti nell'input. Un'altra difficoltà sta fingendo quelle parti dell'albero di analisi che non sono state ancora raggiunte.

Un parser che restituisce una foresta di tutti i possibili alberi di analisi, ovvero restituisce un albero di analisi per ogni possibile derivazione di una grammatica ambigua, viene chiamato ... Non sono sicuro che queste cose abbiano ancora un nome. So che il generatore di parser Marpa è in grado di farlo, ma qualsiasi parser basato su Earley o GLR dovrebbe essere in grado di farlo.


Tuttavia, sembra che tu non voglia nulla di tutto ciò. Hai un flusso con più documenti incorporati, con immondizia tra:

 garbagegarbage{key:42}garbagegarbage[1,2,3]{id:0}garbage...

Sembra che tu voglia un parser che salta sulla spazzatura e (pigramente) produce una sequenza di AST per ogni documento. Questo potrebbe essere considerato un parser incrementale nel suo senso più generale. Ma in realtà implementeresti un ciclo come questo:

while stream is not empty:
  try:
    yield parse_document(stream at current position)
  except:
    advance position in stream by 1 character or token

La parse_docmentfunzione sarebbe quindi un parser convenzionale, non incrementale. Vi è una piccola difficoltà nell'assicurarsi di aver letto abbastanza del flusso di input per un'analisi corretta. La modalità di gestione dipende dal tipo di parser che si sta utilizzando. Le possibilità includono la crescita di un buffer su determinati errori di analisi o l'utilizzo di tokenizzazione pigra.

La tokenizzazione pigra è probabilmente la soluzione più elegante grazie al tuo flusso di input. Invece di avere una fase lexer che produce un elenco fisso di token, il parser richiederebbe pigramente il token successivo da un callback lexer [1] . Il lexer consumerebbe quindi tutto il flusso necessario. In questo modo, il parser può avere esito negativo solo quando viene raggiunta la fine reale del flusso o quando si è verificato un errore di analisi reale (ovvero abbiamo iniziato l'analisi durante la spazzatura).

[1] un lexer basato sulla callback è una buona idea anche in altri contesti, perché questo può evitare alcuni problemi con la corrispondenza token più lungo .

Se sai che tipo di documenti stai cercando, puoi ottimizzare il salto per interrompere solo in posizioni promettenti. Ad esempio, un documento JSON inizia sempre con il carattere {o [. Pertanto, Garbage è qualsiasi stringa che non contiene questi caratteri.


5
Il tuo pseudocodice è in realtà quello che ho fatto, ma ho pensato che fosse solo un brutto hack. Il parser genera due tipi di eccezioni ( NO_MATCHe UNDERFLOW) che mi consentono di distinguere se devo avanzare la posizione del flusso o attendere ulteriori input.
Kevin Krumwiede,

5
@Kevin: lo uso anche io, con alcune funzionalità di sicurezza, per gestire i dati in arrivo da una rete in un formato proprietario. Niente di strano!
Lightness Races con Monica il

5

Non esiste un nome specifico per un parser che lo fa. Ma sottolineerò un algoritmo che lo fa: analizzare con i derivati .

Consuma input, un token alla volta. Produrrà una foresta di analisi alla fine dell'input. In alternativa, è anche possibile ottenere l'intera foresta di analisi mentre si sta eseguendo l'analisi (analisi parziale ).

L'analisi con derivati ​​gestisce grammatiche senza contesto e produrrà una foresta di analisi per grammatiche ambigue.

È una teoria elegante, davvero, ma è solo agli inizi, e non è ampiamente diffusa. Matt Might ha un elenco di collegamenti a varie implementazioni in Scala / Racket / ecc.

La teoria è più facile da imparare se inizi con il riconoscimento con le derivate (ovvero, inizia con l'assunzione di derivate di lingue , con l'obiettivo di riconoscere alcuni input per determinare se è valida o meno), e quindi modificare il programma per analizzare con le derivate ( vale a dire, cambiarlo così invece di prendere le derivate delle lingue , prende le derivate dei parser e calcola una foresta di analisi).


4
Downvoter: potresti per favore spiegare cosa è degno di un downvote? Se c'è qualcosa che devo correggere o migliorare, sarebbe sicuramente bello saperlo.
Cornstalks,

Non sono il downvoter e non mi sognerei di effettuare il downvoting senza un commento. Ma la tua carta entusiasta non ha alcun riferimento ai molti parser esistenti che ottengono lo stesso risultato, per quanto riguarda la complessità e l'analisi della foresta. La programmazione funzionale è ottima, ma anche confrontare un risultato con la letteratura esistente sull'argomento è bello. Quanto è conveniente la tua foresta di analisi per un ulteriore utilizzo?
babou,

@babou: per la cronaca, non sono l'autore di quel blog / documento. Ma sì, sono d'accordo che potrei aggiungere maggiori dettagli confrontando questo algoritmo con altri e spiegarlo in dettaglio. Matt Might ha un'intera lezione su di esso , ma sarebbe bello consolidarlo in questa risposta. Se avrò tempo proverò ad espandere questa risposta.
Cornstalks,

1
Non dedicare troppo tempo all'espansione. Per quanto ne so, questo non è ciò che l'OP sta cercando. La sua domanda richiede un'attenta lettura. Il suo uso della foresta di analisi non è tuo. - - Per quanto riguarda i derivati ​​... sembra che debba essere interessante, ma bisogna collegarlo a lavori precedenti ... e ne esiste una parte significativa. Ma non intendo in questa risposta, ma nei giornali di M Might, o nel suo blog.
babou,

2

Lungi dall'essere ideale, ma l'ho visto fatto più di una volta: ad ogni linea di input prova ad analizzare. se fallisce, mantieni la linea e aggiungi quella successiva. In pseudocodice:

buffer = ''
for each line from input:
    buffer = buffer + line
    if can parse buffer:
        emit tree
        buffer = ''

Il grosso problema è che in alcune lingue non puoi sapere se un'espressione è completa prima di leggere la riga successiva. In tal caso, sembra che tu possa leggere quello successivo e verificare se è un inizio valido o una continuazione valida ... Ma per questo hai bisogno della sintassi esatta della lingua

Peggio ancora, in quelle lingue non è difficile creare un caso patologico che non può essere analizzato fino alla fine del file, anche se non è stata una sola lunga dichiarazione.


0

In poche parole

Sembra che la soluzione rapida al tuo problema sia quella di definire un REGEX, o un FSA (automa a stati finiti), che riconosca tutti i possibili inizi dei documenti (sono consentiti falsi positivi, che non corrisponderebbero effettivamente a un documento). È quindi possibile eseguirlo molto rapidamente sull'input per identificare il punto successivo in cui un documento potrebbe iniziare con pochi errori. Potrebbe causare alcune posizioni errate per l'avvio di un documento, ma verranno riconosciute dal parser e abbandonate.

Quindi Finite State Automaton potrebbe essere il nome del parser che stavi cercando. :)

Il problema

È sempre difficile comprendere un problema pratico, soprattutto quando il vocabolario può avere molte interpretazioni. La parola foresta di analisi è stata coniata (afaik) per l'analisi senza contesto (CF) di frasi ambigue che hanno diversi alberi di analisi. Può essere in qualche modo generalizzato all'analisi di un reticolo di frasi o ad altri tipi di grammatica. Da qui tutte le risposte su Earley, GLR, Marpa e sui parser derivati ​​(ce ne sono molti altri) che non erano rilevanti in questo caso.

Ma a quanto pare non è quello che hai in mente. Vuoi analizzare una stringa univoca che è una sequenza di documenti non ambigui e ottenere un albero di analisi per ciascuno o un qualche tipo di rappresentazione strutturata, dal momento che non dici davvero come viene definita la sintassi dei tuoi documenti, da dove proviene un punto di vista formale del linguaggio. Quello che hai è un algoritmo e tabelle che eseguiranno il lavoro di analisi quando avviato all'inizio di un documento. Così sia.

Il vero problema è che il flusso di documenti contiene una notevole spazzatura che separa i documenti. E sembra che la tua difficoltà sia scansionare questa spazzatura abbastanza velocemente. La tua tecnica attuale è quella di iniziare all'inizio, provare a scansionare dal primo carattere e saltare al riavvio al carattere successivo ogni volta che fallisce, fino a quando non viene acquisito un intero documento. Quindi ripeti affermando il primo carattere dopo che il documento è stato appena scansionato.

Questa è anche la soluzione suggerita da @amon nella seconda parte della sua risposta .

Potrebbe non essere una soluzione molto veloce (non ho modo di testare), perché è improbabile che il codice del parser sia ottimizzato per essere avviato in modo molto efficiente all'inizio di un documento. Nell'uso normale lo fa solo una volta, quindi non è un hot spot dal punto di vista dell'ottimizzazione. Quindi, la tua moderata felicità con questa soluzione non è troppo sorprendente.

Quindi ciò di cui hai veramente bisogno è un algoritmo che possa trovare rapidamente l'inizio di un documento che inizia con una massa di immondizia. E tu sei fortunato: tali algoritmi esistono. E sono sicuro che lo sai: si chiama alla ricerca di un REGEX.

La soluzione semplice

Quello che devi fare è analizzare le specifiche dei tuoi documenti per scoprire come iniziano questi documenti. Non posso dirti esattamente come, poiché non sono sicuro di come siano organizzate formalmente le loro specifiche di sintassi. Forse iniziano tutti con una parola da un elenco finito, eventualmente mescolato con alcuni segni di punteggiatura o numeri. Questo è per te da controllare.

Quello che devi fare è definire un automa a stati finiti (FSA), o equivalentemente per la maggior parte dei programmatori un'espressione regolare (REGEX) in grado di riconoscere i primi caratteri di un documento: più sono, meglio è, ma non è necessario che sia molto grande (poiché ciò potrebbe richiedere tempo e spazio). Questo dovrebbe essere relativamente facile da fare dalle specifiche dei tuoi documenti e probabilmente può essere fatto automaticamente con un programma che legge le specifiche dei tuoi documenti.

Una volta prodotto regexp, è possibile eseguirlo sul flusso di input per arrivare molto rapidamente all'inizio del primo (o successivo) documento come segue:

Presumo:
- docstartè una regex che corrisponde all'inizio di tutti i documenti
- search(regex, stream)è una funzione che cerca streamuna sottostringa che corrisponde regex. Quando ritorna, il flusso viene ridotto al suo substream di suffisso a partire dall'inizio della prima sottostringa corrispondente, oppure al flusso vuoto se non viene trovata alcuna corrispondenza.
- parse(stream)tenta di analizzare un documento dall'inizio del flusso (ciò che ne rimane) e restituisce l'albero di analisi in qualsiasi formato o non riesce. Quando ritorna, il flusso viene ridotto al suo substream di suffisso a partire dalla posizione immediatamente successiva alla fine del documento analizzato. Chiama un'eccezione se l'analisi non riesce.

forest = empty_forest
search(docstart, stream)
while stream is not empty:
  try:
    forest = forest + parse(stream)
  except
    remove first character from stream
  search(docstart, stream)

Si noti che la rimozione del primo carattere è necessaria in modo che la ricerca successiva non trovi nuovamente la stessa corrispondenza.

Naturalmente, l'accorciamento del flusso è un'immagine. Potrebbe essere solo un indice sullo stream.

Un'ultima nota è che il tuo regex non deve essere troppo preciso, purché riconosca tutti gli inizi. Se a volte riconosce una stringa che non può essere l'inizio di un documento (falso positivo), l'unica penalità è il costo di una chiamata inutile al parser.

In modo che possa eventualmente aiutare a semplificare la regex, se utile.

Sulla possibilità di una soluzione più rapida

La soluzione di cui sopra dovrebbe funzionare abbastanza bene nella maggior parte dei casi. Tuttavia, se hai davvero un sacco di immondizia e terabyte di file da elaborare, potrebbero esserci altri algoritmi che funzionano più velocemente.

L'idea è derivata dall'algoritmo di ricerca delle stringhe di Boyer-Moore . Questo algoritmo può cercare uno stream per una singola stringa in modo estremamente rapido perché utilizza un'analisi strutturale della stringa per saltare la lettura della maggior parte dello stream, saltando su frammenti senza nemmeno guardarli. È l'algoritmo di ricerca più veloce per una singola stringa.

La difficoltà è che il suo adattamento alla ricerca di regex, piuttosto che a una singola stringa, sembra molto delicato e potrebbe non funzionare altrettanto bene, a seconda delle caratteristiche della regex che stai prendendo in considerazione. Ciò a sua volta potrebbe dipendere dalla sintassi dei documenti che stai analizzando. Ma non fidarti troppo di me, poiché non ho avuto il tempo di leggere attentamente i documenti che ho trovato.

Ti lascio con uno o due suggerimenti che ho trovato sul web, incluso uno che è apparentemente un documento di ricerca arbitrato , ma dovresti considerare questo come più speculativo, possibilmente ricercato, da considerare solo se hai avuto forti problemi di prestazioni. E probabilmente non c'è nessuno dei programmi sugli scaffali che lo farà.


-2

Quello che stai descrivendo può essere descritto come SAX vs. SOM.

SAX - (API semplice per XML) è un'API del parser ad accesso sequenziale di eventi sviluppato dalla mailing list XML-DEV per i documenti XML.

SOM - (XML Schema Object Model) accesso casuale alla rappresentazione in memoria di un file XML

Esistono implementazioni di entrambi i tipi in C # e Java e probabilmente molte altre. Di solito un XSD o DTD è opzionale.

La gioia di SAX è che ha un sovraccarico di memoria insufficiente, ottimo per file XML di grandi dimensioni. Il compromesso è che l'accesso casuale tramite SAX è inesistente o lento e, peggio ancora, il tempo di sviluppo è di solito considerevole più che con SOM. L'ovvio problema con SOM sono requisiti RAM potenzialmente elevati.

Questa risposta non è applicabile per tutte le piattaforme e tutte le lingue.


1
Perché pensi che l'OP stia analizzando XML?
Dan Pichelman,

1
Questo non risponde alla domanda.

@Snowman Quasi nulla finora ha risposto alla domanda, inclusa la prima metà della risposta accettata. Non ha senso scegliere nessuno. La domanda richiede un'attenta lettura.
babou,

@babou Non stavo prendendo in giro nessuno, stavo spiegando il mio voto negativo.

@Snowman che spiega il mio downvote . È giusto e vorrei che lo facessero più utenti. Non sono un madrelingua: scegliere lui potrebbe essere un'espressione troppo forte. È solo che tutti hanno formulato ipotesi ingiustificate. Quindi non vale nemmeno la pena notare. È vero che questo sembra un po 'più fuori degli altri.
babou,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.