Tokenización

En esta sección veremos tareas sencillas de reconocimiento de entidades. Para ello usaremos tres librerías nuevas, una de las cuales (openNLP) es una interfaz a una librería en Java. Usaremos además modelos pre-entrenados que no están disponibles en CRAN (debido debido a la política del repositorio):

library(NLP)
library(openNLP)

## install.packages("openNLPmodels.en",
##                  repos="http://datacube.wu.ac.at/",
##                  type="source")

Vamos a hacer una tarea sencilla de separar un texto en oraciones y en palabras. En concreto empezaremos con uno de los textos que estudiaremos en la siguiente sección

text <- readLines("./dta/senate-releases/Vitter/14Jun2007Vitter110.txt")

y lo transformaremos en un formato más adecuado para trabajar con NLP:

text <- as.String(text)

Las funciones en openNLP funcionan siempre del mismo modo. En primer lugar, inicializamos un anotador y después aplicamos ese anotador a un texto. Por ejemplo, podemos empezar por anotar palabras y oraciones utilizando las funciones:

word_ann <- Maxent_Word_Token_Annotator()
sent_ann <- Maxent_Sent_Token_Annotator()

y a continuación anotamos el texto aplicando estos dos objetos al texto que queremos analizar:

annotated_text <- annotate(text, list(sent_ann, word_ann))

Lo que obtenemos es un objeto de clase

class(annotated_text)

que en realidad se parece mucho a un data.frame en el que tenemos la lista de oraciones y palabras identificadas por la posición en la que empiezan y en la que acaban. Podemos, por ejemplo, recuperar el total de oraciones:

head(subset(annotated_text, type=="sentence"))

y podemos comprobar el contenido de esta oración

subset(annotated_text, type=='sentence')[[4]]$features
text[subset(annotated_text, type=='sentence')[[4]]]

También podríamos recuperar las palabras que constituyen el texto de modo similar

head(subset(annotated_text, type=="word"))

Quizás sea más fácil explorar los resultados utilizando la función AnnotatedPlainTextDocument:

adoc <- AnnotatedPlainTextDocument(text, annotated_text)
head(sents(adoc))
head(words(adoc))

Anotador de entidades y POS

Podemos utilizar el mismo proceso para anotar entidades dentro de un texto. Por ejemplo, podemos anotar personas, localizaciones, organizaciones y fechas dentro del documento que ya hemos anotado anteriormente.

person_ann <- Maxent_Entity_Annotator(kind="person")
loc_ann <- Maxent_Entity_Annotator(kind="location")
org_ann <- Maxent_Entity_Annotator(kind="organization")
date_ann <- Maxent_Entity_Annotator(kind="date")

y ahora aplicamos estos nuevos anotadores al texto que ya tokenizamos previamente:

annotated_text <- annotate(text,
                          list(person_ann, loc_ann, org_ann, date_ann),
                          annotated_text)

podemos ver que, a la lista de tokens, se han añadido las entidades que el clasificador ha logrado identificar.

subset(annotated_text, type=="entity")$features

y ahora podemos ver las entidades que hemos recuperado

sel <- subset(annotated_text, type=="entity")
cbind(unlist(sel$features), text[sel])

La anotación de POS sigue la misma lógica. Ahora generaremos un nuevo objeto en el que inicializamos el anotador de POS y aplicamos este nuevo anotador a nuestro texto:

pos_ann <- Maxent_POS_Tag_Annotator()
pos_annotated_text <- annotate(text, pos_ann, annotated_text)

Ahora lo que tenemos en la columna de features es una indiación de la POS de cada una de las palabras

head(subset(pos_annotated_text, type=="word"))

En este caso vemos que, por ejemplo,

text[subset(pos_annotated_text, type=="word")[55]]
subset(pos_annotated_text, type=="word")[55]

está etiquetado como NNS que se corresponde como sustantivo plural y

text[subset(pos_annotated_text, type=="word")[100]]
subset(pos_annotated_text, type=="word")[100]

está etiquetado como JJ que es un adjetivo. La lista completa de abreviaturas es:

