Paquetes necesarios

Empezamos por cargar a nuestro espacio de trabajo los paquetes que usaremos:

library(tm)
Cargando paquete requerido: NLP
library(SnowballC)
library(wordcloud)
Cargando paquete requerido: RColorBrewer
library(ggplot2)

Adjuntando el paquete: ‘ggplot2’

The following object is masked from ‘package:NLP’:

    annotate
library(dplyr)

Adjuntando el paquete: ‘dplyr’

The following objects are masked from ‘package:stats’:

    filter, lag

The following objects are masked from ‘package:base’:

    intersect, setdiff, setequal, union
library(readr)
library(cluster)

Nuestro texto: Niebla

El texto con el que trabajeremos es el texto del libro Niebla de Miguel de Unamuno que se puede descargar en Gutenberg.

Una vez hemos almacenado este documento a nuestro directorio de trabajo, procedemos a leerlo usando la función read_lines de readr.

Nuestro interés está en el contenido de este libro y no en los avisos legales de Gutenberg, prólogo y anotaciones, así que los omitiremos de la lectura. Empezaremos a leer el documento desde la línea 420, que es donde termina el prólogo, introducción e índice de Niebla, para ello nos saltaremos (skip) 419 líneas previas. De manera complementaria, detendremos la lectura en la línea 8313, que es donde inician los avisos legales de Gutenberg, por lo tanto leeremos un máximo (nmax) de 8313-419 líneas.

Realizamos estas operaciones y asignamos el resultado al objeto nov_raw.

nov_raw <- read_lines(file.choose(), skip = 419, n_max = 8313-419)

Preparación del texto

El objeto nov_raw que obtuvimos es uno de tipo character, con 7894 elementos.

str(nov_raw)
 chr [1:7894] "NIEBLA" "" "" "" "" "I" "" "" ...

Cada uno de estos elementos corresponde a un renglón de Niebla y tiene ancho máximo 70 caracteres, pues este es el ancho usado en los textos electrónicos de Gutenberg. Esta es una cantidad muy pequeña de texto para encontrar asociaciones entre palabras, por lo que necesitamos crear elementos con una mayor cantidad de caracteres en cada uno.

Por lo tanto, crearemos elementos del tamaño aproximado a un párrafo.

Creación de “párrafos”

