El análisis de temas en minería de texto, como hemos visto, es un caso particular del aprendizaje no-supervisado en el que queremos agrupar documentos de acuerdo con la frecuencia de términos que contienen.

library(stringdist)
library(tm)
library(stringi)

Empezaremos leyendo el listado de archivos que contienen las notas de prensa. Recordemos que cada nota está en un documento de texto separado bajo una carpeta que lleva el nombre del senador.

allfiles <- list.files("./dta/senate-releases", full.names=TRUE, recursive=TRUE)
head(allfiles)

Usando expresiones regulares, podemos extraer del nombre del archivo toda la información que necesitamos sobre el autor, la fecha y, además, tener un identificador de cada documento.

extraer_metadatos <- function(x) {
    str <- ".*/([a-zA-Z]{1,})/([0-9]{1,2}[a-zA-Z]{3}[0-9]{4}).*([0-9]{1,3}).txt$"
    nombre <- gsub(str, "\\1", x)
    fecha <- gsub(str, "\\2", x)
    narchivo <- gsub(str, "\\3", x)
    return(data.frame("nombre"=nombre,
                      "fecha"=strptime(fecha, "%d%b%Y"),
                      "narchivo"=narchivo))
}

Tomaremos una muestra aleatoria simple de documentos para los siguientes análisis.

allfiles <- sample(allfiles, 1000) ## Sampling!
metadatos <- extraer_metadatos(allfiles)
head(metadatos)

Ahora podemos leer los documentos en nuestra sesión

alltexts <- lapply(allfiles, function(x) readLines(x, encoding="ISO-8859"))
head(alltexts[[1]])

Antes de poder trabajar con estos documentos, tenemos que procesarlos. Los pasos son bastante habituales en practicamente todas los analisis de minería de textos. Empezaremos por asegurarnos de que cada documento está contenido en un vector

alltexts <- lapply(alltexts, function(x) paste(x, collapse="\n"))
substring(alltexts[[1]], 1, 100)

Aunque los textos están en inglés, es posible que algunos términos, por ejemplo, nombres propios, contengan acentos. La mejor solución para nosotros es transliterar los documentos a ASCII.

alltexts <- lapply(alltexts, function(x) stri_trans_general(x, "latin-ascii"))

También convertiremos todas las palabras a minúscula:

alltexts <- lapply(alltexts, tolower)

y eliminaremos puntuación, dígitos, espacios, y símbolos de dolar.

alltexts <- lapply(alltexts, function(x) stri_replace_all_regex(x, "[[:punct:]]", " "))
alltexts <- lapply(alltexts, function(x) stri_replace_all_regex(x, "[[:digit:]]", " "))
alltexts <- lapply(alltexts, function(x) stri_replace_all_regex(x, "[[:space:]]", " "))
alltexts <- lapply(alltexts, function(x) stri_replace_all_regex(x, "\\$", ""))
substring(alltexts[[1]], 1, 100)

Para las operaciones de limpieza que nos quedan, es conveniente echar mano del paquete tm que implementa versiones eficientes de funciones como las que hemos usado pero para trabajar sobre documentos de texto.

docs <- Corpus(VectorSource(alltexts))

Empezaremos por quitar palabras vacías. tm contiene una lista que podemos usar para una primera pasada.

docs <- tm_map(docs, removeWords, stopwords("english"))

En muchas ocasiones, y dependiendo del tema de los documentos, querremos definir nuestra propia lista de palabras a eliminar. Esto lo podemos usar cambiando el nargumento que pasamos a removeWords con un vector de palabras:

otras_stopwords <- c("can", "say", "one", "way", "use", "also", "howev", "tell",
                     "will", "much", "need", "take", "tend", "even", "like",
                     "particular", "rather", "said", "get", "well", "make",
                     "ask", "come", "end", "first", "two", "help", "often",
                     "may", "might", "see", "someth", "thing", "point", "post",
                     "look", "right", "now", "think", "‘ve ", "‘re ", "anoth",
                     "put", "set", "new", "good", "want", "sure", "kind",
                     "larg", "yes, ", "day", "etc", "quit", "sinc", "attempt",
                     "lack", "seen", "awar", "littl", "ever", "moreov",
                     "though", "found", "abl", "enough", "far", "earli", "away",
                     "achiev", "draw", "last", "never", "brief", "bit", "entir",
                     "brief", "great", "lot")
docs <- tm_map(docs, removeWords, otras_stopwords)

