# install.packages("rtweet")
library(rtweet)
library(tidyverse)
library(tm)
library(wordcloud2)
library(topicmodels)
library(LDAvis)
library(tsne)

Descargas de tweets con rtweet

rt <- search_tweets(q = "Bolivia OR bolivia OR Evo",type = "mixed", n = 18000, include_rts = FALSE)

saveRDS(rt,'fuentes/rt.RDS')
rt <- read_rds('fuentes/rt.RDS')
rt %>% 
  sample_n(10)
range(rt$created_at)
[1] "2019-11-19 18:22:11 UTC" "2019-11-20 22:30:30 UTC"

Nos da los tweets de los últimos nueve días, o el máximo que indicamos más reciente

rt %>%
  ts_plot(by = "minutes") +
  ggplot2::theme_minimal() +
  ggplot2::theme(plot.title = ggplot2::element_text(face = "bold")) +
  ggplot2::labs(
    x = NULL, y = NULL,
    title = "Frecuencia de los tweets relacionados NLP",
    subtitle = "Agregado a intervalos de tres horas")+
  scale_x_datetime(limits = c((max(rt$created_at)- 12800),max(rt$created_at)))

  • Me quedo con el texto
texto <-  rt$text 

texto[1:10]
 [1] "Más represión y muertes en Bolivia gracias al Golpe de Estado. Esta vez en El Alto. La Comunidad Internacional debe exigirle a los gobernantes de facto y a las fuerzas militares y policiales bolivianas el cese inmediato de la represión y la flagrante violación de Derechos Humanos"                         
 [2] "“Nos están matando como un perro... dónde está la prensa de aquí? Relato de una masacre. Bolivia. 19 Nov de 2019 https://t.co/puZnyFT2L5"                                                                                                                                                                         
 [3] "https://t.co/mlPNjP2mR9"                                                                                                                                                                                                                                                                                          
 [4] "Todo o socialista quer implantar este nefasto sistema em seus países.\nMas ir viver onde ele já foi implementado, como na Venezuela, ninguém quer, nem o Evo Morales @evoespueblo \n\nOu será que o Evo não foi para lá porque sabe que @NicolasMaduro está a ponto de cair? https://t.co/ahN38AfT3h"             
 [5] "@JulianMaciasT @suarezluis @suarezluis amigo !!! Este tipo te hizo famoso 😂😂😂 que manera de desinformar a la gente !!\n\n#BoliviaLibre #BoliviaUnida #EvoEsFraude #Bolivia #BoliviaNoHayGolpe #EvoDictador #NoFueGolpeFueFraude #EvoAsesino #EvoMentiroso"                                                        
 [6] "@magne_caro @luismebx @MashiRafael @evoespueblo #BoliviaLibre #BoliviaUnida #EvoEsFraude #Bolivia #BoliviaNoHayGolpe #EvoDictador #NoFueGolpeFueFraude #EvoAsesino #EvoMentiroso https://t.co/KKAQ22ERqt"                                                                                                         
 [7] "Los afines al Dictador Evo Morales son #terroristas, aplaudo a las FFAA y la Policía por defender la seguridad de los Bolivianos 🇧🇴\n\n#BoliviaLibre #BoliviaUnida #EvoEsFraude #Bolivia #BoliviaNoHayGolpe #EvoDictador #NoFueGolpeFueFraude #EvoAsesino #EvoMentiroso https://t.co/fHJpaq5FKZ"                  
 [8] "@F_ortegazabala Las verdades duelen 🤷🏻‍♀️\n\n#BoliviaLibre #BoliviaUnida #EvoEsFraude #Bolivia #BoliviaNoHayGolpe #EvoDictador #NoFueGolpeFueFraude #EvoAsesino #EvoMentiroso"                                                                                                                                      
 [9] "@ammg1912 @JeanineAnez Quién eres tu para opinar de MI país #Bolivia 🇧🇴??? Ni vives acá ni te da la gana de informarte bien, que vergüenza ajena das !!! #BoliviaLibre #BoliviaUnida #EvoEsFraude #Bolivia #BoliviaNoHayGolpe #EvoDictador #NoFueGolpeFueFraude #EvoAsesino #EvoMentiroso https://t.co/h6F00OvEZv"
[10] "@albertovargas30 @JeanineAnez Jajajajaja cual paz ?? Evo Morales ?? Ppffffff ese narcotraficante sigue instigando a sus seguidores a la violencia !\n#BoliviaLibre #BoliviaUnida #EvoEsFraude #Bolivia #BoliviaNoHayGolpe #EvoDictador #NoFueGolpeFueFraude #EvoAsesino #EvoMentiroso https://t.co/JtT6oXLuTO"    

Este texto es necesario limpiarlo para que sea más fácil de utilizar.

Armado del corpus con tm

  • Primero creo un objeto de tipo Corpus. Utilizamos algo distinto a los conocidos vectores o dataframes porque es un objeto optimizado para trabajar con texto. Esto nos permite que los procesos sean mucho más eficientes, y por lo tanto trabajar con grandes corpus de manera rápida