Creamos un vector llamado diez con 10 repeticiones (rep) de los números desde 1 hasta el número de renglones en el documento, dividido entre 10 (length(nov_raw)/10.

Con esto, tendremos un vector con diez 1, luego diez 2, etc, hasta llegar al número máxico de grupos de diez posibles en función del número de renglones de nuestro documento.

Usaremos estos números para hacer grupos de diez renglones consecutivos.

diez <- rep(1:ceiling(length(nov_raw)/10), each = 10)

De este vector, nos quedamos con un número de elementos igual al número de renglones del objeto nov_raw (length(nov_raw)), para facilitar combinarlos.

diez <- diez[1:length(nov_raw)]

Combinamos diez con now_raw y los asignamos al objeto nov_text. Así tenemos una columna con los renglones de texto y otra con un número que identifica a qué grupo de diez renglones pertenece.

Además, convertimos a data.frame para que las columnas estén identificadas con un nombre, lo cual será útil en los siguientes pasos.

nov_text <- cbind(diez, nov_raw) %>% data.frame()

Como sólo necesitamos la columna con los ahora párrafos de texto, con eso nos quedamos. Aprovechamos para transformar nov_text en una matrix, pues esto nos facilitará los pasos siguientes.

nov_text <- nov_text %>% select(nov_raw) %>% as.matrix

dim(nov_text)
[1] 7894    1

Limpieza del texto

Necesitamos limpiar el texto de caracteres que son de poca utilidad en la mineria de textos.

Empezamos por aseguramos de que no queden caracteres especiales de la codificación, como saltos de línea y tabulaciones, con un poco de ayuda de Regular Expressions.

nov_text <- gsub("[[:cntrl:]]", " ", nov_text)

Convertimos todo a minúsculas.

nov_text <- tolower(nov_text)

Usamos removeWords con stopwords(“spanish”) para eliminar palabras vacias, es decir, aquellas con poco valor para el análisis, tales como algunas preposiciones y muletillas.

nov_text <- removeWords(nov_text, words = stopwords("spanish"))

Nos deshacemos de la puntuación, puesto que fin y fin. son identificadas como palabras diferentes, lo cual no deseamos.

nov_text <- removePunctuation(nov_text)

En este caso, removemos los números, pues en Niebla no hay fechas y otras cantidades que deseemos conservar.


<!-- rnb-source-begin eyJkYXRhIjoiYGBgclxubm92X3RleHQgPC0gcmVtb3ZlTnVtYmVycyhub3ZfdGV4dClcblxuYGBgIn0= -->

```r
nov_text <- removeNumbers(nov_text)

```

<!-- rnb-source-end -->
```r
nov_text <- removeNumbers(nov_text)

<!-- rnb-source-end -->


<!-- rnb-output-end -->

<!-- rnb-chunk-end -->


<!-- rnb-text-begin -->


Por último eliminamos los espacios vacios excesivos, muchos de ellos introducidos por las transformaciones anteriores.


<!-- rnb-text-end -->


<!-- rnb-chunk-begin -->


<!-- rnb-output-begin eyJkYXRhIjoiXG48IS0tIHJuYi1zb3VyY2UtYmVnaW4gZXlKa1lYUmhJam9pWUdCZ2NseHVibTkyWDNSbGVIUWdQQzBnYzNSeWFYQlhhR2wwWlhOd1lXTmxLRzV2ZGw5MFpYaDBLVnh1WEc1Z1lHQWlmUT09IC0tPlxuXG5gYGByXG5ub3ZfdGV4dCA8LSBzdHJpcFdoaXRlc3BhY2Uobm92X3RleHQpXG5cbmBgYFxuXG48IS0tIHJuYi1zb3VyY2UtZW5kIC0tPlxuIn0= -->


<!-- rnb-source-begin eyJkYXRhIjoiYGBgclxubm92X3RleHQgPC0gc3RyaXBXaGl0ZXNwYWNlKG5vdl90ZXh0KVxuXG5gYGAifQ== -->

```r
nov_text <- stripWhitespace(nov_text)

Análisis del Corpus

Con nuestro documento preparado, procedemos a crear nuestro Corpus, es decir, esto es nuestro acervo de documentos a analizar.

En nuestro caso, nuestro Corpus se compone de todos los parrafos del libro Niebla y los asignaremos al objeto nov_corpus usando las funciones VectorSource y Corpus.

nov_corpus <- Corpus(VectorSource(nov_text))

nov_corpus
<<SimpleCorpus>>
Metadata:  corpus specific: 1, document level (indexed): 0
Content:  documents: 7894

Como podemos ver, nustro Copus está compuesto por 7894 documentos. Los siguientes análisis se harán a partir de este Corpus.

# Crear la matriz TDM (Term Document Matrix)
nov_tdm <- TermDocumentMatrix(nov_corpus, control = list(
  wordLengths = c(1, Inf),
  removeNumbers = TRUE,
  removePunctuation = TRUE,
  stopwords = stopwords("spanish")
))

Nube de palabras

# Obtener las palabras y frecuencias
word_freqs <- sort(rowSums(as.matrix(nov_tdm)), decreasing = TRUE)
palabras <- names(word_freqs)
frecuencias <- word_freqs

# Crear la nube de palabras
wordcloud(palabras, frecuencias, max.words = 80, random.order = F, colors = brewer.pal(name = "Dark2", n = 8))

Podemos observar que en nuestro Corpus aun se encuentran palabras de poco interés para el análisis, tales como “usted”, “tal” y “sino”, lo cual nos indica que debemos realizar una segunda limpieza de nuestros textos.

Más depuración

Como observamos en las nubes de palabras que generamos, aún tenemos palabras que aparecen con mucha frecuencia en nuestro texto que en realidad no son de mucha utilidad para el análisis.

Usaremos la función removeWords indicando en el argumento words que palabras deseamos eliminar de nuestro Corpus.

Esta función requiere de un vector de caracteres como argumento principal, así que para esta operación usaremos nuestro objeto nov_text.

Una vez hecho esto, usaremos nuestra nueva versión de nov_text para generar nuestro Corpus y para mapearlo como documento de texto plano.

nov_text <- removeWords(nov_text, words = c("usted", "pues", "tal", "tan", "así", "dijo", "cómo", "sino", "entonces", "aunque", "don", "doña"))

# Crear un nuevo corpus con las palabras eliminadas
nov_corpus <- Corpus(VectorSource(nov_text))

# Generar la matriz TDM
nov_tdm <- TermDocumentMatrix(nov_corpus, control = list(
    wordLengths = c(1, Inf),
    removeNumbers = TRUE,
    removePunctuation = TRUE,
    stopwords = stopwords("spanish")
))

Generamos un nube de palabras nueva, en la cual es posible ver una diferencia significativa en su composición.

# Obtener las palabras y frecuencias
word_freqs <- sort(rowSums(as.matrix(nov_tdm)), decreasing = TRUE)
palabras <- names(word_freqs)
frecuencias <- word_freqs

# Crear la nube de palabras
wordcloud(palabras, frecuencias, max.words = 80, random.order = F, colors = brewer.pal(name = "Dark2", n = 8))

Una última depuración..

# Eliminar las palabras adicionales 
nov_text <- removeWords(nov_text, words = c("usted", "pues", "tal", "tan", "así", "dijo", "cómo", "sino", "entonces", "aunque", "don", "doña"))

# Eliminar caracteres "-" "?", "¿"
nov_text <- gsub("[\\-\\?\\¿\\¡\\—¡\\«\\»]", " ", nov_text) 

# Crear un nuevo corpus con las palabras eliminadas
nov_corpus <- Corpus(VectorSource(nov_text))

# Generar la matriz TDM
nov_tdm <- TermDocumentMatrix(nov_corpus, control = list(
    wordLengths = c(1, Inf),
    removeNumbers = TRUE,
    removePunctuation = TRUE,
    stopwords = stopwords("spanish")
))

# Obtener las palabras y frecuencias
word_freqs <- sort(rowSums(as.matrix(nov_tdm)), decreasing = TRUE)
palabras <- names(word_freqs)
frecuencias <- word_freqs

# Crear la nube de palabras
wordcloud(palabras, frecuencias, max.words = 80, random.order = F, colors = brewer.pal(name = "Dark2", n = 8))

Otra forma de graficarlo

library(wordcloud2)
Registered S3 method overwritten by 'htmlwidgets':
  method           from         
  print.htmlwidget tools:rstudio
##webshot::install_phantomjs()
d <- data.frame(word = palabras,freq=frecuencias)
wordcloud2(d,shape = "diamond",size = 0.4)

Análisis de sentimientos

library(tidytext)
library(textdata)
library(tidyr)
library(reshape2)

Adjuntando el paquete: ‘reshape2’

The following object is masked from ‘package:tidyr’:

    smiths

El paquete tidytext contiene 3 diccionarios distintos:

nrc<-get_sentiments("nrc")
Base_ana<-d %>% inner_join(nrc,"word") # Unimos el data de sentiemientos con nuestras palabras para que las clasifique según concordancia
Base_ana<-arrange(Base_ana,-freq) #ordenamos de forma descendente por frecuencia

Nube de palabras por sentimiento

b5<-data.frame(Base_ana %>% group_by(word,sentiment) %>% summarise(n=n())) %>% arrange(-n)
`summarise()` has grouped output by 'word'. You can override using the `.groups` argument.
b5 %>% acast(word~sentiment,fill = 0,value.var = "n") %>% #llenar los missing values con cero
  comparison.cloud(max.words = 500, #Maximo de palabras a representar
                   colors = brewer.pal(n = 10,"Paired"), #Cantidad de colores diferentes equivalente a cantidad de sentimientos
                   title.size = 1,title.colors = "black", #Color de titulos de sentimientos 
                   title.bg.colors = "white") #Resaltado de titulos

Top 10 por sentimiento

Base_ana %>% group_by(sentiment) %>% top_n(10, freq) %>% # Las 10 palabras con mayor frecuencia clasificadas por su respectivo sentimiento
  ungroup() %>%
  mutate(word=reorder(word,freq)) %>% 
  ggplot(aes(x=word,y=freq,fill=sentiment))+
  geom_col()+ # grafico de columnas
  facet_wrap(facets = ~sentiment,nrow = 3,scales = "free")+ #scales="free" para cada grafico de barra me ponga su propio eje x, es decir sus propias palabras
  coord_flip() + # De horizontal pasa a vertical
  theme_light() + theme(axis.text.y = element_text(size = 7), 
                          axis.text.x = element_text(size = 5)) + theme_bw()

Sentimientos predominantes

Base_ana %>%  group_by(sentiment) %>% summarise(n=n()) %>% 
  ungroup() %>% mutate(sentiment=reorder(sentiment,-n)) %>% 
  ggplot(aes(x=sentiment,y=n)) + geom_col(aes(fill=sentiment)) + 
  theme_classic() + labs(title="Cantidad de sentimientos encontrados",
                         y="Frecuencia",fill="Sentimiento",x="Sentimiento") + 
  theme(plot.title = element_text(face="bold",hjust = 0.5),
        legend.title = element_text(face="bold"))

LS0tDQp0aXRsZTogIk1pbmVyw61hIGRlIFRleHRvIg0Kb3V0cHV0Og0KICBodG1sX25vdGVib29rOiBkZWZhdWx0DQogIHBkZl9kb2N1bWVudDogZGVmYXVsdA0KICBodG1sX2RvY3VtZW50Og0KICAgIGRmX3ByaW50OiBwYWdlZA0KLS0tDQoNCiMgUGFxdWV0ZXMgbmVjZXNhcmlvcw0KRW1wZXphbW9zIHBvciBjYXJnYXIgYSBudWVzdHJvIGVzcGFjaW8gZGUgdHJhYmFqbyBsb3MgcGFxdWV0ZXMgcXVlIHVzYXJlbW9zOg0KDQotIHRtLCBlc3BlY8OtZmljbyBwYXJhIG1pbmVyw61hIGRlIHRleHRvcy4NCi0gd29yZGNsb3VkLCBwYXJhIGdyYWZpY2FyIG51YmVzIGRlIHBhbGFicmFzLg0KLSBnZ3Bsb3QyLCB1bmEgZ3JhbcOhdGljYSBkZSBncsOhZmljYXMgcXVlIGV4cGFuZGUgbGFzIGZ1bmNpb25lcyBiYXNlIGRlIFIuDQotIGRwbHlyLCBjb24gZnVuY2lvbmVzIGF1eGlsaWFyZXMgcGFyYSBtYW5pcHVsYXIgeSB0cmFuc2Zvcm1hciBkYXRvcy4gRW4gcGFydGljdWxhciwgZWwgb3BlcmFkb3IgJT4lIHBlcm1pdGUgZXNjcmliaXIgZnVuY2lvbmVzIG3DoXMgbGVnaWJsZXMgcGFyYSBzZXJlcyBodW1hbm9zLg0KLSByZWFkciwgZmFjaWxpdGFyw6EgbGVlciB5IGVzY3JpYmlyIGRvY3VtZW50b3MuDQoNCg0KYGBge3Igd2FybmluZz1GQUxTRX0NCmxpYnJhcnkodG0pDQpsaWJyYXJ5KFNub3diYWxsQykNCmxpYnJhcnkod29yZGNsb3VkKQ0KbGlicmFyeShnZ3Bsb3QyKQ0KbGlicmFyeShkcGx5cikNCmxpYnJhcnkocmVhZHIpDQpsaWJyYXJ5KGNsdXN0ZXIpDQpgYGANCg0KIyBOdWVzdHJvIHRleHRvOiBOaWVibGENCkVsIHRleHRvIGNvbiBlbCBxdWUgdHJhYmFqZXJlbW9zIGVzIGVsIHRleHRvIGRlbCBsaWJybyBOaWVibGEgZGUgTWlndWVsIGRlIFVuYW11bm8gcXVlIHNlIHB1ZWRlIGRlc2NhcmdhciBlbiBHdXRlbmJlcmcuDQoNClVuYSB2ZXogaGVtb3MgYWxtYWNlbmFkbyBlc3RlIGRvY3VtZW50byBhIG51ZXN0cm8gZGlyZWN0b3JpbyBkZSB0cmFiYWpvLCBwcm9jZWRlbW9zIGEgbGVlcmxvIHVzYW5kbyBsYSBmdW5jacOzbiByZWFkX2xpbmVzIGRlIHJlYWRyLg0KDQpOdWVzdHJvIGludGVyw6lzIGVzdMOhIGVuIGVsIGNvbnRlbmlkbyBkZSBlc3RlIGxpYnJvIHkgbm8gZW4gbG9zIGF2aXNvcyBsZWdhbGVzIGRlIEd1dGVuYmVyZywgcHLDs2xvZ28geSBhbm90YWNpb25lcywgYXPDrSBxdWUgbG9zIG9taXRpcmVtb3MgZGUgbGEgbGVjdHVyYS4gRW1wZXphcmVtb3MgYSBsZWVyIGVsIGRvY3VtZW50byBkZXNkZSBsYSBsw61uZWEgNDIwLCBxdWUgZXMgZG9uZGUgdGVybWluYSBlbCBwcsOzbG9nbywgaW50cm9kdWNjacOzbiBlIMOtbmRpY2UgZGUgTmllYmxhLCBwYXJhIGVsbG8gbm9zIHNhbHRhcmVtb3MgKHNraXApIDQxOSBsw61uZWFzIHByZXZpYXMuIERlIG1hbmVyYSBjb21wbGVtZW50YXJpYSwgZGV0ZW5kcmVtb3MgbGEgbGVjdHVyYSBlbiBsYSBsw61uZWEgODMxMywgcXVlIGVzIGRvbmRlIGluaWNpYW4gbG9zIGF2aXNvcyBsZWdhbGVzIGRlIEd1dGVuYmVyZywgcG9yIGxvIHRhbnRvIGxlZXJlbW9zIHVuIG3DoXhpbW8gKG5tYXgpIGRlIDgzMTMtNDE5IGzDrW5lYXMuDQoNClJlYWxpemFtb3MgZXN0YXMgb3BlcmFjaW9uZXMgeSBhc2lnbmFtb3MgZWwgcmVzdWx0YWRvIGFsIG9iamV0byBub3ZfcmF3Lg0KDQpgYGB7cn0NCm5vdl9yYXcgPC0gcmVhZF9saW5lcyhmaWxlLmNob29zZSgpLCBza2lwID0gNDE5LCBuX21heCA9IDgzMTMtNDE5KQ0KYGBgDQoNCiMgUHJlcGFyYWNpw7NuIGRlbCB0ZXh0bw0KRWwgb2JqZXRvIG5vdl9yYXcgcXVlIG9idHV2aW1vcyBlcyB1bm8gZGUgdGlwbyBjaGFyYWN0ZXIsIGNvbiA3ODk0IGVsZW1lbnRvcy4NCg0KYGBge3J9DQpzdHIobm92X3JhdykNCmBgYA0KDQpDYWRhIHVubyBkZSBlc3RvcyBlbGVtZW50b3MgY29ycmVzcG9uZGUgYSB1biByZW5nbMOzbiBkZSBOaWVibGEgeSB0aWVuZSBhbmNobyBtw6F4aW1vIDcwIGNhcmFjdGVyZXMsIHB1ZXMgZXN0ZSBlcyBlbCBhbmNobyB1c2FkbyBlbiBsb3MgdGV4dG9zIGVsZWN0csOzbmljb3MgZGUgR3V0ZW5iZXJnLiBFc3RhIGVzIHVuYSBjYW50aWRhZCBtdXkgcGVxdWXDsWEgZGUgdGV4dG8gcGFyYSBlbmNvbnRyYXIgYXNvY2lhY2lvbmVzIGVudHJlIHBhbGFicmFzLCBwb3IgbG8gcXVlIG5lY2VzaXRhbW9zIGNyZWFyIGVsZW1lbnRvcyBjb24gdW5hIG1heW9yIGNhbnRpZGFkIGRlIGNhcmFjdGVyZXMgZW4gY2FkYSB1bm8uDQoNClBvciBsbyB0YW50bywgY3JlYXJlbW9zIGVsZW1lbnRvcyBkZWwgdGFtYcOxbyBhcHJveGltYWRvIGEgdW4gcMOhcnJhZm8uDQoNCiMgQ3JlYWNpw7NuIGRlIOKAnHDDoXJyYWZvc+KAnQ0KQ3JlYW1vcyB1biB2ZWN0b3IgbGxhbWFkbyBkaWV6IGNvbiAxMCByZXBldGljaW9uZXMgKHJlcCkgZGUgbG9zIG7Dum1lcm9zIGRlc2RlIDEgaGFzdGEgZWwgbsO6bWVybyBkZSByZW5nbG9uZXMgZW4gZWwgZG9jdW1lbnRvLCBkaXZpZGlkbyBlbnRyZSAxMCAobGVuZ3RoKG5vdl9yYXcpLzEwLg0KDQpDb24gZXN0bywgdGVuZHJlbW9zIHVuIHZlY3RvciBjb24gZGlleiAxLCBsdWVnbyBkaWV6IDIsIGV0YywgaGFzdGEgbGxlZ2FyIGFsIG7Dum1lcm8gbcOheGljbyBkZSBncnVwb3MgZGUgZGlleiBwb3NpYmxlcyBlbiBmdW5jacOzbiBkZWwgbsO6bWVybyBkZSByZW5nbG9uZXMgZGUgbnVlc3RybyBkb2N1bWVudG8uDQoNClVzYXJlbW9zIGVzdG9zIG7Dum1lcm9zIHBhcmEgaGFjZXIgZ3J1cG9zIGRlIGRpZXogcmVuZ2xvbmVzIGNvbnNlY3V0aXZvcy4NCg0KYGBge3J9DQpkaWV6IDwtIHJlcCgxOmNlaWxpbmcobGVuZ3RoKG5vdl9yYXcpLzEwKSwgZWFjaCA9IDEwKQ0KYGBgDQoNCkRlIGVzdGUgdmVjdG9yLCBub3MgcXVlZGFtb3MgY29uIHVuIG7Dum1lcm8gZGUgZWxlbWVudG9zIGlndWFsIGFsIG7Dum1lcm8gZGUgcmVuZ2xvbmVzIGRlbCBvYmpldG8gbm92X3JhdyAobGVuZ3RoKG5vdl9yYXcpKSwgcGFyYSBmYWNpbGl0YXIgY29tYmluYXJsb3MuDQoNCmBgYHtyfQ0KZGlleiA8LSBkaWV6WzE6bGVuZ3RoKG5vdl9yYXcpXQ0KYGBgDQoNCkNvbWJpbmFtb3MgZGlleiBjb24gbm93X3JhdyB5IGxvcyBhc2lnbmFtb3MgYWwgb2JqZXRvIG5vdl90ZXh0LiBBc8OtIHRlbmVtb3MgdW5hIGNvbHVtbmEgY29uIGxvcyByZW5nbG9uZXMgZGUgdGV4dG8geSBvdHJhIGNvbiB1biBuw7ptZXJvIHF1ZSBpZGVudGlmaWNhIGEgcXXDqSBncnVwbyBkZSBkaWV6IHJlbmdsb25lcyBwZXJ0ZW5lY2UuDQoNCkFkZW3DoXMsIGNvbnZlcnRpbW9zIGEgZGF0YS5mcmFtZSBwYXJhIHF1ZSBsYXMgY29sdW1uYXMgZXN0w6luIGlkZW50aWZpY2FkYXMgY29uIHVuIG5vbWJyZSwgbG8gY3VhbCBzZXLDoSDDunRpbCBlbiBsb3Mgc2lndWllbnRlcyBwYXNvcy4NCg0KYGBge3J9DQpub3ZfdGV4dCA8LSBjYmluZChkaWV6LCBub3ZfcmF3KSAlPiUgZGF0YS5mcmFtZSgpDQpgYGANCg0KDQpDb21vIHPDs2xvIG5lY2VzaXRhbW9zIGxhIGNvbHVtbmEgY29uIGxvcyBhaG9yYSBww6FycmFmb3MgZGUgdGV4dG8sIGNvbiBlc28gbm9zIHF1ZWRhbW9zLiBBcHJvdmVjaGFtb3MgcGFyYSB0cmFuc2Zvcm1hciBub3ZfdGV4dCBlbiB1bmEgbWF0cml4LCBwdWVzIGVzdG8gbm9zIGZhY2lsaXRhcsOhIGxvcyBwYXNvcyBzaWd1aWVudGVzLg0KDQpgYGB7cn0NCm5vdl90ZXh0IDwtIG5vdl90ZXh0ICU+JSBzZWxlY3Qobm92X3JhdykgJT4lIGFzLm1hdHJpeA0KDQpkaW0obm92X3RleHQpDQpgYGANCg0KIyBMaW1waWV6YSBkZWwgdGV4dG8NCk5lY2VzaXRhbW9zIGxpbXBpYXIgZWwgdGV4dG8gZGUgY2FyYWN0ZXJlcyBxdWUgc29uIGRlIHBvY2EgdXRpbGlkYWQgZW4gbGEgbWluZXJpYSBkZSB0ZXh0b3MuDQoNCkVtcGV6YW1vcyBwb3IgYXNlZ3VyYW1vcyBkZSBxdWUgbm8gcXVlZGVuIGNhcmFjdGVyZXMgZXNwZWNpYWxlcyBkZSBsYSBjb2RpZmljYWNpw7NuLCBjb21vIHNhbHRvcyBkZSBsw61uZWEgeSB0YWJ1bGFjaW9uZXMsIGNvbiB1biBwb2NvIGRlIGF5dWRhIGRlIFJlZ3VsYXIgRXhwcmVzc2lvbnMuDQoNCmBgYHtyfQ0Kbm92X3RleHQgPC0gZ3N1YigiW1s6Y250cmw6XV0iLCAiICIsIG5vdl90ZXh0KQ0KYGBgDQoNCkNvbnZlcnRpbW9zIHRvZG8gYSBtaW7DunNjdWxhcy4NCg0KYGBge3J9DQpub3ZfdGV4dCA8LSB0b2xvd2VyKG5vdl90ZXh0KQ0KYGBgDQoNClVzYW1vcyByZW1vdmVXb3JkcyBjb24gc3RvcHdvcmRzKCJzcGFuaXNoIikgcGFyYSBlbGltaW5hciBwYWxhYnJhcyB2YWNpYXMsIGVzIGRlY2lyLCBhcXVlbGxhcyBjb24gcG9jbyB2YWxvciBwYXJhIGVsIGFuw6FsaXNpcywgdGFsZXMgY29tbyBhbGd1bmFzIHByZXBvc2ljaW9uZXMgeSBtdWxldGlsbGFzLg0KDQpgYGB7cn0NCm5vdl90ZXh0IDwtIHJlbW92ZVdvcmRzKG5vdl90ZXh0LCB3b3JkcyA9IHN0b3B3b3Jkcygic3BhbmlzaCIpKQ0KYGBgDQoNCk5vcyBkZXNoYWNlbW9zIGRlIGxhIHB1bnR1YWNpw7NuLCBwdWVzdG8gcXVlIGZpbiB5IGZpbi4gc29uIGlkZW50aWZpY2FkYXMgY29tbyBwYWxhYnJhcyBkaWZlcmVudGVzLCBsbyBjdWFsIG5vIGRlc2VhbW9zLg0KDQpgYGB7cn0NCm5vdl90ZXh0IDwtIHJlbW92ZVB1bmN0dWF0aW9uKG5vdl90ZXh0KQ0KYGBgDQoNCkVuIGVzdGUgY2FzbywgcmVtb3ZlbW9zIGxvcyBuw7ptZXJvcywgcHVlcyBlbiBOaWVibGEgbm8gaGF5IGZlY2hhcyB5IG90cmFzIGNhbnRpZGFkZXMgcXVlIGRlc2VlbW9zIGNvbnNlcnZhci4NCg0KYGBge3J9DQpub3ZfdGV4dCA8LSByZW1vdmVOdW1iZXJzKG5vdl90ZXh0KQ0KDQpgYGANCg0KUG9yIMO6bHRpbW8gZWxpbWluYW1vcyBsb3MgZXNwYWNpb3MgdmFjaW9zIGV4Y2VzaXZvcywgbXVjaG9zIGRlIGVsbG9zIGludHJvZHVjaWRvcyBwb3IgbGFzIHRyYW5zZm9ybWFjaW9uZXMgYW50ZXJpb3Jlcy4NCg0KYGBge3J9DQpub3ZfdGV4dCA8LSBzdHJpcFdoaXRlc3BhY2Uobm92X3RleHQpDQoNCmBgYA0KDQojIEFuw6FsaXNpcyBkZWwgQ29ycHVzDQpDb24gbnVlc3RybyBkb2N1bWVudG8gcHJlcGFyYWRvLCBwcm9jZWRlbW9zIGEgY3JlYXIgbnVlc3RybyBDb3JwdXMsIGVzIGRlY2lyLCBlc3RvIGVzIG51ZXN0cm8gYWNlcnZvIGRlIGRvY3VtZW50b3MgYSBhbmFsaXphci4NCg0KRW4gbnVlc3RybyBjYXNvLCBudWVzdHJvIENvcnB1cyBzZSBjb21wb25lIGRlIHRvZG9zIGxvcyBwYXJyYWZvcyBkZWwgbGlicm8gTmllYmxhIHkgbG9zIGFzaWduYXJlbW9zIGFsIG9iamV0byBub3ZfY29ycHVzIHVzYW5kbyBsYXMgZnVuY2lvbmVzIFZlY3RvclNvdXJjZSB5IENvcnB1cy4NCg0KYGBge3J9DQpub3ZfY29ycHVzIDwtIENvcnB1cyhWZWN0b3JTb3VyY2Uobm92X3RleHQpKQ0KDQpub3ZfY29ycHVzDQpgYGANCg0KQ29tbyBwb2RlbW9zIHZlciwgbnVzdHJvIENvcHVzIGVzdMOhIGNvbXB1ZXN0byBwb3IgNzg5NCBkb2N1bWVudG9zLiBMb3Mgc2lndWllbnRlcyBhbsOhbGlzaXMgc2UgaGFyw6FuIGEgcGFydGlyIGRlIGVzdGUgQ29ycHVzLg0KDQpgYGB7cn0NCiMgQ3JlYXIgbGEgbWF0cml6IFRETSAoVGVybSBEb2N1bWVudCBNYXRyaXgpDQpub3ZfdGRtIDwtIFRlcm1Eb2N1bWVudE1hdHJpeChub3ZfY29ycHVzLCBjb250cm9sID0gbGlzdCgNCiAgd29yZExlbmd0aHMgPSBjKDEsIEluZiksDQogIHJlbW92ZU51bWJlcnMgPSBUUlVFLA0KICByZW1vdmVQdW5jdHVhdGlvbiA9IFRSVUUsDQogIHN0b3B3b3JkcyA9IHN0b3B3b3Jkcygic3BhbmlzaCIpDQopKQ0KYGBgDQoNCg0KIyBOdWJlIGRlIHBhbGFicmFzDQoNCmBgYHtyfQ0KIyBPYnRlbmVyIGxhcyBwYWxhYnJhcyB5IGZyZWN1ZW5jaWFzDQp3b3JkX2ZyZXFzIDwtIHNvcnQocm93U3Vtcyhhcy5tYXRyaXgobm92X3RkbSkpLCBkZWNyZWFzaW5nID0gVFJVRSkNCnBhbGFicmFzIDwtIG5hbWVzKHdvcmRfZnJlcXMpDQpmcmVjdWVuY2lhcyA8LSB3b3JkX2ZyZXFzDQoNCiMgQ3JlYXIgbGEgbnViZSBkZSBwYWxhYnJhcw0Kd29yZGNsb3VkKHBhbGFicmFzLCBmcmVjdWVuY2lhcywgbWF4LndvcmRzID0gODAsIHJhbmRvbS5vcmRlciA9IEYsIGNvbG9ycyA9IGJyZXdlci5wYWwobmFtZSA9ICJEYXJrMiIsIG4gPSA4KSkNCmBgYA0KUG9kZW1vcyBvYnNlcnZhciBxdWUgZW4gbnVlc3RybyBDb3JwdXMgYXVuIHNlIGVuY3VlbnRyYW4gcGFsYWJyYXMgZGUgcG9jbyBpbnRlcsOpcyBwYXJhIGVsIGFuw6FsaXNpcywgdGFsZXMgY29tbyDigJx1c3RlZOKAnSwg4oCcdGFs4oCdIHkg4oCcc2lub+KAnSwgbG8gY3VhbCBub3MgaW5kaWNhIHF1ZSBkZWJlbW9zIHJlYWxpemFyIHVuYSBzZWd1bmRhIGxpbXBpZXphIGRlIG51ZXN0cm9zIHRleHRvcy4NCg0KDQojIE3DoXMgZGVwdXJhY2nDs24NCkNvbW8gb2JzZXJ2YW1vcyBlbiBsYXMgbnViZXMgZGUgcGFsYWJyYXMgcXVlIGdlbmVyYW1vcywgYcO6biB0ZW5lbW9zIHBhbGFicmFzIHF1ZSBhcGFyZWNlbiBjb24gbXVjaGEgZnJlY3VlbmNpYSBlbiBudWVzdHJvIHRleHRvIHF1ZSBlbiByZWFsaWRhZCBubyBzb24gZGUgbXVjaGEgdXRpbGlkYWQgcGFyYSBlbCBhbsOhbGlzaXMuDQoNClVzYXJlbW9zIGxhIGZ1bmNpw7NuIHJlbW92ZVdvcmRzIGluZGljYW5kbyBlbiBlbCBhcmd1bWVudG8gd29yZHMgcXVlIHBhbGFicmFzIGRlc2VhbW9zIGVsaW1pbmFyIGRlIG51ZXN0cm8gQ29ycHVzLg0KDQpFc3RhIGZ1bmNpw7NuIHJlcXVpZXJlIGRlIHVuIHZlY3RvciBkZSBjYXJhY3RlcmVzIGNvbW8gYXJndW1lbnRvIHByaW5jaXBhbCwgYXPDrSBxdWUgcGFyYSBlc3RhIG9wZXJhY2nDs24gdXNhcmVtb3MgbnVlc3RybyBvYmpldG8gbm92X3RleHQuDQoNClVuYSB2ZXogaGVjaG8gZXN0bywgdXNhcmVtb3MgbnVlc3RyYSBudWV2YSB2ZXJzacOzbiBkZSBub3ZfdGV4dCBwYXJhIGdlbmVyYXIgbnVlc3RybyBDb3JwdXMgeSBwYXJhIG1hcGVhcmxvIGNvbW8gZG9jdW1lbnRvIGRlIHRleHRvIHBsYW5vLg0KDQoNCmBgYHtyfQ0Kbm92X3RleHQgPC0gcmVtb3ZlV29yZHMobm92X3RleHQsIHdvcmRzID0gYygidXN0ZWQiLCAicHVlcyIsICJ0YWwiLCAidGFuIiwgImFzw60iLCAiZGlqbyIsICJjw7NtbyIsICJzaW5vIiwgImVudG9uY2VzIiwgImF1bnF1ZSIsICJkb24iLCAiZG/DsWEiKSkNCg0KIyBDcmVhciB1biBudWV2byBjb3JwdXMgY29uIGxhcyBwYWxhYnJhcyBlbGltaW5hZGFzDQpub3ZfY29ycHVzIDwtIENvcnB1cyhWZWN0b3JTb3VyY2Uobm92X3RleHQpKQ0KDQojIEdlbmVyYXIgbGEgbWF0cml6IFRETQ0Kbm92X3RkbSA8LSBUZXJtRG9jdW1lbnRNYXRyaXgobm92X2NvcnB1cywgY29udHJvbCA9IGxpc3QoDQogICAgd29yZExlbmd0aHMgPSBjKDEsIEluZiksDQogICAgcmVtb3ZlTnVtYmVycyA9IFRSVUUsDQogICAgcmVtb3ZlUHVuY3R1YXRpb24gPSBUUlVFLA0KICAgIHN0b3B3b3JkcyA9IHN0b3B3b3Jkcygic3BhbmlzaCIpDQopKQ0KYGBgDQpHZW5lcmFtb3MgdW4gbnViZSBkZSBwYWxhYnJhcyBudWV2YSwgZW4gbGEgY3VhbCBlcyBwb3NpYmxlIHZlciB1bmEgZGlmZXJlbmNpYSBzaWduaWZpY2F0aXZhIGVuIHN1IGNvbXBvc2ljacOzbi4NCg0KYGBge3J9DQojIE9idGVuZXIgbGFzIHBhbGFicmFzIHkgZnJlY3VlbmNpYXMNCndvcmRfZnJlcXMgPC0gc29ydChyb3dTdW1zKGFzLm1hdHJpeChub3ZfdGRtKSksIGRlY3JlYXNpbmcgPSBUUlVFKQ0KcGFsYWJyYXMgPC0gbmFtZXMod29yZF9mcmVxcykNCmZyZWN1ZW5jaWFzIDwtIHdvcmRfZnJlcXMNCg0KIyBDcmVhciBsYSBudWJlIGRlIHBhbGFicmFzDQp3b3JkY2xvdWQocGFsYWJyYXMsIGZyZWN1ZW5jaWFzLCBtYXgud29yZHMgPSA4MCwgcmFuZG9tLm9yZGVyID0gRiwgY29sb3JzID0gYnJld2VyLnBhbChuYW1lID0gIkRhcmsyIiwgbiA9IDgpKQ0KYGBgDQoNCiMgVW5hIMO6bHRpbWEgZGVwdXJhY2nDs24uLg0KDQpgYGB7cn0NCiMgRWxpbWluYXIgbGFzIHBhbGFicmFzIGFkaWNpb25hbGVzIA0Kbm92X3RleHQgPC0gcmVtb3ZlV29yZHMobm92X3RleHQsIHdvcmRzID0gYygidXN0ZWQiLCAicHVlcyIsICJ0YWwiLCAidGFuIiwgImFzw60iLCAiZGlqbyIsICJjw7NtbyIsICJzaW5vIiwgImVudG9uY2VzIiwgImF1bnF1ZSIsICJkb24iLCAiZG/DsWEiKSkNCg0KIyBFbGltaW5hciBjYXJhY3RlcmVzICItIiAiPyIsICLCvyINCm5vdl90ZXh0IDwtIGdzdWIoIltcXC1cXD9cXMK/XFzCoVxc4oCUwqFcXMKrXFzCu10iLCAiICIsIG5vdl90ZXh0KSANCg0KIyBDcmVhciB1biBudWV2byBjb3JwdXMgY29uIGxhcyBwYWxhYnJhcyBlbGltaW5hZGFzDQpub3ZfY29ycHVzIDwtIENvcnB1cyhWZWN0b3JTb3VyY2Uobm92X3RleHQpKQ0KDQojIEdlbmVyYXIgbGEgbWF0cml6IFRETQ0Kbm92X3RkbSA8LSBUZXJtRG9jdW1lbnRNYXRyaXgobm92X2NvcnB1cywgY29udHJvbCA9IGxpc3QoDQogICAgd29yZExlbmd0aHMgPSBjKDEsIEluZiksDQogICAgcmVtb3ZlTnVtYmVycyA9IFRSVUUsDQogICAgcmVtb3ZlUHVuY3R1YXRpb24gPSBUUlVFLA0KICAgIHN0b3B3b3JkcyA9IHN0b3B3b3Jkcygic3BhbmlzaCIpDQopKQ0KDQojIE9idGVuZXIgbGFzIHBhbGFicmFzIHkgZnJlY3VlbmNpYXMNCndvcmRfZnJlcXMgPC0gc29ydChyb3dTdW1zKGFzLm1hdHJpeChub3ZfdGRtKSksIGRlY3JlYXNpbmcgPSBUUlVFKQ0KcGFsYWJyYXMgPC0gbmFtZXMod29yZF9mcmVxcykNCmZyZWN1ZW5jaWFzIDwtIHdvcmRfZnJlcXMNCg0KIyBDcmVhciBsYSBudWJlIGRlIHBhbGFicmFzDQp3b3JkY2xvdWQocGFsYWJyYXMsIGZyZWN1ZW5jaWFzLCBtYXgud29yZHMgPSA4MCwgcmFuZG9tLm9yZGVyID0gRiwgY29sb3JzID0gYnJld2VyLnBhbChuYW1lID0gIkRhcmsyIiwgbiA9IDgpKQ0KYGBgDQoNCk90cmEgZm9ybWEgZGUgZ3JhZmljYXJsbw0KYGBge3Igd2FybmluZz1GQUxTRSAsZWNobz1UUlVFfQ0KbGlicmFyeSh3b3JkY2xvdWQyKQ0KIyN3ZWJzaG90OjppbnN0YWxsX3BoYW50b21qcygpDQpkIDwtIGRhdGEuZnJhbWUod29yZCA9IHBhbGFicmFzLGZyZXE9ZnJlY3VlbmNpYXMpDQp3b3JkY2xvdWQyKGQsc2hhcGUgPSAiZGlhbW9uZCIsc2l6ZSA9IDAuNCkNCmBgYA0KDQoNCg0KDQojIEFuw6FsaXNpcyBkZSBzZW50aW1pZW50b3MNCg0KYGBge3Igd2FybmluZz1GQUxTRX0NCmxpYnJhcnkodGlkeXRleHQpDQpsaWJyYXJ5KHRleHRkYXRhKQ0KbGlicmFyeSh0aWR5cikNCmxpYnJhcnkocmVzaGFwZTIpDQpgYGANCg0KRWwgcGFxdWV0ZSB0aWR5dGV4dCBjb250aWVuZSAzIGRpY2Npb25hcmlvcyBkaXN0aW50b3M6DQoNCi0gYWZmaW46IGFzaWduYSBhIGNhZGEgcGFsYWJyYSB1biB2YWxvciBlbnRyZSAtNSB5IDUuIFNpZW5kbyAtNSBlbCBtw6F4aW1vIGRlIG5lZ2F0aXZpZGFkIHkgKzUgZWwgbcOheGltbyBkZSBwb3NpdGl2aWRhZC4NCi0gYmluZzogY2xhc2lmaWNhIGxhcyBwYWxhYnJhcyBkZSBmb3JtYSBiaW5hcmlhIHBvc2l0aXZvL25lZ2F0aXZvLg0KLSBucmM6IGNsYXNpZmljYSBjYWRhIHBhbGFicmEgZW4gdW5vIG8gbcOhcyBkZSBsb3Mgc2lndWllbnRlcyBzZW50aW1pZW50b3M6IHBvc2l0aXZlLCBuZWdhdGl2ZSwgYW5nZXIsIGFudGljaXBhdGlvbiwgZGlzZ3VzdCwgZmVhciwgam95LCBzYWRuZXNzLCBzdXJwcmlzZSwgYW5kIHRydXN0Lg0KDQpgYGB7cn0NCm5yYzwtZ2V0X3NlbnRpbWVudHMoIm5yYyIpDQpCYXNlX2FuYTwtZCAlPiUgaW5uZXJfam9pbihucmMsIndvcmQiKSAjIFVuaW1vcyBlbCBkYXRhIGRlIHNlbnRpZW1pZW50b3MgY29uIG51ZXN0cmFzIHBhbGFicmFzIHBhcmEgcXVlIGxhcyBjbGFzaWZpcXVlIHNlZ8O6biBjb25jb3JkYW5jaWENCkJhc2VfYW5hPC1hcnJhbmdlKEJhc2VfYW5hLC1mcmVxKSAjb3JkZW5hbW9zIGRlIGZvcm1hIGRlc2NlbmRlbnRlIHBvciBmcmVjdWVuY2lhDQpgYGANCiMgTnViZSBkZSBwYWxhYnJhcyBwb3Igc2VudGltaWVudG8NCg0KYGBge3Igd2FybmluZz1GQUxTRX0NCmI1PC1kYXRhLmZyYW1lKEJhc2VfYW5hICU+JSBncm91cF9ieSh3b3JkLHNlbnRpbWVudCkgJT4lIHN1bW1hcmlzZShuPW4oKSkpICU+JSBhcnJhbmdlKC1uKQ0KDQpiNSAlPiUgYWNhc3Qod29yZH5zZW50aW1lbnQsZmlsbCA9IDAsdmFsdWUudmFyID0gIm4iKSAlPiUgI2xsZW5hciBsb3MgbWlzc2luZyB2YWx1ZXMgY29uIGNlcm8NCiAgY29tcGFyaXNvbi5jbG91ZChtYXgud29yZHMgPSA1MDAsICNNYXhpbW8gZGUgcGFsYWJyYXMgYSByZXByZXNlbnRhcg0KICAgICAgICAgICAgICAgICAgIGNvbG9ycyA9IGJyZXdlci5wYWwobiA9IDEwLCJQYWlyZWQiKSwgI0NhbnRpZGFkIGRlIGNvbG9yZXMgZGlmZXJlbnRlcyBlcXVpdmFsZW50ZSBhIGNhbnRpZGFkIGRlIHNlbnRpbWllbnRvcw0KICAgICAgICAgICAgICAgICAgIHRpdGxlLnNpemUgPSAxLHRpdGxlLmNvbG9ycyA9ICJibGFjayIsICNDb2xvciBkZSB0aXR1bG9zIGRlIHNlbnRpbWllbnRvcyANCiAgICAgICAgICAgICAgICAgICB0aXRsZS5iZy5jb2xvcnMgPSAid2hpdGUiKSAjUmVzYWx0YWRvIGRlIHRpdHVsb3MNCmBgYA0KDQpUb3AgMTAgcG9yIHNlbnRpbWllbnRvDQoNCmBgYHtyfQ0KQmFzZV9hbmEgJT4lIGdyb3VwX2J5KHNlbnRpbWVudCkgJT4lIHRvcF9uKDEwLCBmcmVxKSAlPiUgIyBMYXMgMTAgcGFsYWJyYXMgY29uIG1heW9yIGZyZWN1ZW5jaWEgY2xhc2lmaWNhZGFzIHBvciBzdSByZXNwZWN0aXZvIHNlbnRpbWllbnRvDQogIHVuZ3JvdXAoKSAlPiUNCiAgbXV0YXRlKHdvcmQ9cmVvcmRlcih3b3JkLGZyZXEpKSAlPiUgDQogIGdncGxvdChhZXMoeD13b3JkLHk9ZnJlcSxmaWxsPXNlbnRpbWVudCkpKw0KICBnZW9tX2NvbCgpKyAjIGdyYWZpY28gZGUgY29sdW1uYXMNCiAgZmFjZXRfd3JhcChmYWNldHMgPSB+c2VudGltZW50LG5yb3cgPSAzLHNjYWxlcyA9ICJmcmVlIikrICNzY2FsZXM9ImZyZWUiIHBhcmEgY2FkYSBncmFmaWNvIGRlIGJhcnJhIG1lIHBvbmdhIHN1IHByb3BpbyBlamUgeCwgZXMgZGVjaXIgc3VzIHByb3BpYXMgcGFsYWJyYXMNCiAgY29vcmRfZmxpcCgpICsgIyBEZSBob3Jpem9udGFsIHBhc2EgYSB2ZXJ0aWNhbA0KICB0aGVtZV9saWdodCgpICsgdGhlbWUoYXhpcy50ZXh0LnkgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDcpLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoc2l6ZSA9IDUpKSArIHRoZW1lX2J3KCkNCmBgYA0KU2VudGltaWVudG9zIHByZWRvbWluYW50ZXMNCmBgYHtyfQ0KQmFzZV9hbmEgJT4lICBncm91cF9ieShzZW50aW1lbnQpICU+JSBzdW1tYXJpc2Uobj1uKCkpICU+JSANCiAgdW5ncm91cCgpICU+JSBtdXRhdGUoc2VudGltZW50PXJlb3JkZXIoc2VudGltZW50LC1uKSkgJT4lIA0KICBnZ3Bsb3QoYWVzKHg9c2VudGltZW50LHk9bikpICsgZ2VvbV9jb2woYWVzKGZpbGw9c2VudGltZW50KSkgKyANCiAgdGhlbWVfY2xhc3NpYygpICsgbGFicyh0aXRsZT0iQ2FudGlkYWQgZGUgc2VudGltaWVudG9zIGVuY29udHJhZG9zIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICB5PSJGcmVjdWVuY2lhIixmaWxsPSJTZW50aW1pZW50byIseD0iU2VudGltaWVudG8iKSArIA0KICB0aGVtZShwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGZhY2U9ImJvbGQiLGhqdXN0ID0gMC41KSwNCiAgICAgICAgbGVnZW5kLnRpdGxlID0gZWxlbWVudF90ZXh0KGZhY2U9ImJvbGQiKSkNCg0KYGBgDQo=