Il toolbox del Data Scientist Parsing

La cassetta degli attrezzi del Data Scientist il parsing

Il parsing di documenti complessi può essere facile se si hanno gli strumenti giusti

Codice sorgente del nuovo parser rd2md e del trasformatore basato su Python discusso in questo articolo. Immagine dell'autore.

Per molti Data Scientist, convertire documenti complessi in dati utilizzabili è un problema comune. Esaminiamo un documento complesso e esploriamo diversi metodi di trasformazione dei dati.

TL;DR;

Esploreremo queste regole mentre sviluppiamo un parser complesso:

Regola 1: Sii pigro; non fare più di quanto necessarioRegola 2: Inizia con le parti facili del problemaRegola 3: Non aver paura di eliminare il codice e ricominciare!Regola 4: Usa il metodo più semplice possibile per completare il lavoro.

Il problema

Come Capo della Ricerca in un’azienda di ML, mi trovo spesso di fronte a una varietà di problemi che devono essere esplorati e soluzioni progettate. La scorsa settimana è sorto un interessante piccolo problema: avevamo bisogno di un modo per generare documentazione in formato markdown per il nostro SDK R open source, che consente agli esperimenti di ML di registrare dettagli importanti. E avevamo bisogno di una soluzione veloce senza spendere molto tempo su di essa.

Questo problema può essere un po’ più complesso rispetto a quello che un Data Scientist incontrerebbe quotidianamente, ma servirà come un bel esempio di come utilizzare diversi metodi di parsing. E come bonus, otterremo un nuovo progetto open source che copre una nicchia particolare. Iniziamo!

Appena ho sentito del problema, è entrata in gioco la mia prima regola di Ricerca e Progettazione (R&D):

Regola 1: Sii pigro; non fare più di quanto necessario (La pigrizia è stata identificata da Larry Wall come una delle Tre Grandi Virtù di un Programmatore).

Quindi ho iniziato a cercare se la conversione del codice R in markdown fosse un problema risolto. Sembra che lo fosse! Tuttavia, dopo aver provato tutti i programmi disponibili che ho trovato (come il vecchio Rd2md di R) non hanno funzionato e i repository git non erano più attivi. Ok, ero da solo. Se fossi un programmatore R migliore, probabilmente avrei cercato di correggere le soluzioni esistenti. Ma preferisco Python e ho pensato che sarebbe stato un bel esempio di parsing. Quindi sì, analizzeremo la documentazione di R in Python.

Quindi ho iniziato a scrivere del codice. E questo mi ha ricordato la mia prossima regola di R&D:

Regola 2: Inizia con le parti facili del problema.

La Regola 2 è probabilmente solo il mio modo di soddisfare il mio bisogno di avere un feedback istantaneo. Ma serve anche a risolvere un ruolo più importante: se si inizia con le parti facili, forse le parti difficili non risulteranno così difficili. Serve anche come un riscaldamento per iniziare a lavorare su un problema. Di solito faccio uno o due tentativi falliti nella scrittura di una soluzione. Il che mi porta alla mia prossima regola:

Regola 3: Non aver paura di eliminare il codice e ricominciare!

Infine, quando si è sulla strada giusta, l’ultima regola è:

Regola 4: Usa il metodo più semplice possibile per completare il lavoro.

Ok, qual è il metodo più semplice per convertire i file di documentazione di R in markdown? Prima di tutto, cosa è un file di documentazione di R? La documentazione di R viene convertita direttamente dal codice R in qualcosa che assomiglia molto a LaTeX. Ecco un esempio (i file terminano in .Rd):

% Generato da roxygen2: non modificare manualmente% Si prega di modificare la documentazione in R/esperimento.R\nome{Esperimento}\alias{Esperimento}\title{Un oggetto Esperimento di Comet}\description{Un oggetto esperimento di Comet può essere utilizzato per modificare o ottenere informazioni su un esperimento attivo. Tutti i metodi documentati qui sono i diversi modi per interagire con un esperimento. Utilizzare \code{\link[=create_experiment]{create_experiment()}} per creare o \code{\link[=get_experiment]{get_experiment()}} per recuperare un oggetto esperimento di Comet.}

L’obiettivo è convertire il LaTeX in un markdown che assomiglia a qualcosa del genere:

## DescrizioneUn oggetto di esperimento cometa può essere utilizzato per modificare o ottenere informazioni su un esperimento attivo. Tutti i metodi documentati qui sono i diversi modi per interagire con un esperimento. Utilizzare [`create_experiment()`](../create_experiment) per creare o [`get_experiment()`](../get_experiment) perrilevare un oggetto di esperimento cometa.

che appare come:

Esempio di output markdown. Immagine dell'autore.

Ok, cominciamo con qualcosa di molto semplice. Passeremo riga per riga del file Rd con qualcosa del genere:

doc = Documentazione()...per riga in righe:    if riga.startswith("%"):        pass    elif riga.startswith("\\name{"):        corrispondenze = re.search("{(.*)}", riga)        gruppi = gruppi()        nome = gruppi[0]        doc.set_name(nome)    ...