myCorpus = Corpus(VectorSource(texto))

Limpieza del Corpus

  • Con la función tm_map podemos iterar sobre el corpus aplicando una transformación sobre cada documento

En este caso, para la limpieza utilizaremos las siguientes transformaciones.

  1. Pasar todo a minúscula (cómo la función que usamos no es de la librería tm tenemos que usar también content_transformer )
  2. Sacar la puntuación
  3. Sacar los números
  4. Sacar las stopwords
myCorpus = tm_map(myCorpus, content_transformer(tolower))
myCorpus = tm_map(myCorpus, removePunctuation)
myCorpus = tm_map(myCorpus, removeNumbers)
myCorpus = tm_map(myCorpus, removeWords, stopwords(kind = "es"))
inspect(myCorpus[1:10])
<<SimpleCorpus>>
Metadata:  corpus specific: 1, document level (indexed): 0
Content:  documents: 10

 [1]  represión  muertes  bolivia gracias  golpe    vez   alto  comunidad internacional debe exigirle   gobernantes  facto    fuerzas militares  policiales bolivianas  cese inmediato   represión   flagrante violación  derechos humanos                                  
 [2] “  matando   perro dónde   prensa  aquí relato   masacre bolivia  nov   httpstcopuznyftl                                                                                                                                                                               
 [3] httpstcomlpnjpmr                                                                                                                                                                                                                                                       
 [4]   socialista quer implantar  nefasto sistema em seus países\nmas ir viver onde ele já foi implementado  na venezuela ninguém quer nem  evo morales evoespueblo \n\nou    evo não foi  lá  sabe  nicolasmaduro   ponto  cair httpstcoahnafth                            
 [5] julianmaciast suarezluis suarezluis amigo   tipo  hizo famoso 😂😂😂  manera  desinformar   gente \n\nbolivialibre boliviaunida evoesfraude bolivia bolivianohaygolpe evodictador nofuegolpefuefraude evoasesino evomentiroso                                             
 [6] magnecaro luismebx mashirafael evoespueblo bolivialibre boliviaunida evoesfraude bolivia bolivianohaygolpe evodictador nofuegolpefuefraude evoasesino evomentiroso httpstcokkaqerqt                                                                                    
 [7]  afines  dictador evo morales  terroristas aplaudo   ffaa   policía  defender  seguridad   bolivianos 🇧🇴\n\nbolivialibre boliviaunida evoesfraude bolivia bolivianohaygolpe evodictador nofuegolpefuefraude evoasesino evomentiroso httpstcofhjpaqfkz                  
 [8] fortegazabala  verdades duelen 🤷🏻‍♀️\n\nbolivialibre boliviaunida evoesfraude bolivia bolivianohaygolpe evodictador nofuegolpefuefraude evoasesino evomentiroso                                                                                                          
 [9] ammg jeanineanez quién    opinar   país bolivia 🇧🇴  vives acá   da  gana  informarte bien  vergüenza ajena das  bolivialibre boliviaunida evoesfraude bolivia bolivianohaygolpe evodictador nofuegolpefuefraude evoasesino evomentiroso httpstcohfovezv                
[10] albertovargas jeanineanez jajajajaja  paz  evo morales  ppffffff  narcotraficante sigue instigando   seguidores   violencia \nbolivialibre boliviaunida evoesfraude bolivia bolivianohaygolpe evodictador nofuegolpefuefraude evoasesino evomentiroso httpstcojttoxluto

podemos ver que nos quedaron unos que son la forma de representar el “enter”. Lo mejor sería eliminarlos.

También queremos sacar los links. Para eso vamos a usar expresiones regulares para definir el patron que tiene un link, y luego crearemos una función que los elimine.

Expresiones regulares

Para que sea más sencilla la construcción de la expresión regular, usamos la librería RVerbalExpressions

# devtools::install_github("VerbalExpressions/RVerbalExpressions")
library(RVerbalExpressions)

expresion <- rx() %>% 
  rx_find('http') %>% 
  rx_maybe('s') %>% 
  # rx_maybe('://') %>% #como ya lo pasamos por los otros filtros, ya no hay puntuacion
  rx_anything_but(value = ' ')

expresion
[1] "(http)(s)?([^ ]*)"

Probamos la expresion con un ejemplo

txt <- "us blacklists chinese artificial intelligence firms  time\n\nread   httpstcoajreyxfsxy\n\nartificialintelligence ai datascience machinelearning bigdata deeplearning nlp robots iot"      
str_remove_all(txt, pattern = expresion)
[1] "us blacklists chinese artificial intelligence firms  time\n\nread    ai datascience machinelearning bigdata deeplearning nlp robots iot"

Lo pasamos por el corpus