Símbolo Significado
CC Coordinating conjunction
CD Cardinal number
DT Determiner
EX Existential there
FW Foreign word
IN Preposition or subordinating conjunction
JJ Adjective
JJR Adjective, comparative
JJS Adjective, superlative
LS List item marker
MD Modal
NN Noun, singular or mass
NNS Noun, plural
NNP Proper noun, singular
NNPS Proper noun, plural
PDT Predeterminer
POS Possessive ending
PRP Personal pronoun
PRP$ Possessive pronoun
RB Adverb
RBR Adverb, comparative
RBS Adverb, superlative
RP Particle
SYM Symbol
TO to
UH Interjection
VB Verb, base form
VBD Verb, past tense
VBG Verb, gerund or present participle
VBN Verb, past participle
VBP Verb, non­3rd person singular present
VBZ Verb, 3rd person singular present
WDT Wh­determiner
WP Wh­pronoun
WP$ Possessive wh­pronoun
WRB Wh­adverb

Hemos visto que las etiquetaciones son en realidad el resultado de un modelo estadístico. Podemos pedirle al anotador que nos devuelva las probabilidades asociadas a la etiqueta escogida con el fin de tener información acerca de la incertidumbre.

pos_ann <- Maxent_POS_Tag_Annotator(probs=TRUE)
pos_annotated_text <- annotate(text, pos_ann, annotated_text)
head(subset(pos_annotated_text, type=="word"))
LS0tIAp0aXRsZTogIlByb2Nlc2FtaWVudG8gZGUgbGVuZ3VhamUgbmF0dXJhbCIKZGF0ZTogImByIGZvcm1hdChTeXMudGltZSgpLCAnJUIgJWQsICVZJylgIgotLS0KCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFLCBjYWNoZT1GQUxTRX0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGV2YWwgPSBGQUxTRSkgCmBgYAoKIyMgVG9rZW5pemFjacOzbgpFbiBlc3RhIHNlY2Npw7NuIHZlcmVtb3MgdGFyZWFzIHNlbmNpbGxhcyBkZSByZWNvbm9jaW1pZW50byBkZSBlbnRpZGFkZXMuIFBhcmEKZWxsbyB1c2FyZW1vcyB0cmVzIGxpYnJlcsOtYXMgbnVldmFzLCB1bmEgZGUgbGFzIGN1YWxlcyAoYG9wZW5OTFBgKSBlcyB1bmEKaW50ZXJmYXogYSB1bmEgbGlicmVyw61hIGVuIEphdmEuIFVzYXJlbW9zIGFkZW3DoXMgbW9kZWxvcyBwcmUtZW50cmVuYWRvcyBxdWUgbm8KZXN0w6FuIGRpc3BvbmlibGVzIGVuIENSQU4gKGRlYmlkbyBkZWJpZG8gYSBsYSBwb2zDrXRpY2EgZGVsIHJlcG9zaXRvcmlvKTogCgpgYGB7cn0KbGlicmFyeShOTFApCmxpYnJhcnkob3Blbk5MUCkKCiMjIGluc3RhbGwucGFja2FnZXMoIm9wZW5OTFBtb2RlbHMuZW4iLAojIyAgICAgICAgICAgICAgICAgIHJlcG9zPSJodHRwOi8vZGF0YWN1YmUud3UuYWMuYXQvIiwKIyMgICAgICAgICAgICAgICAgICB0eXBlPSJzb3VyY2UiKQpgYGAKClZhbW9zIGEgaGFjZXIgdW5hIHRhcmVhIHNlbmNpbGxhIGRlIHNlcGFyYXIgdW4gdGV4dG8gZW4gb3JhY2lvbmVzIHkgZW4gcGFsYWJyYXMuCkVuIGNvbmNyZXRvIGVtcGV6YXJlbW9zIGNvbiB1bm8gZGUgbG9zIHRleHRvcyBxdWUgZXN0dWRpYXJlbW9zIGVuIGxhIHNpZ3VpZW50ZQpzZWNjacOzbgoKYGBge3J9CnRleHQgPC0gcmVhZExpbmVzKCIuL2R0YS9zZW5hdGUtcmVsZWFzZXMvVml0dGVyLzE0SnVuMjAwN1ZpdHRlcjExMC50eHQiKQpgYGAKCnkgbG8gdHJhbnNmb3JtYXJlbW9zIGVuIHVuIGZvcm1hdG8gbcOhcyBhZGVjdWFkbyBwYXJhIHRyYWJhamFyIGNvbiBgTkxQYDoKCmBgYHtyfQp0ZXh0IDwtIGFzLlN0cmluZyh0ZXh0KQpgYGAKCkxhcyBmdW5jaW9uZXMgZW4gYG9wZW5OTFBgIGZ1bmNpb25hbiBzaWVtcHJlIGRlbCBtaXNtbyBtb2RvLiBFbiBwcmltZXIgbHVnYXIsCmluaWNpYWxpemFtb3MgdW4gX2Fub3RhZG9yXyB5IGRlc3B1w6lzIGFwbGljYW1vcyBlc2UgYW5vdGFkb3IgYSB1biB0ZXh0by4gUG9yCmVqZW1wbG8sIHBvZGVtb3MgZW1wZXphciBwb3IgYW5vdGFyIHBhbGFicmFzIHkgb3JhY2lvbmVzIHV0aWxpemFuZG8gbGFzCmZ1bmNpb25lczogCgpgYGB7cn0Kd29yZF9hbm4gPC0gTWF4ZW50X1dvcmRfVG9rZW5fQW5ub3RhdG9yKCkKc2VudF9hbm4gPC0gTWF4ZW50X1NlbnRfVG9rZW5fQW5ub3RhdG9yKCkKYGBgCgp5IGEgY29udGludWFjacOzbiBhbm90YW1vcyBlbCB0ZXh0byBhcGxpY2FuZG8gZXN0b3MgZG9zIG9iamV0b3MgYWwgdGV4dG8gcXVlCnF1ZXJlbW9zIGFuYWxpemFyOiAKCmBgYHtyfQphbm5vdGF0ZWRfdGV4dCA8LSBhbm5vdGF0ZSh0ZXh0LCBsaXN0KHNlbnRfYW5uLCB3b3JkX2FubikpCmBgYAoKTG8gcXVlIG9idGVuZW1vcyBlcyB1biBvYmpldG8gZGUgY2xhc2UKYGBge3J9CmNsYXNzKGFubm90YXRlZF90ZXh0KQpgYGAKCnF1ZSBlbiByZWFsaWRhZCBzZSBwYXJlY2UgbXVjaG8gYSB1biBgZGF0YS5mcmFtZWAgZW4gZWwgcXVlIHRlbmVtb3MgbGEgbGlzdGEgZGUKb3JhY2lvbmVzIHkgcGFsYWJyYXMgaWRlbnRpZmljYWRhcyBwb3IgbGEgcG9zaWNpw7NuIGVuIGxhIHF1ZSBlbXBpZXphbiB5IGVuIGxhCnF1ZSBhY2FiYW4uIFBvZGVtb3MsIHBvciBlamVtcGxvLCByZWN1cGVyYXIgZWwgdG90YWwgZGUgb3JhY2lvbmVzOiAKCmBgYHtyfQpoZWFkKHN1YnNldChhbm5vdGF0ZWRfdGV4dCwgdHlwZT09InNlbnRlbmNlIikpCmBgYAoKeSBwb2RlbW9zIGNvbXByb2JhciBlbCBjb250ZW5pZG8gZGUgZXN0YSBvcmFjacOzbgoKYGBge3J9CnN1YnNldChhbm5vdGF0ZWRfdGV4dCwgdHlwZT09J3NlbnRlbmNlJylbWzRdXSRmZWF0dXJlcwp0ZXh0W3N1YnNldChhbm5vdGF0ZWRfdGV4dCwgdHlwZT09J3NlbnRlbmNlJylbWzRdXV0KYGBgCgpUYW1iacOpbiBwb2Ryw61hbW9zIHJlY3VwZXJhciBsYXMgcGFsYWJyYXMgcXVlIGNvbnN0aXR1eWVuIGVsIHRleHRvIGRlIG1vZG8Kc2ltaWxhcgpgYGB7cn0KaGVhZChzdWJzZXQoYW5ub3RhdGVkX3RleHQsIHR5cGU9PSJ3b3JkIikpCmBgYAoKUXVpesOhcyBzZWEgbcOhcyBmw6FjaWwgZXhwbG9yYXIgbG9zIHJlc3VsdGFkb3MgdXRpbGl6YW5kbyBsYSBmdW5jacOzbiBgQW5ub3RhdGVkUGxhaW5UZXh0RG9jdW1lbnRgOgoKYGBge3J9CmFkb2MgPC0gQW5ub3RhdGVkUGxhaW5UZXh0RG9jdW1lbnQodGV4dCwgYW5ub3RhdGVkX3RleHQpCmhlYWQoc2VudHMoYWRvYykpCmhlYWQod29yZHMoYWRvYykpCmBgYAoKIyMgQW5vdGFkb3IgZGUgZW50aWRhZGVzIHkgUE9TCgpQb2RlbW9zIHV0aWxpemFyIGVsIG1pc21vIHByb2Nlc28gcGFyYSBhbm90YXIgZW50aWRhZGVzIGRlbnRybyBkZSB1biB0ZXh0by4gUG9yCmVqZW1wbG8sIHBvZGVtb3MgYW5vdGFyIHBlcnNvbmFzLCBsb2NhbGl6YWNpb25lcywgb3JnYW5pemFjaW9uZXMgeSBmZWNoYXMgZGVudHJvCmRlbCBkb2N1bWVudG8gcXVlIHlhIGhlbW9zIGFub3RhZG8gYW50ZXJpb3JtZW50ZS4gCgpgYGB7cn0KcGVyc29uX2FubiA8LSBNYXhlbnRfRW50aXR5X0Fubm90YXRvcihraW5kPSJwZXJzb24iKQpsb2NfYW5uIDwtIE1heGVudF9FbnRpdHlfQW5ub3RhdG9yKGtpbmQ9ImxvY2F0aW9uIikKb3JnX2FubiA8LSBNYXhlbnRfRW50aXR5X0Fubm90YXRvcihraW5kPSJvcmdhbml6YXRpb24iKQpkYXRlX2FubiA8LSBNYXhlbnRfRW50aXR5X0Fubm90YXRvcihraW5kPSJkYXRlIikKYGBgCgp5IGFob3JhIGFwbGljYW1vcyBlc3RvcyBudWV2b3MgYW5vdGFkb3JlcyBhbCB0ZXh0byBxdWUgeWEgdG9rZW5pemFtb3MKcHJldmlhbWVudGU6CgpgYGB7cn0KYW5ub3RhdGVkX3RleHQgPC0gYW5ub3RhdGUodGV4dCwKICAgICAgICAgICAgICAgICAgICAgICAgICBsaXN0KHBlcnNvbl9hbm4sIGxvY19hbm4sIG9yZ19hbm4sIGRhdGVfYW5uKSwKICAgICAgICAgICAgICAgICAgICAgICAgICBhbm5vdGF0ZWRfdGV4dCkKYGBgCnBvZGVtb3MgdmVyIHF1ZSwgYSBsYSBsaXN0YSBkZSB0b2tlbnMsIHNlIGhhbiBhw7FhZGlkbyBsYXMgZW50aWRhZGVzIHF1ZSBlbApjbGFzaWZpY2Fkb3IgaGEgbG9ncmFkbyBpZGVudGlmaWNhci4gCgpgYGB7cn0Kc3Vic2V0KGFubm90YXRlZF90ZXh0LCB0eXBlPT0iZW50aXR5IikkZmVhdHVyZXMKYGBgCgp5IGFob3JhIHBvZGVtb3MgdmVyIGxhcyBlbnRpZGFkZXMgcXVlIGhlbW9zIHJlY3VwZXJhZG8gCgpgYGB7cn0Kc2VsIDwtIHN1YnNldChhbm5vdGF0ZWRfdGV4dCwgdHlwZT09ImVudGl0eSIpCmNiaW5kKHVubGlzdChzZWwkZmVhdHVyZXMpLCB0ZXh0W3NlbF0pCmBgYAoKTGEgYW5vdGFjacOzbiBkZSBQT1Mgc2lndWUgbGEgbWlzbWEgbMOzZ2ljYS4gQWhvcmEgZ2VuZXJhcmVtb3MgdW4gbnVldm8gb2JqZXRvIGVuCmVsIHF1ZSBpbmljaWFsaXphbW9zIGVsIGFub3RhZG9yIGRlIFBPUyB5IGFwbGljYW1vcyBlc3RlIG51ZXZvIGFub3RhZG9yIGEKbnVlc3RybyB0ZXh0bzogCgpgYGB7cn0KcG9zX2FubiA8LSBNYXhlbnRfUE9TX1RhZ19Bbm5vdGF0b3IoKQpwb3NfYW5ub3RhdGVkX3RleHQgPC0gYW5ub3RhdGUodGV4dCwgcG9zX2FubiwgYW5ub3RhdGVkX3RleHQpCmBgYAoKQWhvcmEgbG8gcXVlIHRlbmVtb3MgZW4gbGEgY29sdW1uYSBkZSBgZmVhdHVyZXNgIGVzIHVuYSBpbmRpYWNpw7NuIGRlIGxhIFBPUyBkZQpjYWRhIHVuYSBkZSBsYXMgcGFsYWJyYXMKCmBgYHtyfQpoZWFkKHN1YnNldChwb3NfYW5ub3RhdGVkX3RleHQsIHR5cGU9PSJ3b3JkIikpCmBgYAoKRW4gZXN0ZSBjYXNvIHZlbW9zIHF1ZSwgcG9yIGVqZW1wbG8sIAoKYGBge3J9CnRleHRbc3Vic2V0KHBvc19hbm5vdGF0ZWRfdGV4dCwgdHlwZT09IndvcmQiKVs1NV1dCnN1YnNldChwb3NfYW5ub3RhdGVkX3RleHQsIHR5cGU9PSJ3b3JkIilbNTVdCmBgYAoKZXN0w6EgZXRpcXVldGFkbyBjb21vIGBOTlNgIHF1ZSBzZSBjb3JyZXNwb25kZSBjb21vIHN1c3RhbnRpdm8gcGx1cmFsIHkgCgpgYGB7cn0KdGV4dFtzdWJzZXQocG9zX2Fubm90YXRlZF90ZXh0LCB0eXBlPT0id29yZCIpWzEwMF1dCnN1YnNldChwb3NfYW5ub3RhdGVkX3RleHQsIHR5cGU9PSJ3b3JkIilbMTAwXQpgYGAKZXN0w6EgZXRpcXVldGFkbyBjb21vIGBKSmAgcXVlIGVzIHVuIGFkamV0aXZvLiBMYSBsaXN0YSBjb21wbGV0YSBkZQphYnJldmlhdHVyYXMgZXM6Cgo8dGFibGU+Cjx0cj4KPHRoPlPDrW1ib2xvPC90aD48dGg+U2lnbmlmaWNhZG88L3RoPgo8L3RyPgo8dHI+PHRkPkNDPC90ZD48dGQ+Q29vcmRpbmF0aW5nIGNvbmp1bmN0aW9uPC90ZD48L3RyPgo8dHI+PHRkPkNEPC90ZD48dGQ+IENhcmRpbmFsIG51bWJlciA8L3RkPjwvdHI+Cjx0cj48dGQ+RFQ8L3RkPjx0ZD4gRGV0ZXJtaW5lcjwvdGQ+PC90cj4KPHRyPjx0ZD5FWDwvdGQ+PHRkPiBFeGlzdGVudGlhbCB0aGVyZTwvdGQ+PC90cj4KPHRyPjx0ZD5GVzwvdGQ+PHRkPiBGb3JlaWduIHdvcmQ8L3RkPjwvdHI+Cjx0cj48dGQ+SU48L3RkPjx0ZD4gUHJlcG9zaXRpb24gb3Igc3Vib3JkaW5hdGluZyBjb25qdW5jdGlvbjwvdGQ+PC90cj4KPHRyPjx0ZD5KSjwvdGQ+PHRkPiBBZGplY3RpdmU8L3RkPjwvdHI+Cjx0cj48dGQ+SkpSPC90ZD48dGQ+IEFkamVjdGl2ZSwgY29tcGFyYXRpdmU8L3RkPjwvdHI+Cjx0cj48dGQ+SkpTPC90ZD48dGQ+IEFkamVjdGl2ZSwgc3VwZXJsYXRpdmU8L3RkPjwvdHI+Cjx0cj48dGQ+TFM8L3RkPjx0ZD4gTGlzdCBpdGVtIG1hcmtlcjwvdGQ+PC90cj4KPHRyPjx0ZD5NRDwvdGQ+PHRkPiBNb2RhbDwvdGQ+PC90cj4KPHRyPjx0ZD5OTjwvdGQ+PHRkPiBOb3VuLCBzaW5ndWxhciBvciBtYXNzPC90ZD48L3RyPgo8dHI+PHRkPk5OUzwvdGQ+PHRkPiBOb3VuLCBwbHVyYWw8L3RkPjwvdHI+Cjx0cj48dGQ+Tk5QPC90ZD48dGQ+IFByb3BlciBub3VuLCBzaW5ndWxhcjwvdGQ+PC90cj4KPHRyPjx0ZD5OTlBTPC90ZD48dGQ+IFByb3BlciBub3VuLCBwbHVyYWw8L3RkPjwvdHI+Cjx0cj48dGQ+UERUPC90ZD48dGQ+IFByZWRldGVybWluZXI8L3RkPjwvdHI+Cjx0cj48dGQ+UE9TPC90ZD48dGQ+IFBvc3Nlc3NpdmUgZW5kaW5nPC90ZD48L3RyPgo8dHI+PHRkPlBSUDwvdGQ+PHRkPiBQZXJzb25hbCBwcm9ub3VuPC90ZD48L3RyPgo8dHI+PHRkPlBSUFwkPC90ZD48dGQ+IFBvc3Nlc3NpdmUgcHJvbm91bjwvdGQ+PC90cj4KPHRyPjx0ZD5SQjwvdGQ+PHRkPiBBZHZlcmI8L3RkPjwvdHI+Cjx0cj48dGQ+UkJSPC90ZD48dGQ+IEFkdmVyYiwgY29tcGFyYXRpdmU8L3RkPjwvdHI+Cjx0cj48dGQ+UkJTPC90ZD48dGQ+IEFkdmVyYiwgc3VwZXJsYXRpdmU8L3RkPjwvdHI+Cjx0cj48dGQ+UlA8L3RkPjx0ZD4gUGFydGljbGU8L3RkPjwvdHI+Cjx0cj48dGQ+U1lNPC90ZD48dGQ+IFN5bWJvbDwvdGQ+PC90cj4KPHRyPjx0ZD5UTzwvdGQ+PHRkPiB0bzwvdGQ+PC90cj4KPHRyPjx0ZD5VSDwvdGQ+PHRkPiBJbnRlcmplY3Rpb248L3RkPjwvdHI+Cjx0cj48dGQ+VkI8L3RkPjx0ZD4gVmVyYiwgYmFzZSBmb3JtPC90ZD48L3RyPgo8dHI+PHRkPlZCRDwvdGQ+PHRkPiBWZXJiLCBwYXN0IHRlbnNlPC90ZD48L3RyPgo8dHI+PHRkPlZCRzwvdGQ+PHRkPiBWZXJiLCBnZXJ1bmQgb3IgcHJlc2VudCBwYXJ0aWNpcGxlPC90ZD48L3RyPgo8dHI+PHRkPlZCTjwvdGQ+PHRkPiBWZXJiLCBwYXN0IHBhcnRpY2lwbGU8L3RkPjwvdHI+Cjx0cj48dGQ+VkJQPC90ZD48dGQ+IFZlcmIsIG5vbsKtM3JkIHBlcnNvbiBzaW5ndWxhciBwcmVzZW50PC90ZD48L3RyPgo8dHI+PHRkPlZCWjwvdGQ+PHRkPiBWZXJiLCAzcmQgcGVyc29uIHNpbmd1bGFyIHByZXNlbnQ8L3RkPjwvdHI+Cjx0cj48dGQ+V0RUPC90ZD48dGQ+IFdowq1kZXRlcm1pbmVyPC90ZD48L3RyPgo8dHI+PHRkPldQPC90ZD48dGQ+IFdowq1wcm9ub3VuPC90ZD48L3RyPgo8dHI+PHRkPldQJDwvdGQ+PHRkPiBQb3NzZXNzaXZlIHdowq1wcm9ub3VuPC90ZD48L3RyPgo8dHI+PHRkPldSQjwvdGQ+PHRkPiBXaMKtYWR2ZXJiPC90ZD48L3RyPgo8L3RhYmxlPgoKSGVtb3MgdmlzdG8gcXVlIGxhcyBldGlxdWV0YWNpb25lcyBzb24gZW4gcmVhbGlkYWQgZWwgcmVzdWx0YWRvIGRlIHVuIG1vZGVsbwplc3RhZMOtc3RpY28uIFBvZGVtb3MgcGVkaXJsZSBhbCBhbm90YWRvciBxdWUgbm9zIGRldnVlbHZhIGxhcyBwcm9iYWJpbGlkYWRlcwphc29jaWFkYXMgYSBsYSBldGlxdWV0YSBlc2NvZ2lkYSBjb24gZWwgZmluIGRlIHRlbmVyIGluZm9ybWFjacOzbiBhY2VyY2EgZGUgbGEKaW5jZXJ0aWR1bWJyZS4gCgpgYGB7cn0KcG9zX2FubiA8LSBNYXhlbnRfUE9TX1RhZ19Bbm5vdGF0b3IocHJvYnM9VFJVRSkKcG9zX2Fubm90YXRlZF90ZXh0IDwtIGFubm90YXRlKHRleHQsIHBvc19hbm4sIGFubm90YXRlZF90ZXh0KQpoZWFkKHN1YnNldChwb3NfYW5ub3RhdGVkX3RleHQsIHR5cGU9PSJ3b3JkIikpCmBgYAoK