Por último, eliminaremos los espacios en blanco sobrantes. Aunque es sencillo escribir una expresión regular, podemos utilizar en su lugar tm otra vez:

docs <- tm_map(docs, stripWhitespace)
docs[[21]]$content

Lo que nos interesa es cómo los términos se agrupan en documentos. Variaciones de los términos a través de sufijos son en este caso un problema. Por ejemplo, en textos referidos a educación, no queremos que nuestro modelo intente capturar diferencias entre educación, educativa, educado, … Nuestro intento de clasificar documentos funcionará mejor si reducimos todas estas palabras a una raíz común, como educ. Este proceso se denomina stemming y tm implementa varios algoritmos que funcionan bien en inglés.

docs <- tm_map(docs, stemDocument)
docs[[21]]$content

Esta colección de documentos podemos, ahora sí, usarla para nuestros análisis. Pero antes de eso, tenemos que convertirla a una matriz de términos.

dtm <- DocumentTermMatrix(docs,
                          control=list(wordLengths=c(2, Inf),
                                       sparse=TRUE))
inspect(dtm[1:5, 1:5])

En esta matriz hemos eliminado palabras muy cortas, de menos de dos caracteres. Además, también queremos eliminar palabras muy poco frecuentes (palabras que son sparse en un 95% de los documentos o, dicho de otra forma, que solo aparezcan en un 5% de documentos). Con eso conseguiremos que la matriz no sea tan sparse.

dtm <- removeSparseTerms(dtm , 0.95)
findFreqTerms(dtm, 1000) ## palabras que aparecen 1000 veces

Con el fin de poder referirnos a los documentos que originan cada término, pondremos los nombres de los archivos como nombres de las filas de la base de datos.