myCorpus = tm_map(myCorpus, content_transformer(function(x) str_replace_all(x, pattern = expresion, replacement = ' ')))
transformation drops documents
myCorpus = tm_map(myCorpus, content_transformer(function(x) str_replace_all(x, pattern = '\n',replacement = ' ')))
transformation drops documents
inspect(myCorpus[1:10])
<<SimpleCorpus>>
Metadata:  corpus specific: 1, document level (indexed): 0
Content:  documents: 10

 [1]  represión  muertes  bolivia gracias  golpe    vez   alto  comunidad internacional debe exigirle   gobernantes  facto    fuerzas militares  policiales bolivianas  cese inmediato   represión   flagrante violación  derechos humanos                 
 [2] “  matando   perro dónde   prensa  aquí relato   masacre bolivia  nov                                                                                                                                                                                 
 [3]                                                                                                                                                                                                                                                       
 [4]   socialista quer implantar  nefasto sistema em seus países mas ir viver onde ele já foi implementado  na venezuela ninguém quer nem  evo morales evoespueblo   ou    evo não foi  lá  sabe  nicolasmaduro   ponto  cair                              
 [5] julianmaciast suarezluis suarezluis amigo   tipo  hizo famoso 😂😂😂  manera  desinformar   gente   bolivialibre boliviaunida evoesfraude bolivia bolivianohaygolpe evodictador nofuegolpefuefraude evoasesino evomentiroso                              
 [6] magnecaro luismebx mashirafael evoespueblo bolivialibre boliviaunida evoesfraude bolivia bolivianohaygolpe evodictador nofuegolpefuefraude evoasesino evomentiroso                                                                                    
 [7]  afines  dictador evo morales  terroristas aplaudo   ffaa   policía  defender  seguridad   bolivianos 🇧🇴  bolivialibre boliviaunida evoesfraude bolivia bolivianohaygolpe evodictador nofuegolpefuefraude evoasesino evomentiroso                     
 [8] fortegazabala  verdades duelen 🤷🏻‍♀️  bolivialibre boliviaunida evoesfraude bolivia bolivianohaygolpe evodictador nofuegolpefuefraude evoasesino evomentiroso                                                                                           
 [9] ammg jeanineanez quién    opinar   país bolivia 🇧🇴  vives acá   da  gana  informarte bien  vergüenza ajena das  bolivialibre boliviaunida evoesfraude bolivia bolivianohaygolpe evodictador nofuegolpefuefraude evoasesino evomentiroso               
[10] albertovargas jeanineanez jajajajaja  paz  evo morales  ppffffff  narcotraficante sigue instigando   seguidores   violencia  bolivialibre boliviaunida evoesfraude bolivia bolivianohaygolpe evodictador nofuegolpefuefraude evoasesino evomentiroso  

Creamos una matriz de Término-documento. En el parámetro control aclaro el tamaño mínimo de las palabras con minWordLength

myDTM = DocumentTermMatrix(myCorpus, control = list(minWordLength = 1))
inspect(myDTM)
<<DocumentTermMatrix (documents: 17997, terms: 46526)>>
Non-/sparse entries: 246992/837081430
Sparsity           : 100%
Maximal term length: 80
Weighting          : term frequency (tf)
Sample             :
       Terms
Docs    and bolivia elecciones evo evoespueblo gobierno golpe morales pueblo the
  11957   0       0          0   1           0        0     0       0      0   0
  16535   0       1          0   0           0        0     0       0      0   0
  17246   0       1          0   0           0        0     0       0      0   0
  2482    1       1          0   1           0        0     0       0      0   2
  2788    0       0          0   1           0        0     0       0      0   0
  584     0       0          1   0           0        0     0       1      0   0
  5939    0       0          0   1           0        0     0       0      0   0
  7944    0       1          0   0           0        0     0       0      0   0
  8646    0       0          0   1           0        0     0       1      0   0
  8894    0       1          0   0           0        0     0       0      0   0

findMostFreqTerms sirve para encontrar las palabras más frecuentes por documento o grupo de documentos. Como sólo me interesa el corpus total, tengo que modificar el INDEX para que sea el mismo en todos los documentos.

palabras_frecuentes <- findMostFreqTerms(myDTM,n = 25, INDEX = rep(1,nDocs(myDTM)))[[1]]

palabras_frecuentes
    bolivia         evo     morales         the evoespueblo       golpe  elecciones 
      10282        7443        3200        1967        1666        1326        1135 
   gobierno      pueblo         and      méxico  presidente        país   venezuela 
        918         903         872         774         710         675         587 
      ahora         mas       gente jeanineanez         paz         ser       audio 
        580         570         559         554         532         520         516 
        así       chile         oea     bolívia 
        503         498         489         488 

Armo un dataframe con esta información y elimino las palabras Bolivia y Evo, que fueron utilizadas para el scrapper y son por ello las más importantes

palabras_frecuentes <- tibble(word = names(palabras_frecuentes), freq =palabras_frecuentes)

palabras_frecuentes <- palabras_frecuentes %>% 
  filter(!(word %in% c('bolivia','evo')))

Con el comando wordcloud2 creo la nube de palabras

wordcloud2(palabras_frecuentes)

Topic Modeling

necesito eliminar los documentos vacíos (que luego de la limpieza quedaron sin ningúna palabra)

ui = unique(myDTM$i)
dtm = myDTM[ui,]

dim(myDTM)
[1] 17997 46526
dim(dtm)
[1] 17857 46526