In questo codice, controlliamo se una riga inizia con “%” e se lo fa, la saltiamo (è solo un commento nel file Rd). Allo stesso modo, se inizia con “\name” impostiamo il nome della doc attuale. Nota che dobbiamo fare l’escape del backslash se non usiamo stringhe Python “raw”. Il codice re.search(“{(.*)}”, riga) assume che la riga contenga la parentesi graffa di chiusura. Questa presunzione è vera in tutti gli esempi del nostro SDK, quindi non renderò questo codice più complicato di quanto sia necessario, come indica la Regola 3.

Nota che costruiamo un’istanza di Documentazione() prima di elaborare le righe del file. Facciamo questo per raccogliere tutte le parti e chiamare poi doc.generate() alla fine. Facciamo questo (anziché generare il markdown su due piedi) perché alcuni degli elementi che analizziamo saranno in un ordine diverso nel markdown.

Possiamo gestire parte del codice R esattamente in questa modalità: cercare un modello su una riga del file Rd e elaborarlo immediatamente. Tuttavia, guardiamo la parte successiva che non può essere gestita in questo modo:

\usage{create_experiment(  experiment_name = NULL,  project_name = NULL,  workspace_name = NULL,  api_key = NULL,  keep_active = TRUE,  log_output = TRUE,  log_error = FALSE,  log_code = TRUE,  log_system_details = TRUE,  log_git_info = FALSE)}

La sezione di utilizzo inizia sempre con una riga che è \usage{ e termina con una riga che è una singola }. Poiché è così, possiamo usare questi fatti per creare un parser leggermente più complicato:

...per riga in righe:    ...    elif riga.startswith("\\usage{"):        utilizzo = ""        riga = fp_in.readline().rstrip()        while riga != "}":            utilizzo += riga + "\n"            riga = fp_in.readline().rstrip()        doc.set_usage(utilizzo)

Questo leggerà riga per riga, raccogliendo tutto il testo nella sezione \usage{}.

Mentre passiamo alla parte successiva più complicata, dobbiamo essere un po’ intelligenti e, per la prima volta, utilizzare l’idea di “stato”.

Considera questo codice LaTeX:

\item{log_error}{If \code{TRUE}, all output from 'stderr' (which includes errors,warnings, and messages) will be redirected to the Comet servers to display as messagelogs for the experiment. Note that unlike \code{auto_log_output}, if this option is on thenthese messages will not be shown in the console and instead they will only be loggedto the Comet experiment. This option is set to \code{FALSE} by default because of thisbehavior.}

Questa è un po’ complicata. Il formato di livello superiore è:

\item{NOME}{DESCRIZIONE}

Tuttavia, DESCRIZIONE può a sua volta avere elementi tra parentesi graffe all’interno di essa. Se avessi questa sezione di codice come stringa (anche con nuove righe), potresti usare il modulo re (Regular Expression) di Python, così:

testo = """\item{log_error}{If \code{TRUE}, all output from 'stderr' (which includes errors,warnings, and messages) will be redirected to the Comet servers to display as messagelogs for the experiment. Note that unlike \code{auto_log_output}, if this option is on thenthese messages will not be shown in the console and instead they will only be loggedto the Comet experiment. This option is set to \code{FALSE} by default because of thisbehavior.}"""corrispondenze = re.search("{(.*)}{(.*)}", testo, re.DOTALL)

Puoi ottenere il NOME e la DESCRIZIONE come matches.groups(). Le parentesi nel modello di re “{(.*)}{(.*)}” indicano di abbinare due gruppi: il primo gruppo tra le prime parentesi graffe e il secondo gruppo tra le successive. Questo funziona molto bene, assumendo che il testo sia solo quella sezione. Per essere in grado di analizzarlo senza prima dividere questa sezione, effettivamente dovremo analizzare il testo, carattere per carattere. Ma non è difficile.

Ecco una piccola funzione che otterrà un numero di sezioni tra parentesi graffe dato un puntatore al file (noto anche come “file-like” nell’uso moderno di Python):

def get_curly_contents(number, fp):    retval = []    count = 0    current = ""    while True:        char = fp.read(1)        if char == "}":            count -= 1            if count == 0:                if current.startswith("{"):                    retval.append(current[1:])                elif current.startswith("}{"):                    retval.append(current[2:])                else:                    raise Exception("malformed?", current)                current = ""        elif char == "{":            count += 1        if len(retval) == number:            return retval        current += char

Nella funzione get_curly_contents() dovresti passare il numero di sezioni tra parentesi graffe e un puntatore al file. Quindi, per ottenere 2 sezioni tra parentesi graffe da un file, puoi fare:

fp = open(NOME_FILE)nome, descrizione = get_curly_contents(2, fp)

get_curly_contents() è quasi complesso come si può ottenere in questo progetto. Ha tre variabili di stato: retval, count e current. retval è una lista di sezioni analizzate. count è la profondità degli oggetti tra parentesi graffe correnti. current è ciò che viene elaborato attualmente. Questa funzione è effettivamente utile in alcuni punti, come vedremo.

Infine, c’è un altro livello di complessità. L’area problematica è la sottosezione Metodo di una definizione di classe R. Ecco un esempio ridotto al minimo:

\if{html}{\out{<hr>}}\if{html}{\out{<a id="method-Experiment-new"></a>}}\if{latex}{\out{\hypertarget{method-Experiment-new}{}}}\subsection{Metodo \code{new()}}{Non chiamare direttamente questa funzione. Usa invece \code{create_experiment()} o \code{get_experiment()}.\subsection{Uso}{\if{html}{\out{<div class="r">}}\preformatted{Experiment$new(  experiment_key,  project_name = NULL)}\if{html}{\out{</div>}}}\subsection{Argomenti}{\if{html}{\out{<div class="arguments">}}\describe{\item{\code{experiment_key}}{La chiave dell'Esperimento.}\item{\code{project_name}}{Il nome del progetto (può anche essere specificato utilizzando il parametro \code{COMET_PROJECT_NAME} come variabile d'ambiente o in un file di configurazione di Comet).}}\if{html}{\out{</div>}}}}

Questa è complicata perché abbiamo sezioni nidificate: Uso e Argomenti sono all’interno di Metodo. Andremo ad utilizzare tutto il nostro arsenale di analisi per questo.

Per rendere le cose un po’ più semplici, la prima cosa che faremo sarà “tokenizzare” la sottosezione Metodo. Quello è un termine elegante per suddividere il testo in stringhe rilevanti. Ad esempio, considera questo testo in LaTeX:

\subsection{Uso}{\if{html}{\out{<div class="r">}}\preformatted{Experiment$new(  experiment_key,  project_name = NULL)}\if{html}{\out{</div>}}}

Può essere suddiviso in una lista di stringhe, come:

[ "\\", "subsection", "{", "Usage", "}", "\\", "if", "{", "html", "}", "{", "\\", "out", "{", "<", "div",  " ",  "class", "=", "\"r\"", ">", "}", "}", "\\",  "preformatted", "{", "Experiment$new", "(", "experiment_key", "project_name", "=", "NULL", ")", "}", "\\", "if", "{", "html", "}", "{", "\\", "out", "{", "<", "/", "div", ">", "}", "}", "}"]

Un elenco di stringhe tokenizzate ti permette di elaborarlo facilmente nelle sue parti separate. Inoltre, puoi facilmente “guardare avanti” uno o più token per vedere cosa sta arrivando. Questo può essere difficile da fare con le Espressioni Regolari, o se stai lavorando con singoli caratteri anziché token. Ecco un esempio di analisi di una sezione tokenizzata:

doc = Documentazione()...metodo = Metodo()posizione = 0preamble = ""tokens = tokenizza(testo)while posizione < len(tokens):    token = tokens[posizione]    if token == "\\":        if tokens[posizione + 1] == "sottosezione":            in_preamble = False            if tokens[posizione + 3] == "Utilizzo":                posizione, utilizzo = ottieni_sezione_tokenizzata(                    posizione + 5, tokens                )                 metodo.imposta_utilizzo(utilizzo)            elif tokens[posizione + 3] == "Argomenti":                # salta questo, lo otterremo con descrittivo                posizione += 5            elif tokens[posizione + 3] == "Esempi":                posizione, esempi = ottieni_sezione_tokenizzata(                    posizione + 5, tokens                )                metodo.imposta_esempi(esempi)            elif tokens[posizione + 3] == "Restituisce":                posizione, restituisce = ottieni_sezione_tokenizzata(                    posizione + 5, tokens                )                 metodo.imposta_restituisce(restituisce)            else:                raise Exception("sottosezione sconosciuta:", tokens[posizione + 3])        elif tokens[posizione + 1] == "descrittivo":            posizione, descrittivo = ottieni_sezione_tokenizzata(posizione + 2, tokens)  # noqa            metodo.imposta_descrittivo(descrittivo)        else:            # \html            posizione += 1    else:        if in_preamble:            preamble += token        posizione += 1metodo.imposta_preamble(preamble)doc.aggiungi_metodo(metodo)

Ecco fatto! Per vedere il progetto finito, controlla il nuovo rd2md basato su Python. È una libreria Python open source installabile tramite pip per generare markdown dai file Rd di R. L’abbiamo utilizzata nella nostra documentazione R:

https://www.comet.com/docs/v2/api-and-sdk/r-sdk/overview/

È un po’ una soluzione “improvvisata” del pomeriggio? Sì. È composta da non meno di 4 metodi diversi di analisi. Ma fa il suo lavoro ed è l’unico convertitore Rd in markdown che funziona che conosco. Se dovessi rifattorizzarlo, probabilmente tokenizzerei l’intero file prima e poi lo elaborerei utilizzando l’ultimo metodo mostrato. Ricorda la Regola 3: Non avere paura di eliminare il codice e ricominciare da capo!

Se vuoi contribuire al repository di github, fallo pure. Se hai domande, faccelo sapere nelle issue.

Interessato all’Intelligenza Artificiale, all’Apprendimento Automatico e alla Scienza dei Dati? Considera un applauso e un follow. Doug è il Responsabile della Ricerca presso comet.com, un’azienda di tracciamento sperimentale di machine learning e monitoraggio dei modelli.