rownames(dtm) <- gsub(".*/([0-9]*.*).txt$", "\\1", allfiles)
saveRDS(dtm, "./dta/dtm.RDS")
LS0tIAp0aXRsZTogIkFuw6FsaXNpcyBkZSB0ZW1hczogUHJlcGFyYWNpw7NuIGRlIGxvcyBkYXRvcyIKZGF0ZTogImByIGZvcm1hdChTeXMudGltZSgpLCAnJUIgJWQsICVZJylgIgpvdXRwdXQ6CiAgbWRfZG9jdW1lbnQKLS0tCgpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRSwgY2FjaGU9RkFMU0V9CmtuaXRyOjpvcHRzX2NodW5rJHNldChldmFsID0gRkFMU0UpIApgYGAKCkVsIGFuw6FsaXNpcyBkZSB0ZW1hcyBlbiBtaW5lcsOtYSBkZSB0ZXh0bywgY29tbyBoZW1vcyB2aXN0bywgZXMgdW4gY2FzbwpwYXJ0aWN1bGFyIGRlbCBhcHJlbmRpemFqZSBuby1zdXBlcnZpc2FkbyBlbiBlbCBxdWUgcXVlcmVtb3MgYWdydXBhciBkb2N1bWVudG9zCmRlIGFjdWVyZG8gY29uIGxhIGZyZWN1ZW5jaWEgZGUgdMOpcm1pbm9zIHF1ZSBjb250aWVuZW4uCgpgYGB7cn0KbGlicmFyeShzdHJpbmdkaXN0KQpsaWJyYXJ5KHRtKQpsaWJyYXJ5KHN0cmluZ2kpCmBgYAoKRW1wZXphcmVtb3MgbGV5ZW5kbyBlbCBsaXN0YWRvIGRlIGFyY2hpdm9zIHF1ZSBjb250aWVuZW4gbGFzIG5vdGFzIGRlIHByZW5zYS4KUmVjb3JkZW1vcyBxdWUgY2FkYSBub3RhIGVzdMOhIGVuIHVuIGRvY3VtZW50byBkZSB0ZXh0byBzZXBhcmFkbyBiYWpvIHVuYSBjYXJwZXRhCnF1ZSBsbGV2YSBlbCBub21icmUgZGVsIHNlbmFkb3IuIAoKYGBge3J9CmFsbGZpbGVzIDwtIGxpc3QuZmlsZXMoIi4vZHRhL3NlbmF0ZS1yZWxlYXNlcyIsIGZ1bGwubmFtZXM9VFJVRSwgcmVjdXJzaXZlPVRSVUUpCmhlYWQoYWxsZmlsZXMpCmBgYAoKVXNhbmRvIGV4cHJlc2lvbmVzIHJlZ3VsYXJlcywgcG9kZW1vcyBleHRyYWVyIGRlbCBub21icmUgZGVsIGFyY2hpdm8gdG9kYSBsYQppbmZvcm1hY2nDs24gcXVlIG5lY2VzaXRhbW9zIHNvYnJlIGVsIGF1dG9yLCBsYSBmZWNoYSB5LCBhZGVtw6FzLCB0ZW5lciB1bgppZGVudGlmaWNhZG9yIGRlIGNhZGEgZG9jdW1lbnRvLiAKYGBge3J9CmV4dHJhZXJfbWV0YWRhdG9zIDwtIGZ1bmN0aW9uKHgpIHsKICAgIHN0ciA8LSAiLiovKFthLXpBLVpdezEsfSkvKFswLTldezEsMn1bYS16QS1aXXszfVswLTldezR9KS4qKFswLTldezEsM30pLnR4dCQiCiAgICBub21icmUgPC0gZ3N1YihzdHIsICJcXDEiLCB4KQogICAgZmVjaGEgPC0gZ3N1YihzdHIsICJcXDIiLCB4KQogICAgbmFyY2hpdm8gPC0gZ3N1YihzdHIsICJcXDMiLCB4KQogICAgcmV0dXJuKGRhdGEuZnJhbWUoIm5vbWJyZSI9bm9tYnJlLAogICAgICAgICAgICAgICAgICAgICAgImZlY2hhIj1zdHJwdGltZShmZWNoYSwgIiVkJWIlWSIpLAogICAgICAgICAgICAgICAgICAgICAgIm5hcmNoaXZvIj1uYXJjaGl2bykpCn0KYGBgCgpUb21hcmVtb3MgdW5hIG11ZXN0cmEgYWxlYXRvcmlhIHNpbXBsZSBkZSBkb2N1bWVudG9zIHBhcmEgbG9zIHNpZ3VpZW50ZXMKYW7DoWxpc2lzLiAKCmBgYHtyfQphbGxmaWxlcyA8LSBzYW1wbGUoYWxsZmlsZXMsIDEwMDApICMjIFNhbXBsaW5nIQptZXRhZGF0b3MgPC0gZXh0cmFlcl9tZXRhZGF0b3MoYWxsZmlsZXMpCmhlYWQobWV0YWRhdG9zKQpgYGAKCkFob3JhIHBvZGVtb3MgbGVlciBsb3MgZG9jdW1lbnRvcyBlbiBudWVzdHJhIHNlc2nDs24KYGBge3J9CmFsbHRleHRzIDwtIGxhcHBseShhbGxmaWxlcywgZnVuY3Rpb24oeCkgcmVhZExpbmVzKHgsIGVuY29kaW5nPSJJU08tODg1OSIpKQpoZWFkKGFsbHRleHRzW1sxXV0pCmBgYAoKQW50ZXMgZGUgcG9kZXIgdHJhYmFqYXIgY29uIGVzdG9zIGRvY3VtZW50b3MsIHRlbmVtb3MgcXVlIHByb2Nlc2FybG9zLiBMb3MgcGFzb3MKc29uIGJhc3RhbnRlIGhhYml0dWFsZXMgZW4gcHJhY3RpY2FtZW50ZSB0b2RhcyBsb3MgYW5hbGlzaXMgZGUgbWluZXLDrWEgZGUKdGV4dG9zLiBFbXBlemFyZW1vcyBwb3IgYXNlZ3VyYXJub3MgZGUgcXVlIGNhZGEgZG9jdW1lbnRvIGVzdMOhIGNvbnRlbmlkbyBlbiB1biB2ZWN0b3IKCmBgYHtyfQphbGx0ZXh0cyA8LSBsYXBwbHkoYWxsdGV4dHMsIGZ1bmN0aW9uKHgpIHBhc3RlKHgsIGNvbGxhcHNlPSJcbiIpKQpzdWJzdHJpbmcoYWxsdGV4dHNbWzFdXSwgMSwgMTAwKQpgYGAKCkF1bnF1ZSBsb3MgdGV4dG9zIGVzdMOhbiBlbiBpbmdsw6lzLCBlcyBwb3NpYmxlIHF1ZSBhbGd1bm9zIHTDqXJtaW5vcywgcG9yIGVqZW1wbG8sCm5vbWJyZXMgcHJvcGlvcywgY29udGVuZ2FuIGFjZW50b3MuIExhIG1lam9yIHNvbHVjacOzbiBwYXJhIG5vc290cm9zIGVzCnRyYW5zbGl0ZXJhciBsb3MgZG9jdW1lbnRvcyBhIEFTQ0lJLgoKYGBge3J9CmFsbHRleHRzIDwtIGxhcHBseShhbGx0ZXh0cywgZnVuY3Rpb24oeCkgc3RyaV90cmFuc19nZW5lcmFsKHgsICJsYXRpbi1hc2NpaSIpKQpgYGAKClRhbWJpw6luIGNvbnZlcnRpcmVtb3MgdG9kYXMgbGFzIHBhbGFicmFzIGEgbWluw7pzY3VsYToKCmBgYHtyfQphbGx0ZXh0cyA8LSBsYXBwbHkoYWxsdGV4dHMsIHRvbG93ZXIpCmBgYAoKeSBlbGltaW5hcmVtb3MgcHVudHVhY2nDs24sIGTDrWdpdG9zLCBlc3BhY2lvcywgeSBzw61tYm9sb3MgZGUgZG9sYXIuIAoKYGBge3J9CmFsbHRleHRzIDwtIGxhcHBseShhbGx0ZXh0cywgZnVuY3Rpb24oeCkgc3RyaV9yZXBsYWNlX2FsbF9yZWdleCh4LCAiW1s6cHVuY3Q6XV0iLCAiICIpKQphbGx0ZXh0cyA8LSBsYXBwbHkoYWxsdGV4dHMsIGZ1bmN0aW9uKHgpIHN0cmlfcmVwbGFjZV9hbGxfcmVnZXgoeCwgIltbOmRpZ2l0Ol1dIiwgIiAiKSkKYWxsdGV4dHMgPC0gbGFwcGx5KGFsbHRleHRzLCBmdW5jdGlvbih4KSBzdHJpX3JlcGxhY2VfYWxsX3JlZ2V4KHgsICJbWzpzcGFjZTpdXSIsICIgIikpCmFsbHRleHRzIDwtIGxhcHBseShhbGx0ZXh0cywgZnVuY3Rpb24oeCkgc3RyaV9yZXBsYWNlX2FsbF9yZWdleCh4LCAiXFwkIiwgIiIpKQpzdWJzdHJpbmcoYWxsdGV4dHNbWzFdXSwgMSwgMTAwKQpgYGAKClBhcmEgbGFzIG9wZXJhY2lvbmVzIGRlIGxpbXBpZXphIHF1ZSBub3MgcXVlZGFuLCBlcyBjb252ZW5pZW50ZSBlY2hhciBtYW5vIGRlbApwYXF1ZXRlIGB0bWAgcXVlIGltcGxlbWVudGEgdmVyc2lvbmVzIGVmaWNpZW50ZXMgZGUgZnVuY2lvbmVzIGNvbW8gbGFzIHF1ZSBoZW1vcwp1c2FkbyBwZXJvIHBhcmEgdHJhYmFqYXIgc29icmUgZG9jdW1lbnRvcyBkZSB0ZXh0by4gCgpgYGB7cn0KZG9jcyA8LSBDb3JwdXMoVmVjdG9yU291cmNlKGFsbHRleHRzKSkKYGBgCkVtcGV6YXJlbW9zIHBvciBxdWl0YXIgcGFsYWJyYXMgdmFjw61hcy4gYHRtYCBjb250aWVuZSB1bmEgbGlzdGEgcXVlIHBvZGVtb3MgdXNhcgpwYXJhIHVuYSBwcmltZXJhIHBhc2FkYS4gCiAKYGBge3J9CmRvY3MgPC0gdG1fbWFwKGRvY3MsIHJlbW92ZVdvcmRzLCBzdG9wd29yZHMoImVuZ2xpc2giKSkKYGBgCgpFbiBtdWNoYXMgb2Nhc2lvbmVzLCB5IGRlcGVuZGllbmRvIGRlbCB0ZW1hIGRlIGxvcyBkb2N1bWVudG9zLCBxdWVycmVtb3MgZGVmaW5pcgpudWVzdHJhIHByb3BpYSBsaXN0YSBkZSBwYWxhYnJhcyBhIGVsaW1pbmFyLiBFc3RvIGxvIHBvZGVtb3MgdXNhciBjYW1iaWFuZG8gZWwKbmFyZ3VtZW50byBxdWUgcGFzYW1vcyBhIGByZW1vdmVXb3Jkc2AgY29uIHVuIHZlY3RvciBkZSBwYWxhYnJhczoKCmBgYHtyfQpvdHJhc19zdG9wd29yZHMgPC0gYygiY2FuIiwgInNheSIsICJvbmUiLCAid2F5IiwgInVzZSIsICJhbHNvIiwgImhvd2V2IiwgInRlbGwiLAogICAgICAgICAgICAgICAgICAgICAid2lsbCIsICJtdWNoIiwgIm5lZWQiLCAidGFrZSIsICJ0ZW5kIiwgImV2ZW4iLCAibGlrZSIsCiAgICAgICAgICAgICAgICAgICAgICJwYXJ0aWN1bGFyIiwgInJhdGhlciIsICJzYWlkIiwgImdldCIsICJ3ZWxsIiwgIm1ha2UiLAogICAgICAgICAgICAgICAgICAgICAiYXNrIiwgImNvbWUiLCAiZW5kIiwgImZpcnN0IiwgInR3byIsICJoZWxwIiwgIm9mdGVuIiwKICAgICAgICAgICAgICAgICAgICAgIm1heSIsICJtaWdodCIsICJzZWUiLCAic29tZXRoIiwgInRoaW5nIiwgInBvaW50IiwgInBvc3QiLAogICAgICAgICAgICAgICAgICAgICAibG9vayIsICJyaWdodCIsICJub3ciLCAidGhpbmsiLCAi4oCYdmUgIiwgIuKAmHJlICIsICJhbm90aCIsCiAgICAgICAgICAgICAgICAgICAgICJwdXQiLCAic2V0IiwgIm5ldyIsICJnb29kIiwgIndhbnQiLCAic3VyZSIsICJraW5kIiwKICAgICAgICAgICAgICAgICAgICAgImxhcmciLCAieWVzLCAiLCAiZGF5IiwgImV0YyIsICJxdWl0IiwgInNpbmMiLCAiYXR0ZW1wdCIsCiAgICAgICAgICAgICAgICAgICAgICJsYWNrIiwgInNlZW4iLCAiYXdhciIsICJsaXR0bCIsICJldmVyIiwgIm1vcmVvdiIsCiAgICAgICAgICAgICAgICAgICAgICJ0aG91Z2giLCAiZm91bmQiLCAiYWJsIiwgImVub3VnaCIsICJmYXIiLCAiZWFybGkiLCAiYXdheSIsCiAgICAgICAgICAgICAgICAgICAgICJhY2hpZXYiLCAiZHJhdyIsICJsYXN0IiwgIm5ldmVyIiwgImJyaWVmIiwgImJpdCIsICJlbnRpciIsCiAgICAgICAgICAgICAgICAgICAgICJicmllZiIsICJncmVhdCIsICJsb3QiKQpkb2NzIDwtIHRtX21hcChkb2NzLCByZW1vdmVXb3Jkcywgb3RyYXNfc3RvcHdvcmRzKQpgYGAKClBvciDDumx0aW1vLCBlbGltaW5hcmVtb3MgbG9zIGVzcGFjaW9zIGVuIGJsYW5jbyBzb2JyYW50ZXMuIEF1bnF1ZSBlcyBzZW5jaWxsbwplc2NyaWJpciB1bmEgZXhwcmVzacOzbiByZWd1bGFyLCBwb2RlbW9zIHV0aWxpemFyIGVuIHN1IGx1Z2FyIGB0bWAgb3RyYSB2ZXo6CgpgYGB7cn0KZG9jcyA8LSB0bV9tYXAoZG9jcywgc3RyaXBXaGl0ZXNwYWNlKQpkb2NzW1syMV1dJGNvbnRlbnQKYGBgCgpMbyBxdWUgbm9zIGludGVyZXNhIGVzIGPDs21vIGxvcyB0w6lybWlub3Mgc2UgYWdydXBhbiBlbiBkb2N1bWVudG9zLiBWYXJpYWNpb25lcwpkZSBsb3MgdMOpcm1pbm9zIGEgdHJhdsOpcyBkZSBzdWZpam9zIHNvbiBlbiBlc3RlIGNhc28gdW4gcHJvYmxlbWEuIFBvciBlamVtcGxvLAplbiB0ZXh0b3MgcmVmZXJpZG9zIGEgZWR1Y2FjacOzbiwgbm8gcXVlcmVtb3MgcXVlIG51ZXN0cm8gbW9kZWxvIGludGVudGUgY2FwdHVyYXIKZGlmZXJlbmNpYXMgZW50cmUgX2VkdWNhY2nDs25fLCBfZWR1Y2F0aXZhXywgX2VkdWNhZG9fLCAuLi4gTnVlc3RybyBpbnRlbnRvIGRlCmNsYXNpZmljYXIgZG9jdW1lbnRvcyBmdW5jaW9uYXLDoSBtZWpvciBzaSByZWR1Y2ltb3MgdG9kYXMgZXN0YXMgcGFsYWJyYXMgYSB1bmEKcmHDrXogY29tw7puLCBjb21vIF9lZHVjXy4gRXN0ZSBwcm9jZXNvIHNlIGRlbm9taW5hIF9zdGVtbWluZ18geSBgdG1gIGltcGxlbWVudGEKdmFyaW9zIGFsZ29yaXRtb3MgcXVlIGZ1bmNpb25hbiBiaWVuIGVuIGluZ2zDqXMuIAoKYGBge3J9CmRvY3MgPC0gdG1fbWFwKGRvY3MsIHN0ZW1Eb2N1bWVudCkKZG9jc1tbMjFdXSRjb250ZW50CmBgYAoKRXN0YSBjb2xlY2Npw7NuIGRlIGRvY3VtZW50b3MgcG9kZW1vcywgYWhvcmEgc8OtLCB1c2FybGEgcGFyYSBudWVzdHJvcyBhbsOhbGlzaXMuClBlcm8gYW50ZXMgZGUgZXNvLCB0ZW5lbW9zIHF1ZSBjb252ZXJ0aXJsYSBhIHVuYSBtYXRyaXogZGUgdMOpcm1pbm9zLiAKCmBgYHtyfQpkdG0gPC0gRG9jdW1lbnRUZXJtTWF0cml4KGRvY3MsCiAgICAgICAgICAgICAgICAgICAgICAgICAgY29udHJvbD1saXN0KHdvcmRMZW5ndGhzPWMoMiwgSW5mKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3BhcnNlPVRSVUUpKQppbnNwZWN0KGR0bVsxOjUsIDE6NV0pCmBgYAoKRW4gZXN0YSBtYXRyaXogaGVtb3MgZWxpbWluYWRvIHBhbGFicmFzIG11eSBjb3J0YXMsIGRlIG1lbm9zIGRlIGRvcyBjYXJhY3RlcmVzLgpBZGVtw6FzLCB0YW1iacOpbiBxdWVyZW1vcyBlbGltaW5hciBwYWxhYnJhcyBtdXkgcG9jbyBmcmVjdWVudGVzIChwYWxhYnJhcyBxdWUgc29uCl9zcGFyc2VfIGVuIHVuIDk1XCUgZGUgbG9zIGRvY3VtZW50b3MgbywgZGljaG8gZGUgb3RyYSBmb3JtYSwgcXVlIHNvbG8gYXBhcmV6Y2FuCmVuIHVuIDVcJSBkZSBkb2N1bWVudG9zKS4gQ29uIGVzbyBjb25zZWd1aXJlbW9zIHF1ZSBsYSBtYXRyaXogbm8gc2VhIHRhbiBfc3BhcnNlXy4KCmBgYHtyfQpkdG0gPC0gcmVtb3ZlU3BhcnNlVGVybXMoZHRtICwgMC45NSkKZmluZEZyZXFUZXJtcyhkdG0sIDEwMDApICMjIHBhbGFicmFzIHF1ZSBhcGFyZWNlbiAxMDAwIHZlY2VzCmBgYAoKQ29uIGVsIGZpbiBkZSBwb2RlciByZWZlcmlybm9zIGEgbG9zIGRvY3VtZW50b3MgcXVlIG9yaWdpbmFuIGNhZGEgdMOpcm1pbm8sCnBvbmRyZW1vcyBsb3Mgbm9tYnJlcyBkZSBsb3MgYXJjaGl2b3MgY29tbyBub21icmVzIGRlIGxhcyBmaWxhcyBkZSBsYSBiYXNlIGRlCmRhdG9zLiAKCmBgYHtyfQpyb3duYW1lcyhkdG0pIDwtIGdzdWIoIi4qLyhbMC05XSouKikudHh0JCIsICJcXDEiLCBhbGxmaWxlcykKc2F2ZVJEUyhkdG0sICIuL2R0YS9kdG0uUkRTIikKYGBgCg==