Entreno el modelo de LDA. k indica el número de tópicos (dimensiones latentes).

  • Como ejemplo de una manipulación más fina del algorítmo, modificamos el parámetro delta, que como sólo es modificable cuando se utiliza el método de optimización Gibbs, debemos especificar esto también en al parámetro method. delta lo modificamos dentro del parámetro control que reúne varias especificaciones.

  • Delta es el parametro de la distribución a priori de los términos sobre los tópicos. Esto ajusta cuanto comparten los términos los tópicos (por lo general buscamos que no compartan demasiados términos, dado que es más sencilla la interpretación).

lda_fit <- LDA(dtm, k = 10,method = "Gibbs", control = list(delta=0.6,seed = 1234))
saveRDS(lda_fit,'resultados/lda_fit.rds')
lda_fit <- read_rds('resultados/lda_fit.rds')
lda_fit
A LDA_Gibbs topic model with 10 topics.

Terms <- terms(lda_fit, 10)
Terms
      Topic 1      Topic 2     Topic 3            Topic 4  Topic 5  Topic 6     
 [1,] "bolivia"    "evo"       "bolivia"          "the"    "país"   "evo"       
 [2,] "golpe"      "morales"   "muertos"          "and"    "gente"  "morales"   
 [3,] "pueblo"     "gobierno"  "represión"        "for"    "ser"    "méxico"    
 [4,] "ahora"      "audio"     "vía"              "you"    "mas"    "asilo"     
 [5,] "dice"       "paz"       "youtube"          "that"   "poder"  "rita"      
 [6,] "golpistas"  "boliviano" "senkata"          "coup"   "solo"   "segato"    
 [7,] "pues"       "video"     "alto"             "this"   "puede"  "amlo"      
 [8,] "democracia" "comida"    "militares"        "are"    "bien"   "hija"      
 [9,] "derecha"    "ministro"  "masacreenbolivia" "with"   "quiere" "dijo"      
[10,] "golpista"   "ciudades"  "masacre"          "people" "hacer"  "presidente"
      Topic 7      Topic 8          Topic 9      Topic 10     
 [1,] "bolivia"    "evoespueblo"    "bolivia"    "bolivia"    
 [2,] "así"        "jeanineanez"    "elecciones" "venezuela"  
 [3,] "bolivianos" "cnnee"          "presidenta" "chile"      
 [4,] "menos"      "larazonbolivia" "oea"        "almagrooea" 
 [5,] "mismo"      "soyfdelrincon"  "presidente" "cuba"       
 [6,] "pitita"     "terrorismo"     "bolívia"    "argentina"  
 [7,] "seguir"     "lopezobrador"   "áñez"       "upacificopy"
 [8,] "twitter"    "dictador"       "convocar"   "maduro"     
 [9,] "seguidores" "oeaoficial"     "jeanine"    "violencia"  
[10,] "vuelta"     "mundo"          "ley"        "ecuador"    
topicmodels_json_ldavis <- function(fitted, dtm){
    svd_tsne <- function(x) tsne(svd(x)$u)

    # Find required quantities
    phi <- as.matrix(posterior(fitted)$terms)
    theta <- as.matrix(posterior(fitted)$topics)
    vocab <- colnames(phi)
    term_freq <- slam::col_sums(dtm)

    # Convert to json
    json_lda <- LDAvis::createJSON(phi = phi, theta = theta,
                            vocab = vocab,
                            mds.method = svd_tsne,
                            plot.opts = list(xlab="tsne", ylab=""),
                            doc.length = as.vector(table(dtm$i)),
                            term.frequency = term_freq)

    return(json_lda)
}
json_res <- topicmodels_json_ldavis(lda_fit, dtm)
sigma summary: Min. : 33554432 |1st Qu. : 33554432 |Median : 33554432 |Mean : 33554432 |3rd Qu. : 33554432 |Max. : 33554432 |
Epoch: Iteration #100 error is: 12.9021288675413
Epoch: Iteration #200 error is: 0.468249056852732
Epoch: Iteration #300 error is: 0.289778848429587
Epoch: Iteration #400 error is: 0.268120180579266
Epoch: Iteration #500 error is: 0.260966680795684
Epoch: Iteration #600 error is: 0.256274456341558
Epoch: Iteration #700 error is: 0.253567240179068
Epoch: Iteration #800 error is: 0.252797289427224
Epoch: Iteration #900 error is: 0.252669111770735
Epoch: Iteration #1000 error is: 0.25264570458198
serVis(json_res)
createTcpServer: address already in use
To stop the server, run servr::daemon_stop(2) or restart your R session
Serving the directory /tmp/RtmpVeQCHt/file5bf820302827 at http://127.0.0.1:3864
LS0tCnRpdGxlOiAgTWluZXLDrWEgZGUgVGV4dG9zCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgdG9jOiB5ZXMKICAgIHRvY19mbG9hdDogeWVzCmRhdGU6ICIiCnN1YnRpdGxlOiBQcsOhY3RpY2EgR3VpYWRhCi0tLQoKCgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQojIGluc3RhbGwucGFja2FnZXMoInJ0d2VldCIpCmxpYnJhcnkocnR3ZWV0KQpsaWJyYXJ5KHRpZHl2ZXJzZSkKbGlicmFyeSh0bSkKbGlicmFyeSh3b3JkY2xvdWQyKQpsaWJyYXJ5KHRvcGljbW9kZWxzKQpsaWJyYXJ5KExEQXZpcykKbGlicmFyeSh0c25lKQpgYGAKCiMjIyBEZXNjYXJnYXMgZGUgdHdlZXRzIGNvbiBgcnR3ZWV0YAoKCmBgYHtyIGV2YWw9RkFMU0V9CnJ0IDwtIHNlYXJjaF90d2VldHMocSA9ICJCb2xpdmlhIE9SIGJvbGl2aWEgT1IgRXZvIix0eXBlID0gIm1peGVkIiwgbiA9IDE4MDAwLCBpbmNsdWRlX3J0cyA9IEZBTFNFKQoKc2F2ZVJEUyhydCwnZnVlbnRlcy9ydC5SRFMnKQpgYGAKCmBgYHtyfQpydCA8LSByZWFkX3JkcygnZnVlbnRlcy9ydC5SRFMnKQpgYGAKCgpgYGB7cn0KcnQgJT4lIAogIHNhbXBsZV9uKDEwKQpgYGAKCgpgYGB7cn0KcmFuZ2UocnQkY3JlYXRlZF9hdCkKYGBgCgpOb3MgZGEgbG9zIHR3ZWV0cyBkZSBsb3Mgw7psdGltb3MgbnVldmUgZMOtYXMsIG8gZWwgbcOheGltbyBxdWUgaW5kaWNhbW9zIG3DoXMgcmVjaWVudGUKCmBgYHtyfQpydCAlPiUKICB0c19wbG90KGJ5ID0gIm1pbnV0ZXMiKSArCiAgZ2dwbG90Mjo6dGhlbWVfbWluaW1hbCgpICsKICBnZ3Bsb3QyOjp0aGVtZShwbG90LnRpdGxlID0gZ2dwbG90Mjo6ZWxlbWVudF90ZXh0KGZhY2UgPSAiYm9sZCIpKSArCiAgZ2dwbG90Mjo6bGFicygKICAgIHggPSBOVUxMLCB5ID0gTlVMTCwKICAgIHRpdGxlID0gIkZyZWN1ZW5jaWEgZGUgbG9zIHR3ZWV0cyByZWxhY2lvbmFkb3MgTkxQIiwKICAgIHN1YnRpdGxlID0gIkFncmVnYWRvIGEgaW50ZXJ2YWxvcyBkZSB0cmVzIGhvcmFzIikrCiAgc2NhbGVfeF9kYXRldGltZShsaW1pdHMgPSBjKChtYXgocnQkY3JlYXRlZF9hdCktIDEyODAwKSxtYXgocnQkY3JlYXRlZF9hdCkpKQoKYGBgCgotIE1lIHF1ZWRvIGNvbiBlbCB0ZXh0bwoKYGBge3J9CnRleHRvIDwtICBydCR0ZXh0IAoKdGV4dG9bMToxMF0KYGBgCgpFc3RlIHRleHRvIGVzIG5lY2VzYXJpbyBsaW1waWFybG8gcGFyYSBxdWUgc2VhIG3DoXMgZsOhY2lsIGRlIHV0aWxpemFyLgoKIyMjIEFybWFkbyBkZWwgY29ycHVzIGNvbiBgdG1gCgotIFByaW1lcm8gY3JlbyB1biBvYmpldG8gZGUgdGlwbyBDb3JwdXMuIFV0aWxpemFtb3MgYWxnbyBkaXN0aW50byBhIGxvcyBjb25vY2lkb3MgdmVjdG9yZXMgbyBkYXRhZnJhbWVzIHBvcnF1ZSBlcyB1biBvYmpldG8gb3B0aW1pemFkbyBwYXJhIHRyYWJhamFyIGNvbiB0ZXh0by4gRXN0byBub3MgcGVybWl0ZSBxdWUgbG9zIHByb2Nlc29zIHNlYW4gbXVjaG8gbcOhcyBlZmljaWVudGVzLCB5IHBvciBsbyB0YW50byB0cmFiYWphciBjb24gZ3JhbmRlcyBjb3JwdXMgZGUgbWFuZXJhIHLDoXBpZGEKCmBgYHtyfQpteUNvcnB1cyA9IENvcnB1cyhWZWN0b3JTb3VyY2UodGV4dG8pKQpgYGAKCgojIyMgTGltcGllemEgZGVsIENvcnB1cwoKLSBDb24gbGEgZnVuY2nDs24gYHRtX21hcGAgcG9kZW1vcyBpdGVyYXIgc29icmUgZWwgY29ycHVzIGFwbGljYW5kbyB1bmEgdHJhbnNmb3JtYWNpw7NuIHNvYnJlIGNhZGEgZG9jdW1lbnRvCgpFbiBlc3RlIGNhc28sIHBhcmEgbGEgbGltcGllemEgdXRpbGl6YXJlbW9zIGxhcyBzaWd1aWVudGVzIHRyYW5zZm9ybWFjaW9uZXMuCgoxLiBQYXNhciB0b2RvIGEgbWluw7pzY3VsYSAoY8OzbW8gbGEgZnVuY2nDs24gcXVlIHVzYW1vcyBubyBlcyBkZSBsYSBsaWJyZXLDrWEgYHRtYCB0ZW5lbW9zIHF1ZSB1c2FyIHRhbWJpw6luIGBjb250ZW50X3RyYW5zZm9ybWVyYCApCjIuIFNhY2FyIGxhIHB1bnR1YWNpw7NuCjMuIFNhY2FyIGxvcyBuw7ptZXJvcwo0LiBTYWNhciBsYXMgc3RvcHdvcmRzCmBgYHtyIHdhcm5pbmc9RkFMU0V9Cm15Q29ycHVzID0gdG1fbWFwKG15Q29ycHVzLCBjb250ZW50X3RyYW5zZm9ybWVyKHRvbG93ZXIpKQpteUNvcnB1cyA9IHRtX21hcChteUNvcnB1cywgcmVtb3ZlUHVuY3R1YXRpb24pCm15Q29ycHVzID0gdG1fbWFwKG15Q29ycHVzLCByZW1vdmVOdW1iZXJzKQpteUNvcnB1cyA9IHRtX21hcChteUNvcnB1cywgcmVtb3ZlV29yZHMsIHN0b3B3b3JkcyhraW5kID0gImVzIikpCmBgYAoKCmBgYHtyfQppbnNwZWN0KG15Q29ycHVzWzE6MTBdKQpgYGAKCnBvZGVtb3MgdmVyIHF1ZSBub3MgcXVlZGFyb24gdW5vcyBfX1xuX18gcXVlIHNvbiBsYSBmb3JtYSBkZSByZXByZXNlbnRhciBlbCAiZW50ZXIiLiBMbyBtZWpvciBzZXLDrWEgZWxpbWluYXJsb3MuCgpUYW1iacOpbiBxdWVyZW1vcyBzYWNhciBsb3MgbGlua3MuIFBhcmEgZXNvIHZhbW9zIGEgdXNhciBleHByZXNpb25lcyByZWd1bGFyZXMgcGFyYSBkZWZpbmlyIGVsIHBhdHJvbiBxdWUgdGllbmUgdW4gbGluaywgeSBsdWVnbyBjcmVhcmVtb3MgdW5hIGZ1bmNpw7NuIHF1ZSBsb3MgZWxpbWluZS4KCiMjIyMgRXhwcmVzaW9uZXMgcmVndWxhcmVzCgpQYXJhIHF1ZSBzZWEgbcOhcyBzZW5jaWxsYSBsYSBjb25zdHJ1Y2Npw7NuIGRlIGxhIGV4cHJlc2nDs24gcmVndWxhciwgdXNhbW9zIGxhIGxpYnJlcsOtYSBbUlZlcmJhbEV4cHJlc3Npb25zXShodHRwczovL3J2ZXJiYWxleHByZXNzaW9ucy5uZXRsaWZ5LmNvbS9pbmRleC5odG1sKQoKYGBge3J9CiMgZGV2dG9vbHM6Omluc3RhbGxfZ2l0aHViKCJWZXJiYWxFeHByZXNzaW9ucy9SVmVyYmFsRXhwcmVzc2lvbnMiKQpsaWJyYXJ5KFJWZXJiYWxFeHByZXNzaW9ucykKCmV4cHJlc2lvbiA8LSByeCgpICU+JSAKICByeF9maW5kKCdodHRwJykgJT4lIAogIHJ4X21heWJlKCdzJykgJT4lIAogICMgcnhfbWF5YmUoJzovLycpICU+JSAjY29tbyB5YSBsbyBwYXNhbW9zIHBvciBsb3Mgb3Ryb3MgZmlsdHJvcywgeWEgbm8gaGF5IHB1bnR1YWNpb24KICByeF9hbnl0aGluZ19idXQodmFsdWUgPSAnICcpCgpleHByZXNpb24KCmBgYAoKUHJvYmFtb3MgbGEgZXhwcmVzaW9uIGNvbiB1biBlamVtcGxvCmBgYHtyfQp0eHQgPC0gInVzIGJsYWNrbGlzdHMgY2hpbmVzZSBhcnRpZmljaWFsIGludGVsbGlnZW5jZSBmaXJtcyAgdGltZVxuXG5yZWFkICAgaHR0cHN0Y29hanJleXhmc3h5XG5cbmFydGlmaWNpYWxpbnRlbGxpZ2VuY2UgYWkgZGF0YXNjaWVuY2UgbWFjaGluZWxlYXJuaW5nIGJpZ2RhdGEgZGVlcGxlYXJuaW5nIG5scCByb2JvdHMgaW90IiAgICAgIApzdHJfcmVtb3ZlX2FsbCh0eHQsIHBhdHRlcm4gPSBleHByZXNpb24pCmBgYAoKTG8gcGFzYW1vcyBwb3IgZWwgY29ycHVzCgpgYGB7cn0KbXlDb3JwdXMgPSB0bV9tYXAobXlDb3JwdXMsIGNvbnRlbnRfdHJhbnNmb3JtZXIoZnVuY3Rpb24oeCkgc3RyX3JlcGxhY2VfYWxsKHgsIHBhdHRlcm4gPSBleHByZXNpb24sIHJlcGxhY2VtZW50ID0gJyAnKSkpCm15Q29ycHVzID0gdG1fbWFwKG15Q29ycHVzLCBjb250ZW50X3RyYW5zZm9ybWVyKGZ1bmN0aW9uKHgpIHN0cl9yZXBsYWNlX2FsbCh4LCBwYXR0ZXJuID0gJ1xuJyxyZXBsYWNlbWVudCA9ICcgJykpKQoKYGBgCgpgYGB7cn0KaW5zcGVjdChteUNvcnB1c1sxOjEwXSkKYGBgCgpDcmVhbW9zIHVuYSBtYXRyaXogZGUgVMOpcm1pbm8tZG9jdW1lbnRvLiBFbiBlbCBwYXLDoW1ldHJvIGBjb250cm9sYCBhY2xhcm8gZWwgdGFtYcOxbyBtw61uaW1vIGRlIGxhcyBwYWxhYnJhcyBjb24gIG1pbldvcmRMZW5ndGgKCmBgYHtyfQpteURUTSA9IERvY3VtZW50VGVybU1hdHJpeChteUNvcnB1cywgY29udHJvbCA9IGxpc3QobWluV29yZExlbmd0aCA9IDEpKQpgYGAKCgoKYGBge3J9Cmluc3BlY3QobXlEVE0pCmBgYAoKZmluZE1vc3RGcmVxVGVybXMgc2lydmUgcGFyYSBlbmNvbnRyYXIgbGFzIHBhbGFicmFzIG3DoXMgZnJlY3VlbnRlcyBwb3IgZG9jdW1lbnRvIG8gZ3J1cG8gZGUgZG9jdW1lbnRvcy4gQ29tbyBzw7NsbyBtZSBpbnRlcmVzYSBlbCBjb3JwdXMgdG90YWwsIHRlbmdvIHF1ZSBtb2RpZmljYXIgZWwgSU5ERVggcGFyYSBxdWUgc2VhIGVsIG1pc21vIGVuIHRvZG9zIGxvcyBkb2N1bWVudG9zLgoKYGBge3J9CnBhbGFicmFzX2ZyZWN1ZW50ZXMgPC0gZmluZE1vc3RGcmVxVGVybXMobXlEVE0sbiA9IDI1LCBJTkRFWCA9IHJlcCgxLG5Eb2NzKG15RFRNKSkpW1sxXV0KCnBhbGFicmFzX2ZyZWN1ZW50ZXMKYGBgCgoKQXJtbyB1biBkYXRhZnJhbWUgY29uIGVzdGEgaW5mb3JtYWNpw7NuIHkgZWxpbWlubyBsYXMgcGFsYWJyYXMgQm9saXZpYSB5IEV2bywgcXVlIGZ1ZXJvbiB1dGlsaXphZGFzIHBhcmEgZWwgc2NyYXBwZXIgeSBzb24gcG9yIGVsbG8gbGFzIG3DoXMgaW1wb3J0YW50ZXMKCmBgYHtyfQpwYWxhYnJhc19mcmVjdWVudGVzIDwtIHRpYmJsZSh3b3JkID0gbmFtZXMocGFsYWJyYXNfZnJlY3VlbnRlcyksIGZyZXEgPXBhbGFicmFzX2ZyZWN1ZW50ZXMpCgpwYWxhYnJhc19mcmVjdWVudGVzIDwtIHBhbGFicmFzX2ZyZWN1ZW50ZXMgJT4lIAogIGZpbHRlcighKHdvcmQgJWluJSBjKCdib2xpdmlhJywnZXZvJykpKQpgYGAKCgpDb24gZWwgY29tYW5kbyBgd29yZGNsb3VkMmAgY3JlbyBsYSBudWJlIGRlIHBhbGFicmFzCmBgYHtyLCBmaWcud2lkdGg9OCwgZmlnLmhlaWdodD04fQp3b3JkY2xvdWQyKHBhbGFicmFzX2ZyZWN1ZW50ZXMpCmBgYAoKIyMjIFRvcGljIE1vZGVsaW5nCgoKbmVjZXNpdG8gZWxpbWluYXIgbG9zIGRvY3VtZW50b3MgdmFjw61vcyAocXVlIGx1ZWdvIGRlIGxhIGxpbXBpZXphIHF1ZWRhcm9uIHNpbiBuaW5nw7puYSBwYWxhYnJhKQoKYGBge3J9CnVpID0gdW5pcXVlKG15RFRNJGkpCmR0bSA9IG15RFRNW3VpLF0KCmRpbShteURUTSkKZGltKGR0bSkKYGBgCgpFbnRyZW5vIGVsIG1vZGVsbyBkZSBMREEuIGBrYCBpbmRpY2EgZWwgbsO6bWVybyBkZSB0w7NwaWNvcyAoZGltZW5zaW9uZXMgbGF0ZW50ZXMpLgoKLSBDb21vIGVqZW1wbG8gZGUgdW5hIG1hbmlwdWxhY2nDs24gbcOhcyBmaW5hIGRlbCBhbGdvcsOtdG1vLCBtb2RpZmljYW1vcyBlbCBwYXLDoW1ldHJvIF9kZWx0YV8sIHF1ZSBjb21vIHPDs2xvIGVzIG1vZGlmaWNhYmxlIGN1YW5kbyBzZSB1dGlsaXphIGVsIG3DqXRvZG8gZGUgb3B0aW1pemFjacOzbiBfR2liYnNfLCBkZWJlbW9zIGVzcGVjaWZpY2FyIGVzdG8gdGFtYmnDqW4gZW4gYWwgcGFyw6FtZXRybyBgbWV0aG9kYC4gX2RlbHRhXyBsbyBtb2RpZmljYW1vcyBkZW50cm8gZGVsIHBhcsOhbWV0cm8gYGNvbnRyb2xgIHF1ZSByZcO6bmUgdmFyaWFzIGVzcGVjaWZpY2FjaW9uZXMuCgotIERlbHRhIGVzIGVsIHBhcmFtZXRybyBkZSBsYSBkaXN0cmlidWNpw7NuIF9hIHByaW9yaV8gZGUgbG9zIHTDqXJtaW5vcyBzb2JyZSBsb3MgdMOzcGljb3MuIEVzdG8gYWp1c3RhIGN1YW50byBjb21wYXJ0ZW4gbG9zIHTDqXJtaW5vcyBsb3MgdMOzcGljb3MgKHBvciBsbyBnZW5lcmFsIGJ1c2NhbW9zIHF1ZSBubyBjb21wYXJ0YW4gZGVtYXNpYWRvcyB0w6lybWlub3MsIGRhZG8gcXVlIGVzIG3DoXMgc2VuY2lsbGEgbGEgaW50ZXJwcmV0YWNpw7NuKS4KCmBgYHtyIGV2YWw9RkFMU0V9CmxkYV9maXQgPC0gTERBKGR0bSwgayA9IDEwLG1ldGhvZCA9ICJHaWJicyIsIGNvbnRyb2wgPSBsaXN0KGRlbHRhPTAuNixzZWVkID0gMTIzNCkpCnNhdmVSRFMobGRhX2ZpdCwncmVzdWx0YWRvcy9sZGFfZml0LnJkcycpCmBgYAoKCmBgYHtyfQpsZGFfZml0IDwtIHJlYWRfcmRzKCdyZXN1bHRhZG9zL2xkYV9maXQucmRzJykKbGRhX2ZpdApgYGAKCgpgYGB7cn0KClRlcm1zIDwtIHRlcm1zKGxkYV9maXQsIDEwKQpUZXJtcwpgYGAKCgpgYGB7cn0KdG9waWNtb2RlbHNfanNvbl9sZGF2aXMgPC0gZnVuY3Rpb24oZml0dGVkLCBkdG0pewogICAgc3ZkX3RzbmUgPC0gZnVuY3Rpb24oeCkgdHNuZShzdmQoeCkkdSkKCiAgICAjIEZpbmQgcmVxdWlyZWQgcXVhbnRpdGllcwogICAgcGhpIDwtIGFzLm1hdHJpeChwb3N0ZXJpb3IoZml0dGVkKSR0ZXJtcykKICAgIHRoZXRhIDwtIGFzLm1hdHJpeChwb3N0ZXJpb3IoZml0dGVkKSR0b3BpY3MpCiAgICB2b2NhYiA8LSBjb2xuYW1lcyhwaGkpCiAgICB0ZXJtX2ZyZXEgPC0gc2xhbTo6Y29sX3N1bXMoZHRtKQoKICAgICMgQ29udmVydCB0byBqc29uCiAgICBqc29uX2xkYSA8LSBMREF2aXM6OmNyZWF0ZUpTT04ocGhpID0gcGhpLCB0aGV0YSA9IHRoZXRhLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgdm9jYWIgPSB2b2NhYiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1kcy5tZXRob2QgPSBzdmRfdHNuZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBsb3Qub3B0cyA9IGxpc3QoeGxhYj0idHNuZSIsIHlsYWI9IiIpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgZG9jLmxlbmd0aCA9IGFzLnZlY3Rvcih0YWJsZShkdG0kaSkpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgdGVybS5mcmVxdWVuY3kgPSB0ZXJtX2ZyZXEpCgogICAgcmV0dXJuKGpzb25fbGRhKQp9CmBgYAoKYGBge3J9Cmpzb25fcmVzIDwtIHRvcGljbW9kZWxzX2pzb25fbGRhdmlzKGxkYV9maXQsIGR0bSkKCnNlclZpcyhqc29uX3JlcykKYGBg