15. Text as data, 텍스트 마이닝

15.1. Tools for working with text

15.1.1. Regular expressions using Macbeth

: 셰익스피어의 ’맥베스’를 이용하여 텍스트데이터를 다루는 법을 알아보자. 먼저 데이터를 다운로드 받는다.

library(mdsr)

macbeth_url <- "http://www.gutenberg.org/cache/epub/1129/pg1129.txt"
Macbeth_raw <- RCurl::getURL(macbeth_url)
data(Macbeth_raw)


데이터를 확인해보면 전체 희곡을 단일 텍스트 문자열로 구성했음을 볼 수 있다. 이를 strsplit() 함수를 사용하고 행의 끝 문자를 줄넘김 지정하여 문자열 벡터로 분할해보자.

macbeth <- strsplit(Macbeth_raw, "\r\n")[[1]]
macbeth[300:310]
 [1] "meeting a bleeding Sergeant."                      ""                                                 
 [3] "  DUNCAN. What bloody man is that? He can report," "    As seemeth by his plight, of the revolt"      
 [5] "    The newest state."                             "  MALCOLM. This is the sergeant"                  
 [7] "    Who like a good and hardy soldier fought"      "    'Gainst my captivity. Hail, brave friend!"    
 [9] "    Say to the King the knowledge of the broil"    "    As thou didst leave it."                      
[11] "  SERGEANT. Doubtful it stood,"                   


본문을 살펴보니, 각 대사 앞에는 두 칸의 공백을 두며 문장을 시작하고 그 뒤에 대문자로 화자의 이름이 나오는 것을 볼 수 있었다. 텍스트마이닝의 힘은 텍스트에 포함된 아이디어를 정량화하는 데 있다. 그 예시로 극 중 맥베스의 대사가 몇 번 나왔는지 알아보자.

macbeth_lines <- grep(" MACBETH", macbeth, value = TRUE)
length(macbeth_lines)
[1] 208

맥베스의 대사는 극 중 208번 나왔다. 여기서 사용한 grep()함수는 첫번째 옵션을 두번째 변수에서 찾아주는 기능을 한다. value 옵션을 TRUE로 설정하면 값을 모두 반환하고, FALSE인 경우 값이 위치한 인덱스만 반환한다.


실제로 일치하는 줄을 추출하려면 stringr 패키지의 str_extract() 함수를 사용한다.

library(stringr)

pattern <- " MACBETH"
grep(pattern, macbeth, value = TRUE) %>%
  str_extract(pattern) %>%
  head()
[1] " MACBETH" " MACBETH" " MACBETH" " MACBETH" " MACBETH" " MACBETH"



grep() 함수의 첫번째 옵션인 pattern의 유형:
1. Metacharacters: 미리 정해둔 역할이 있는 정규식의 사용을 선언한다(이스케이프). R에서는 \ 백슬래시 두 개를 사용한다.

head(grep("MACBETH\\.", macbeth, value = TRUE))
[1] "  MACBETH. So foul and fair a day I have not seen."      
[2] "  MACBETH. Speak, if you can. What are you?"             
[3] "  MACBETH. Stay, you imperfect speakers, tell me more."  
[4] "  MACBETH. Into the air, and what seem'd corporal melted"
[5] "  MACBETH. Your children shall be kings."                
[6] "  MACBETH. And Thane of Cawdor too. Went it not so?"     


2. 문자 집합: [] 대괄호를 사용하여 찾을 문자열 조건을 정의한다.

head(grep("MAC[B-Z]", macbeth, value = TRUE))
[1] "MACHINE READABLE COPIES MAY BE DISTRIBUTED SO LONG AS SUCH COPIES"
[2] "MACHINE READABLE COPIES OF THIS ETEXT, SO LONG AS SUCH COPIES"    
[3] "WITH PERMISSION.  ELECTRONIC AND MACHINE READABLE COPIES MAY BE"  
[4] "THE TRAGEDY OF MACBETH"                                           
[5] "  MACBETH, Thane of Glamis and Cawdor, a general in the King's"   
[6] "  LADY MACBETH, his wife"                                         

위 예시에서는 MAC 뒤에 A가 없는 텍스트만 불러온다.


  1. 대체: 몇 가지 대안을 검색할 때 괄호와 함께 |를 사용한다.
head(grep("MAC(B|D)", macbeth, value = TRUE))
[1] "THE TRAGEDY OF MACBETH"                                        
[2] "  MACBETH, Thane of Glamis and Cawdor, a general in the King's"
[3] "  LADY MACBETH, his wife"                                      
[4] "  MACDUFF, Thane of Fife, a nobleman of Scotland"              
[5] "  LADY MACDUFF, his wife"                                      
[6] "  MACBETH. So foul and fair a day I have not seen."            

예시에서는 MACB, MACD를 포함하는 텍스트만 불러온다.


  1. 앵커(닻): 시작 텍스트를 고정할 때는 ^를, 끝 텍스트를 고정할 때는 $를 사용한다.
head(grep("^  MAC[B-Z]", macbeth, value = TRUE))
[1] "  MACBETH, Thane of Glamis and Cawdor, a general in the King's"
[2] "  MACDUFF, Thane of Fife, a nobleman of Scotland"              
[3] "  MACBETH. So foul and fair a day I have not seen."            
[4] "  MACBETH. Speak, if you can. What are you?"                   
[5] "  MACBETH. Stay, you imperfect speakers, tell me more."        
[6] "  MACBETH. Into the air, and what seem'd corporal melted"      

예시에서는 공백 두 칸과 MAC[B-Z]로 시작하는 텍스트만 불러온다.


  1. 반복: 패턴이 발생하는 횟수를 지정한다. ?는 0또는 1회를, *는 0회 이상을, +는 1회 이상을 지정한다.
grep("^ ?MAC[B-Z]", macbeth, value = TRUE) %>% head(1)
[1] "MACHINE READABLE COPIES MAY BE DISTRIBUTED SO LONG AS SUCH COPIES"
grep("^ *MAC[B-Z]", macbeth, value = TRUE) %>% head(1)
[1] "MACHINE READABLE COPIES MAY BE DISTRIBUTED SO LONG AS SUCH COPIES"
grep("^ +MAC[B-Z]", macbeth, value = TRUE) %>% head(1)
[1] "  MACBETH, Thane of Glamis and Cawdor, a general in the King's"




15.1.2. EX: Life and death in Macbeth

Macbect의 대사 패턴을 분석해보자. 주요 인물은 Macbeth, Lady Macbeth, Banquo, Duncan이다. grepl() 함수로 대사 벡터의 길이를 알 수 있고, 쓸모없는 데이터를 제외하기 위해 범위를 218~3172 행으로 제한한다.

Macbeth <- grepl(" MACBETH\\.", macbeth)
LadyMacbeth <- grepl(" LADY MACBETH\\.", macbeth)
Banquo <- grepl(" BANQUO\\.", macbeth)
Duncan <- grepl(" DUNCAN\\.", macbeth)


library(tidyr)

speaker_freq <- data.frame(Macbeth, LadyMacbeth, Banquo, Duncan) %>%
  mutate(line = 1:length(macbeth)) %>%
  gather(key = "character", value = "speak", -line) %>%
  mutate(speak = as.numeric(speak)) %>%
  filter(line > 218 & line < 3172)

glimpse(speaker_freq)
Rows: 11,812
Columns: 3
$ line      <int> 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 23…
$ character <chr> "Macbeth", "Macbeth", "Macbeth", "Macbeth", "Macbeth", "Macbeth", "Macbeth", "Macbeth", "Macbeth"…
$ speak     <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…



위 정리한 데이터로 플롯을 만들기 전에, 유요한 contectual 정보를 수집하고 플로팅을 하자. 각 막이 언제 시작하는 지에 대한 정보를 추가해준다.

acts_idx <- grep("^ACT [I|V]+", macbeth)
acts_labels <- str_extract(macbeth[acts_idx], "^ACT [I|V]+")
acts <- data.frame(line = acts_idx, labels = acts_labels)

ggplot(data = speaker_freq, aes(x = line, y = speak)) +
  geom_smooth(aes(color = character), method = "loess", se = 0, span = 0.4) +
  geom_vline(xintercept = acts_idx, color = "darkgray", lty = 3) +
  geom_text(data = acts, aes(y = 0.085, label = labels),
            hjust = "left", color = "darkgray") +
  ylim(c(0, NA)) + xlab("Line Number") + ylab("Proportion of Speeches")

플롯을 토대로 Duncan이 2막 초기에, 그리고 Banquo가 3막에 죽을 것이란 정보를 유추할 수 있다.



15.2. Analyzing textual data

arXiv, 아카이브는 빠르게 성장하고 있는 과학논문 저장소다. aRxiv 패키지는 아카이브에서 사용할 수 있는 파일 및 메타데이터에 대한 API를 제공한다. “데이터과학”과 일치하는 논문을 찾아서 그 정의를 크라우드소싱해보아라.

library(aRxiv)
DataSciencePapers <- arxiv_search(query = '"Data Science"', limit = 200)
data(DataSciencePapers)
head(DataSciencePapers)

제출일과 갱신일이 날짜 형식이 아닌 문자열로 입력되어있다. lubridate 패키지를 사용하여 이를 시간 형식으로 변환하자.

library(lubridate)
DataSciencePapers <- DataSciencePapers %>%
  mutate(submitted = ymd_hms(submitted), updated = ymd_hms(updated))

glimpse(DataSciencePapers)
Rows: 1,089
Columns: 15
$ id               <chr> "astro-ph/0701361v1", "0901.2805v1", "0901.3118v2", "0909.3895v1", "1106.2503v5", "1106.33…
$ submitted        <dttm> 2007-01-12 03:28:11, 2009-01-19 10:38:33, 2009-01-20 18:48:59, 2009-09-22 02:55:14, 2011-…
$ updated          <dttm> 2007-01-12 03:28:11, 2009-01-19 10:38:33, 2009-01-24 19:23:47, 2009-09-22 02:55:14, 2013-…
$ title            <chr> "How to Make the Dream Come True: The Astronomers' Data Manifesto", "Safeguarding Old and …
$ abstract         <chr> "  Astronomy is one of the most data-intensive of the sciences. Data technology\nis accele…
$ authors          <chr> "Ray P Norris", "Heinz Andernach", "O. V. Verkhodanov|S. A. Trushkin|H. Andernach|V. N. Ch…
$ affiliations     <chr> "", "", "Special Astrophysical Observatory, Nizhnij Arkhyz, Karachaj-Cherkesia, Russia;|Sp…
$ link_abstract    <chr> "http://arxiv.org/abs/astro-ph/0701361v1", "http://arxiv.org/abs/0901.2805v1", "http://arx…
$ link_pdf         <chr> "http://arxiv.org/pdf/astro-ph/0701361v1", "http://arxiv.org/pdf/0901.2805v1", "http://arx…
$ link_doi         <chr> "", "http://dx.doi.org/10.2481/dsj.8.41", "http://dx.doi.org/10.2481/dsj.8.34", "", "http:…
$ comment          <chr> "Submitted to Data Science Journal Presented at CODATA, Beijing,\n  October 2006", "11 pag…
$ journal_ref      <chr> "", "", "", "", "EPJ Data Science, 1:9, 2012", "", "EPJ Data Science 2012, 1:3", "EPJ Data…
$ doi              <chr> "", "10.2481/dsj.8.41", "10.2481/dsj.8.34", "", "10.1140/epjds9", "10.1007/978-1-4614-3323…
$ primary_category <chr> "astro-ph", "astro-ph.IM", "astro-ph.IM", "astro-ph.IM", "cs.SI", "astro-ph.IM", "cs.CL", …
$ categories       <chr> "astro-ph", "astro-ph.IM|astro-ph.CO", "astro-ph.IM|astro-ph.CO", "astro-ph.IM|cs.DB|cs.DL…

submitted와 updated가 chr 형식에서 dttm으로 잘 변경된 것을 볼 수 있다.




이제 제출 연도 분포를 살펴보자. 최근에 데이터과학에 대해 관심도가 증가하고 있는가?

tally(~ year(submitted), data = DataSciencePapers)
year(submitted)
2007 2009 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 
   1    3    3    7   15   25   52   94  151  187  313  238 

첫 논문이 2007년이고 그 뒤로 거의 2배씩 증가해왔음을 볼 수 있다.



첫 논문을 뜯어보자.

DataSciencePapers %>%
  filter(year(submitted) == 2007) %>%
  glimpse()
Rows: 1
Columns: 15
$ id               <chr> "astro-ph/0701361v1"
$ submitted        <dttm> 2007-01-12 03:28:11
$ updated          <dttm> 2007-01-12 03:28:11
$ title            <chr> "How to Make the Dream Come True: The Astronomers' Data Manifesto"
$ abstract         <chr> "  Astronomy is one of the most data-intensive of the sciences. Data technology\nis accele…
$ authors          <chr> "Ray P Norris"
$ affiliations     <chr> ""
$ link_abstract    <chr> "http://arxiv.org/abs/astro-ph/0701361v1"
$ link_pdf         <chr> "http://arxiv.org/pdf/astro-ph/0701361v1"
$ link_doi         <chr> ""
$ comment          <chr> "Submitted to Data Science Journal Presented at CODATA, Beijing,\n  October 2006"
$ journal_ref      <chr> ""
$ doi              <chr> ""
$ primary_category <chr> "astro-ph"
$ categories       <chr> "astro-ph"

자세히보니 본 논문은 저널명으로 인해 검색에 포함되었지만 데이터과학이 아닌 천문학 카테고리에 속한다. 검색을 primary-category 필드에서 다시 해보자.


tally(~ primary_category, data = DataSciencePapers)
primary_category
          astro-ph        astro-ph.CO        astro-ph.EP        astro-ph.GA        astro-ph.IM        astro-ph.SR 
                 1                  3                  1                  7                 20                  6 
   cond-mat.dis-nn  cond-mat.mtrl-sci cond-mat.quant-gas    cond-mat.str-el              cs.AI              cs.AR 
                 1                  7                  1                  3                 38                  1 
             cs.CE              cs.CG              cs.CL              cs.CR              cs.CV              cs.CY 
                 1                  5                 20                 13                 52                102 
             cs.DB              cs.DC              cs.DL              cs.DM              cs.DS              cs.ET 
                44                 20                  9                  4                 13                  1 
             cs.GL              cs.GR              cs.GT              cs.HC              cs.IR              cs.IT 
                 1                  1                  5                 20                 22                 15 
             cs.LG              cs.MA              cs.MS              cs.NA              cs.NE              cs.NI 
               165                  1                  2                  2                  5                  4 
             cs.OH              cs.PF              cs.PL              cs.RO              cs.SD              cs.SE 
                 5                  1                  7                  1                  1                 16 
             cs.SI              cs.SY            econ.GN            eess.AS            eess.IV            eess.SP 
                48                  1                  5                  2                 13                 13 
           eess.SY              gr-qc             hep-ph             hep-th            math.AG            math.AP 
                 1                  1                  1                  3                  2                  1 
           math.AT            math.CA            math.CO            math.CT            math.DG            math.HO 
                 5                  2                  8                  1                  1                  2 
           math.MG            math.NA            math.NT            math.OC            math.PR            math.RA 
                 1                 17                  1                 25                  9                  1 
           math.SP            math.ST            nucl-th    physics.chem-ph    physics.comp-ph    physics.data-an 
                 1                 26                  1                  1                 12                  4 
     physics.ed-ph    physics.flu-dyn     physics.geo-ph     physics.med-ph   physics.plasm-ph     physics.soc-ph 
                 5                  3                  3                  3                  1                 28 
  physics.space-ph           q-bio.BM           q-bio.GN           q-bio.MN           q-bio.OT           q-bio.PE 
                 2                  2                  5                  1                  1                  3 
          q-bio.QM           q-fin.CP           q-fin.EC           q-fin.GN           q-fin.MF           q-fin.PM 
                 4                  1                  1                  4                  1                  1 
          q-fin.RM           q-fin.ST           q-fin.TR           quant-ph            stat.AP            stat.CO 
                 1                  5                  1                  7                 25                  9 
           stat.ME            stat.ML            stat.OT 
                23                 62                 31 

정규식을 사용하여 -를 포함하고 소문자로만 필드를 다시 집계해보자.

DataSciencePapers %>%
  mutate(field = str_extract(primary_category, "^[a-z,-]+")) %>%
  tally(x = ~field) %>%
  sort()
field
   gr-qc   hep-ph  nucl-th   hep-th     econ quant-ph cond-mat    q-fin    q-bio     eess astro-ph  physics     math 
       1        1        1        3        5        7       12       15       16       29       38       62      103 
    stat       cs 
     150      646 

결과를 통해볼 때 논문의 대부분이 cs, 즉 컴퓨터 사이언스로 분류되고 그 뒤로 통계학과 수학이 잇고 있는 것을 알 수 있다.




15.2.1. Corpora

텍스트마이닝은 하나의 문서가 아니라 여러 개의 문서, corpus에서 수행되는 경우가 많다. 우선 tm 패키지를 사용하여 arXiv 초록의 텍스트 corpus를 만들어보자.

library(tm)

Corpus <- with(DataSciencePapers, VCorpus(VectorSource(abstract)))
Corpus[[1]] %>%
  as.character() %>%
  strwrap()
[1] "Astronomy is one of the most data-intensive of the sciences. Data technology is accelerating the quality"
[2] "and effectiveness of its research, and the rate of astronomical discovery is higher than ever. As a"     
[3] "result, many view astronomy as being in a 'Golden Age', and projects such as the Virtual Observatory are"
[4] "amongst the most ambitious data projects in any field of science. But these powerful tools will be"      
[5] "impotent unless the data on which they operate are of matching quality. Astronomy, like other fields of" 
[6] "science, therefore needs to establish and agree on a set of guiding principles for the management of"    
[7] "astronomical data. To focus this process, we are constructing a 'data manifesto', which proposes"        
[8] "guidelines to maximise the rate and cost-effectiveness of scientific discovery."                         



위 과정에서 만들어진 텍스트를 불필요한 공백과 숫자, 구두점을 제거하고 모두 소문자로 변환하며 불용어를 제거해보자. 텍스트 분석의 일반적인 작업이다.

Corpus <- Corpus %>%
  tm_map(stripWhitespace) %>%
  tm_map(removeNumbers) %>%
  tm_map(removePunctuation) %>%
  tm_map(content_transformer(tolower)) %>%
  tm_map(removeWords, stopwords("english"))

strwrap(as.character(Corpus[[1]]))
[1] "astronomy one dataintensive sciences data technology accelerating quality effectiveness research rate"  
[2] "astronomical discovery higher ever result many view astronomy golden age projects virtual observatory"  
[3] "amongst ambitious data projects field science powerful tools will impotent unless data operate matching"
[4] "quality astronomy like fields science therefore needs establish agree set guiding principles management"
[5] "astronomical data focus process constructing data manifesto proposes guidelines maximise rate"          
[6] "costeffectiveness scientific discovery"                                                                 




15.2.2. Word clouds

: 단어에 대한 일종의 다변량 히스토그램이다.

library(wordcloud)

wordcloud(Corpus, max.words = 30, scale = c(8, 1),
          colors = topo.colors(n = 30), random.color = TRUE)

아직 의미전달은 모호해보이지만 단어 빈도를 빠르게 시각화할 수 있다.




15.2.3. Document term matrices

텍스트마이닝에서 또 하나의 중요한 기술은 tf-idf 또는 dtm과 관련된다. tf-idf는 문서 전체에서 특정 단어의 중요성이 필요할 때 사용된다. DocumentTermMatrix() 함수는 문서 당 하나의 행과 용어 당 하나의 열로 dtm을 생성한다. 대부분의 문서에서 모든 단어가 다 나타나는 것이 아니기 때문에, 다음에 볼 예시에서 DTM행렬은 거의 98%가 0이고 그만큼 희박하기에 의미가 있다.

DTM <- DocumentTermMatrix(Corpus, control = list(weighting = weightTfIdf))
DTM
<<DocumentTermMatrix (documents: 1089, terms: 13089)>>
Non-/sparse entries: 89806/14164115
Sparsity           : 99%
Maximal term length: 62
Weighting          : term frequency - inverse document frequency (normalized) (tf-idf)



이제 DTM 오브젝트와 findFreqTerm() 함수로 tf-idf 점수가 높은 단어를 찾아보자.

findFreqTerms(DTM, lowfreq = 0.8) %>% head()
[1] "ability"  "able"     "absence"  "abstract" "academia" "academic"



DTM에는 각 단어에 대한 tf-idf 점수가 포함되어 있으므로 이를 살펴볼 수도 있다.

DTM %>% as.matrix() %>%
  apply(MARGIN = 2, sum) %>%
  sort(decreasing = TRUE) %>%
  head(9)
  learning      model     models    machine   analysis algorithms  algorithm        can    methods 
 10.553094   9.731736   9.318641   8.279509   8.207749   8.194278   8.137839   8.056505   7.944418 



또한 findAssocs() 함수로 특정 단어와 동일한 문서에 쓰이는 경향이 있는 단어를 찾을 수도 있다.

findAssocs(DTM, terms = "mathematics", corlimit = 0.5)
$mathematics
 conceptual       light  historical perspective      review        role      modern 
       0.79        0.72        0.71        0.67        0.59        0.55        0.52 

그 예시로 수학은 conceptual(개념)이 상위권에 랭크됐다.




15.3. Ingesting text

15.3.1. EX: Scraping the songs of the Beatles

비틀즈의 노래가 나열된 wikipedia의 콘텐츠를 다운로드하고 데이터를 살펴보자.

library(rvest)
library(tidyr)
library(methods)

url <- "http://en.wikipedia.org/wiki/List_of_songs_recorded_by_the_Beatles"

tables <- url %>%
  read_html() %>%
  html_nodes(css = "table")

songs <- html_table(tables[[5]])

glimpse(songs)
Rows: 82
Columns: 7
$ Song               <chr> "\"12-Bar Original\"", "\"Ain't She Sweet\"", "\"Ain't She Sweet\"", "\"All Things Must …
$ `Release(s)`       <chr> "Anthology 2", "Anthology 1", "Anthology 3", "Anthology 3", "The Beatles Bootleg Recordi…
$ `Songwriter(s)`    <chr> "LennonMcCartneyHarrisonStarkey", "Jack YellenMilton Ager", "Jack YellenMilton Ager", "H…
$ `Lead vocal(s)[d]` <chr> "Instrumental", "Lennon", "McCartney", "Harrison", "Lennon", "McCartney", "McCartney", "…
$ Yearrecorded       <int> 1965, 1961, 1969, 1969, 1963, 1963, 1962, 1968, 1968, 1963, 1960, 1968, 1967, 1968, 1963…
$ Yearreleased       <int> 1996, 1995, 1996, 1996, 2013, 2013, 1995, 2018, 2018, 1994, 1995, 2018, 1995, 2018, 1994…
$ `Ref(s)`           <chr> "[98][99]", "[84][100]", "[101][102]", "[101][103]", "[73][104]", "[105]", "[84][106]", …


이제 데이터를 정리해야한다. Title 변수의 따옴표를 제거하고 연도를 숫자형으로 바꾸며, songwriter(s) 변수는 괄호를 없애보자.

songs <- songs %>%
  mutate(Title = gsub('\\"', "", Song), Yearreleased = as.numeric(Yearreleased)) %>%
  rename(songwriters = `Songwriter(s)`)



다음은 비틀즈의 노래를 누가 작곡했는지 알아보자.

tally(~songwriters, data = songs) %>%
  sort(decreasing = TRUE) %>%
  head()
songwriters
               LennonMcCartney                    Chuck Berry                       Harrison 
                            11                              7                              5 
LennonMcCartneyHarrisonStarkey                      McCartney       Jerry LeiberMike Stoller 
                             5                              4                              3 

거의 레논과 매카트니이므로 두 인물이 기여한 곡의 수를 파악해보자.


length(grep("McCartney", songs$songwriters))
[1] 23
length(grep("Lennon", songs$songwriters))
[1] 20



정규식을 활용하여 두 사람이 공동작업한 곡에 대해 알아볼 수도 있다.

songs %>%
  filter(grepl("(McCartney|Lennon).*(McCartney|Lennon)", songwriters)) %>%
  select(Title) %>%
  head()



비틀즈는 무엇에 대해 노래했을까? 노래 제목으로 corpus를 만들고 불용어 제거 및 tf-idf로 dtm을 만들어서 알아보았다.

song_titles <- VCorpus(VectorSource(songs$Title)) %>%
  tm_map(removeWords, stopwords("english")) %>%
  DocumentTermMatrix(control = list(weighting = weightTfIdf))

findFreqTerms(song_titles, 7)
[1] "love"




15.3.2. Scraping data from twitter

트위터에는 고도의 API가 사용된다. twitteR 패키지로 이러한 데이터에 엑세스할 수 있다. API사용은 setup_twitter_oauth() 함수를 사용하고 계정과 개인 key를 설정해야 한다.

library(twitteR)

setup_twitter_oauth(consumer_key = "u2UthjbK6YHyQSp4sPk6yjsuV",
                    consumer_secret = "sC4mjd2WME5nH1FoWeSTuSy7JCP5DHjNtTYU1X6BwQ1vPZ0j3v",
                    access_token = "1365606414-7vPfPxStYNq6kWEATQlT8HZBd4G83BBcX4VoS9T",
                    access_secret = "0hJq9KYC3eBRuZzJqSacmtJ4PNJ7tNLkGrQrVl00JHirs")
[1] "Using direct authentication"


해시태그를 사용하여 트윗 목록을 검색할 수 있게 되었다. #datascience가 포함된 퇴근 1,000개 트윗을 검색해보자.

tweets <- searchTwitter("#datascience", lang = "en", n = 1000,
                        retryOnRateLimit = 100)

class(tweets)
[1] "list"
class(tweets[[1]])
[1] "status"
attr(,"package")
[1] "twitteR"

트위터는 트윗을 JSON 오브젝트로 제공하므로, twitteR의 twListToDF 기능을 통해 데이터프레임으로 변환한다.


tweet_df <- twListToDF(tweets) %>% as.tbl()
tweet_df %>%
  select(text) %>%
  head()



가져온 트윗의 문자 수 분포를 알아보았다.

ggplot(data = tweet_df, aes(x = nchar(text))) +
  geom_density(size = 2) +
  geom_vline(xintercept = 140) +
  scale_x_continuous("Number of Characters")



또한 리트윗 수의 분포도 볼 수 있다.

ggplot(data = tweet_df, aes(x = retweetCount)) +
  geom_density(size = 2)

대부분의 트윗이 리트윗이 많이되지 않는 모습이다.






LS0tDQp0aXRsZTogIk1vZGVybiBEYXRhIFNjaWVuY2Ugd2l0aCBSX0NwdDE1Ig0KYXV0aG9yOiBzZW9uZ3N1LCBraW0NCmRhdGU6IDIwMjMtMDEtMjYNCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KLS0tDQoNCmBgYHtyIGluY2x1ZGU9RkFMU0V9DQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkobW9zYWljKQ0KbGlicmFyeShtZHNyKQ0KbGlicmFyeSh0aWR5cikNCmBgYA0KDQoNCiMgMTUuIFRleHQgYXMgZGF0YSwg7YWN7Iqk7Yq4IOuniOydtOuLnQ0KDQojIyAxNS4xLiBUb29scyBmb3Igd29ya2luZyB3aXRoIHRleHQNCg0KIyMjIDE1LjEuMS4gUmVndWxhciBleHByZXNzaW9ucyB1c2luZyBNYWNiZXRoDQo6IOyFsOydteyKpO2UvOyWtOydmCAn66el67Kg7IqkJ+ulvCDsnbTsmqntlZjsl6wg7YWN7Iqk7Yq4642w7J207YSw66W8IOuLpOujqOuKlCDrspXsnYQg7JWM7JWE67O07J6QLiDrqLzsoIAg642w7J207YSw66W8IOuLpOyatOuhnOuTnCDrsJvripTri6QuDQpgYGB7cn0NCmxpYnJhcnkobWRzcikNCg0KbWFjYmV0aF91cmwgPC0gImh0dHA6Ly93d3cuZ3V0ZW5iZXJnLm9yZy9jYWNoZS9lcHViLzExMjkvcGcxMTI5LnR4dCINCk1hY2JldGhfcmF3IDwtIFJDdXJsOjpnZXRVUkwobWFjYmV0aF91cmwpDQpkYXRhKE1hY2JldGhfcmF3KQ0KYGBgDQpcDQoNCuuNsOydtO2EsOulvCDtmZXsnbjtlbTrs7TrqbQg7KCE7LK0IO2drOqzoeydhCDri6jsnbwg7YWN7Iqk7Yq4IOusuOyekOyXtOuhnCDqtazshLHtlojsnYzsnYQg67O8IOyImCDsnojri6QuIOydtOulvCBzdHJzcGxpdCgpIO2VqOyImOulvCDsgqzsmqntlZjqs6Ag7ZaJ7J2YIOuBnSDrrLjsnpDrpbwg7KSE64SY6rmAIOyngOygle2VmOyXrCDrrLjsnpDsl7Qg67Kh7YSw66GcIOu2hO2VoO2VtOuztOyekC4NCmBgYHtyfQ0KbWFjYmV0aCA8LSBzdHJzcGxpdChNYWNiZXRoX3JhdywgIlxyXG4iKVtbMV1dDQptYWNiZXRoWzMwMDozMTBdDQpgYGANClwNCg0K67O466y47J2EIOyCtO2OtOuztOuLiCwg6rCBIOuMgOyCrCDslZ7sl5DripQg65GQIOy5uOydmCDqs7XrsLHsnYQg65GQ66mwIOusuOyepeydhCDsi5zsnpHtlZjqs6Ag6re4IOuSpOyXkCDrjIDrrLjsnpDroZwg7ZmU7J6Q7J2YIOydtOumhOydtCDrgpjsmKTripQg6rKD7J2EIOuzvCDsiJgg7J6I7JeI64ukLiDthY3siqTtirjrp4jsnbTri53snZgg7Z6Y7J2AIO2FjeyKpO2KuOyXkCDtj6ztlajrkJwg7JWE7J2065SU7Ja066W8IOygleufie2ZlO2VmOuKlCDrjbAg7J6I64ukLiDqt7gg7JiI7Iuc66GcIOq3uSDspJEg66el67Kg7Iqk7J2YIOuMgOyCrOqwgCDrqocg67KIIOuCmOyZlOuKlOyngCDslYzslYTrs7TsnpAuDQpgYGB7cn0NCm1hY2JldGhfbGluZXMgPC0gZ3JlcCgiIE1BQ0JFVEgiLCBtYWNiZXRoLCB2YWx1ZSA9IFRSVUUpDQpsZW5ndGgobWFjYmV0aF9saW5lcykNCmBgYA0K66el67Kg7Iqk7J2YIOuMgOyCrOuKlCDqt7kg7KSRIDIwOOuyiCDrgpjsmZTri6QuIOyXrOq4sOyEnCDsgqzsmqntlZwgZ3JlcCgp7ZWo7IiY64qUIOyyq+uyiOynuCDsmLXshZjsnYQg65GQ67KI7Ke4IOuzgOyImOyXkOyEnCDssL7slYTso7zripQg6riw64ql7J2EIO2VnOuLpC4gdmFsdWUg7Ji17IWY7J2EIFRSVUXroZwg7ISk7KCV7ZWY66m0IOqwkuydhCDrqqjrkZAg67CY7ZmY7ZWY6rOgLCBGQUxTReyduCDqsr3smrAg6rCS7J20IOychOy5mO2VnCDsnbjrjbHsiqTrp4wg67CY7ZmY7ZWc64ukLg0KXA0KXA0KXA0KDQrsi6TsoJzroZwg7J287LmY7ZWY64qUIOykhOydhCDstpTstpztlZjroKTrqbQgc3RyaW5nciDtjKjtgqTsp4DsnZggc3RyX2V4dHJhY3QoKSDtlajsiJjrpbwg7IKs7Jqp7ZWc64ukLg0KYGBge3J9DQpsaWJyYXJ5KHN0cmluZ3IpDQoNCnBhdHRlcm4gPC0gIiBNQUNCRVRIIg0KZ3JlcChwYXR0ZXJuLCBtYWNiZXRoLCB2YWx1ZSA9IFRSVUUpICU+JQ0KICBzdHJfZXh0cmFjdChwYXR0ZXJuKSAlPiUNCiAgaGVhZCgpDQpgYGANClwNClwNCg0KZ3JlcCgpIO2VqOyImOydmCDssqvrsojsp7gg7Ji17IWY7J24IHBhdHRlcm7snZgg7Jyg7ZiVOg0KXA0KMS4gTWV0YWNoYXJhY3RlcnM6IOuvuOumrCDsoJXtlbTrkZQg7Jet7ZWg7J20IOyeiOuKlCDsoJXqt5zsi53snZgg7IKs7Jqp7J2EIOyEoOyWuO2VnOuLpCjsnbTsiqTsvIDsnbTtlIQpLiBS7JeQ7ISc64qUICoqXFwqKiDrsLHsiqzrnpjsi5wg65GQIOqwnOulvCDsgqzsmqntlZzri6QuDQpgYGB7cn0NCmhlYWQoZ3JlcCgiTUFDQkVUSFxcLiIsIG1hY2JldGgsIHZhbHVlID0gVFJVRSkpDQpgYGANClwNCjIuIOusuOyekCDsp5Htlak6IFtdIOuMgOq0hO2YuOulvCDsgqzsmqntlZjsl6wg7LC+7J2EIOusuOyekOyXtCDsobDqsbTsnYQg7KCV7J2Y7ZWc64ukLg0KYGBge3J9DQpoZWFkKGdyZXAoIk1BQ1tCLVpdIiwgbWFjYmV0aCwgdmFsdWUgPSBUUlVFKSkNCmBgYA0K7JyEIOyYiOyLnOyXkOyEnOuKlCBNQUMg65Kk7JeQIEHqsIAg7JeG64qUIO2FjeyKpO2KuOunjCDrtojrn6zsmKjri6QuDQpcDQpcDQpcDQoNCjMuIOuMgOyytDog66qHIOqwgOyngCDrjIDslYjsnYQg6rKA7IOJ7ZWgIOuVjCDqtITtmLjsmYAg7ZWo6ruYICoqfCoq66W8IOyCrOyaqe2VnOuLpC4NCmBgYHtyfQ0KaGVhZChncmVwKCJNQUMoQnxEKSIsIG1hY2JldGgsIHZhbHVlID0gVFJVRSkpDQpgYGANCuyYiOyLnOyXkOyEnOuKlCBNQUNCLCBNQUNE66W8IO2PrO2VqO2VmOuKlCDthY3siqTtirjrp4wg67aI65+s7Jio64ukLg0KXA0KXA0KXA0KDQo0LiDslbXsu6Qo64u7KTog7Iuc7J6RIO2FjeyKpO2KuOulvCDqs6DsoJXtlaAg65WM64qUICoqXioq66W8LCDrgZ0g7YWN7Iqk7Yq466W8IOqzoOygle2VoCDrlYzripQgKiokKirrpbwg7IKs7Jqp7ZWc64ukLg0KYGBge3J9DQpoZWFkKGdyZXAoIl4gIE1BQ1tCLVpdIiwgbWFjYmV0aCwgdmFsdWUgPSBUUlVFKSkNCmBgYA0K7JiI7Iuc7JeQ7ISc64qUIOqzteuwsSDrkZAg7Lm46rO8IE1BQ1tCLVpd66GcIOyLnOyeke2VmOuKlCDthY3siqTtirjrp4wg67aI65+s7Jio64ukLg0KXA0KXA0KXA0KDQo1LiDrsJjrs7U6IO2MqO2EtOydtCDrsJzsg53tlZjripQg7Zqf7IiY66W8IOyngOygle2VnOuLpC4gP+uKlCAw65iQ64qUIDHtmozrpbwsICrripQgMO2ajCDsnbTsg4HsnYQsICvripQgMe2ajCDsnbTsg4HsnYQg7KeA7KCV7ZWc64ukLg0KYGBge3J9DQpncmVwKCJeID9NQUNbQi1aXSIsIG1hY2JldGgsIHZhbHVlID0gVFJVRSkgJT4lIGhlYWQoMSkNCmdyZXAoIl4gKk1BQ1tCLVpdIiwgbWFjYmV0aCwgdmFsdWUgPSBUUlVFKSAlPiUgaGVhZCgxKQ0KZ3JlcCgiXiArTUFDW0ItWl0iLCBtYWNiZXRoLCB2YWx1ZSA9IFRSVUUpICU+JSBoZWFkKDEpDQpgYGANClwNClwNClwNCg0KIyMjIDE1LjEuMi4gRVg6IExpZmUgYW5kIGRlYXRoIGluIE1hY2JldGgNCk1hY2JlY3TsnZgg64yA7IKsIO2MqO2EtOydhCDrtoTshJ3tlbTrs7TsnpAuIOyjvOyalCDsnbjrrLzsnYAgTWFjYmV0aCwgTGFkeSBNYWNiZXRoLCBCYW5xdW8sIER1bmNhbuydtOuLpC4gZ3JlcGwoKSDtlajsiJjroZwg64yA7IKsIOuyoe2EsOydmCDquLjsnbTrpbwg7JWMIOyImCDsnojqs6AsIOyTuOuqqOyXhuuKlCDrjbDsnbTthLDrpbwg7KCc7Jm47ZWY6riwIOychO2VtCDrspTsnITrpbwgMjE4fjMxNzIg7ZaJ7Jy866GcIOygnO2VnO2VnOuLpC4NCmBgYHtyfQ0KTWFjYmV0aCA8LSBncmVwbCgiIE1BQ0JFVEhcXC4iLCBtYWNiZXRoKQ0KTGFkeU1hY2JldGggPC0gZ3JlcGwoIiBMQURZIE1BQ0JFVEhcXC4iLCBtYWNiZXRoKQ0KQmFucXVvIDwtIGdyZXBsKCIgQkFOUVVPXFwuIiwgbWFjYmV0aCkNCkR1bmNhbiA8LSBncmVwbCgiIERVTkNBTlxcLiIsIG1hY2JldGgpDQoNCg0KbGlicmFyeSh0aWR5cikNCg0Kc3BlYWtlcl9mcmVxIDwtIGRhdGEuZnJhbWUoTWFjYmV0aCwgTGFkeU1hY2JldGgsIEJhbnF1bywgRHVuY2FuKSAlPiUNCiAgbXV0YXRlKGxpbmUgPSAxOmxlbmd0aChtYWNiZXRoKSkgJT4lDQogIGdhdGhlcihrZXkgPSAiY2hhcmFjdGVyIiwgdmFsdWUgPSAic3BlYWsiLCAtbGluZSkgJT4lDQogIG11dGF0ZShzcGVhayA9IGFzLm51bWVyaWMoc3BlYWspKSAlPiUNCiAgZmlsdGVyKGxpbmUgPiAyMTggJiBsaW5lIDwgMzE3MikNCg0KZ2xpbXBzZShzcGVha2VyX2ZyZXEpDQpgYGANClwNClwNCg0K7JyEIOygleumrO2VnCDrjbDsnbTthLDroZwg7ZSM66Gv7J2EIOunjOuTpOq4sCDsoITsl5AsIOycoOyalO2VnCBjb250ZWN0dWFsIOygleuztOulvCDsiJjsp5HtlZjqs6Ag7ZSM66Gc7YyF7J2EIO2VmOyekC4g6rCBIOunieydtCDslrjsoJwg7Iuc7J6R7ZWY64qUIOyngOyXkCDrjIDtlZwg7KCV67O066W8IOy2lOqwgO2VtOykgOuLpC4NCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQphY3RzX2lkeCA8LSBncmVwKCJeQUNUIFtJfFZdKyIsIG1hY2JldGgpDQphY3RzX2xhYmVscyA8LSBzdHJfZXh0cmFjdChtYWNiZXRoW2FjdHNfaWR4XSwgIl5BQ1QgW0l8Vl0rIikNCmFjdHMgPC0gZGF0YS5mcmFtZShsaW5lID0gYWN0c19pZHgsIGxhYmVscyA9IGFjdHNfbGFiZWxzKQ0KDQpnZ3Bsb3QoZGF0YSA9IHNwZWFrZXJfZnJlcSwgYWVzKHggPSBsaW5lLCB5ID0gc3BlYWspKSArDQogIGdlb21fc21vb3RoKGFlcyhjb2xvciA9IGNoYXJhY3RlciksIG1ldGhvZCA9ICJsb2VzcyIsIHNlID0gMCwgc3BhbiA9IDAuNCkgKw0KICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQgPSBhY3RzX2lkeCwgY29sb3IgPSAiZGFya2dyYXkiLCBsdHkgPSAzKSArDQogIGdlb21fdGV4dChkYXRhID0gYWN0cywgYWVzKHkgPSAwLjA4NSwgbGFiZWwgPSBsYWJlbHMpLA0KICAgICAgICAgICAgaGp1c3QgPSAibGVmdCIsIGNvbG9yID0gImRhcmtncmF5IikgKw0KICB5bGltKGMoMCwgTkEpKSArIHhsYWIoIkxpbmUgTnVtYmVyIikgKyB5bGFiKCJQcm9wb3J0aW9uIG9mIFNwZWVjaGVzIikNCmBgYA0K7ZSM66Gv7J2EIO2GoOuMgOuhnCBEdW5jYW7snbQgMuuniSDstIjquLDsl5AsIOq3uOumrOqzoCBCYW5xdW/qsIAgM+unieyXkCDso73snYQg6rKD7J20656AIOygleuztOulvCDsnKDstpTtlaAg7IiYIOyeiOuLpC4NClwNClwNClwNClwNCg0KIyMgMTUuMi4gQW5hbHl6aW5nIHRleHR1YWwgZGF0YQ0KYXJYaXYsIOyVhOy5tOydtOu4jOuKlCDruaDrpbTqsowg7ISx7J6l7ZWY6rOgIOyeiOuKlCDqs7ztlZnrhbzrrLgg7KCA7J6l7IaM64ukLiBhUnhpdiDtjKjtgqTsp4DripQg7JWE7Lm07J2067iM7JeQ7IScIOyCrOyaqe2VoCDsiJgg7J6I64qUIO2MjOydvCDrsI8g66mU7YOA642w7J207YSw7JeQIOuMgO2VnCBBUEnrpbwg7KCc6rO17ZWc64ukLiAi642w7J207YSw6rO87ZWZIuqzvCDsnbzsuZjtlZjripQg64W866y47J2EIOywvuyVhOyEnCDqt7gg7KCV7J2Y66W8IO2BrOudvOyasOuTnOyGjOyLse2VtOuztOyVhOudvC4NClwNClwNCg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkoYVJ4aXYpDQpEYXRhU2NpZW5jZVBhcGVycyA8LSBhcnhpdl9zZWFyY2gocXVlcnkgPSAnIkRhdGEgU2NpZW5jZSInLCBsaW1pdCA9IDIwMCkNCmRhdGEoRGF0YVNjaWVuY2VQYXBlcnMpDQpoZWFkKERhdGFTY2llbmNlUGFwZXJzKQ0KYGBgDQrsoJzstpzsnbzqs7wg6rCx7Iug7J287J20IOuCoOynnCDtmJXsi53snbQg7JWE64uMIOusuOyekOyXtOuhnCDsnoXroKXrkJjslrTsnojri6QuIGx1YnJpZGF0ZSDtjKjtgqTsp4Drpbwg7IKs7Jqp7ZWY7JesIOydtOulvCDsi5zqsIQg7ZiV7Iud7Jy866GcIOuzgO2ZmO2VmOyekC4NCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KGx1YnJpZGF0ZSkNCkRhdGFTY2llbmNlUGFwZXJzIDwtIERhdGFTY2llbmNlUGFwZXJzICU+JQ0KICBtdXRhdGUoc3VibWl0dGVkID0geW1kX2htcyhzdWJtaXR0ZWQpLCB1cGRhdGVkID0geW1kX2htcyh1cGRhdGVkKSkNCg0KZ2xpbXBzZShEYXRhU2NpZW5jZVBhcGVycykNCmBgYA0Kc3VibWl0dGVk7JmAIHVwZGF0ZWTqsIAgY2hyIO2YleyLneyXkOyEnCBkdHRt7Jy866GcIOyemCDrs4Dqsr3rkJwg6rKD7J2EIOuzvCDsiJgg7J6I64ukLg0KDQpcDQpcDQpcDQoNCuydtOygnCDsoJzstpwg7Jew64+EIOu2hO2PrOulvCDsgrTtjrTrs7TsnpAuIOy1nOq3vOyXkCDrjbDsnbTthLDqs7ztlZnsl5Ag64yA7ZW0IOq0gOyLrOuPhOqwgCDspp3qsIDtlZjqs6Ag7J6I64qU6rCAPw0KYGBge3J9DQp0YWxseSh+IHllYXIoc3VibWl0dGVkKSwgZGF0YSA9IERhdGFTY2llbmNlUGFwZXJzKQ0KYGBgDQrssqsg64W866y47J20IDIwMDfrhYTsnbTqs6Ag6re4IOuSpOuhnCDqsbDsnZggMuuwsOyUqSDspp3qsIDtlbTsmZTsnYzsnYQg67O8IOyImCDsnojri6QuDQoNClwNClwNCg0K7LKrIOuFvOusuOydhCDrnK/slrTrs7TsnpAuDQpgYGB7cn0NCkRhdGFTY2llbmNlUGFwZXJzICU+JQ0KICBmaWx0ZXIoeWVhcihzdWJtaXR0ZWQpID09IDIwMDcpICU+JQ0KICBnbGltcHNlKCkNCmBgYA0K7J6Q7IS47Z6I67O064uIIOuzuCDrhbzrrLjsnYAg7KCA64SQ66qF7Jy866GcIOyduO2VtCDqsoDsg4nsl5Ag7Y+s7ZWo65CY7JeI7KeA66eMIOuNsOydtO2EsOqzvO2VmeydtCDslYTri4wg7LKc66y47ZWZIOy5tO2FjOqzoOumrOyXkCDsho3tlZzri6QuIOqygOyDieydhCBwcmltYXJ5LWNhdGVnb3J5IO2VhOuTnOyXkOyEnCDri6Tsi5wg7ZW067O07J6QLg0KDQpcDQpgYGB7cn0NCnRhbGx5KH4gcHJpbWFyeV9jYXRlZ29yeSwgZGF0YSA9IERhdGFTY2llbmNlUGFwZXJzKQ0KYGBgDQrsoJXqt5zsi53snYQg7IKs7Jqp7ZWY7JesIC3rpbwg7Y+s7ZWo7ZWY6rOgIOyGjOusuOyekOuhnOunjCDtlYTrk5zrpbwg64uk7IucIOynkeqzhO2VtOuztOyekC4NClwNCg0KYGBge3J9DQpEYXRhU2NpZW5jZVBhcGVycyAlPiUNCiAgbXV0YXRlKGZpZWxkID0gc3RyX2V4dHJhY3QocHJpbWFyeV9jYXRlZ29yeSwgIl5bYS16LC1dKyIpKSAlPiUNCiAgdGFsbHkoeCA9IH5maWVsZCkgJT4lDQogIHNvcnQoKQ0KYGBgDQrqsrDqs7zrpbwg7Ya17ZW067O8IOuVjCDrhbzrrLjsnZgg64yA67aA67aE7J20IGNzLCDspokg7Lu07ZOo7YSwIOyCrOydtOyWuOyKpOuhnCDrtoTrpZjrkJjqs6Ag6re4IOuSpOuhnCDthrXqs4TtlZnqs7wg7IiY7ZWZ7J20IOyeh+qzoCDsnojripQg6rKD7J2EIOyVjCDsiJgg7J6I64ukLg0KDQpcDQpcDQpcDQoNCiMjIyAxNS4yLjEuIENvcnBvcmENCu2FjeyKpO2KuOuniOydtOuLneydgCDtlZjrgpjsnZgg66y47ISc6rCAIOyVhOuLiOudvCDsl6zrn6wg6rCc7J2YIOusuOyEnCwgY29ycHVz7JeQ7IScIOyImO2WieuQmOuKlCDqsr3smrDqsIAg66eO64ukLiDsmrDshKAgdG0g7Yyo7YKk7KeA66W8IOyCrOyaqe2VmOyXrCBhclhpdiDstIjroZ3snZgg7YWN7Iqk7Yq4IGNvcnB1c+ulvCDrp4zrk6TslrTrs7TsnpAuDQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeSh0bSkNCg0KQ29ycHVzIDwtIHdpdGgoRGF0YVNjaWVuY2VQYXBlcnMsIFZDb3JwdXMoVmVjdG9yU291cmNlKGFic3RyYWN0KSkpDQpDb3JwdXNbWzFdXSAlPiUNCiAgYXMuY2hhcmFjdGVyKCkgJT4lDQogIHN0cndyYXAoKQ0KYGBgDQpcDQpcDQoNCuychCDqs7zsoJXsl5DshJwg66eM65Ok7Ja07KeEIO2FjeyKpO2KuOulvCDrtojtlYTsmpTtlZwg6rO167Cx6rO8IOyIq+yekCwg6rWs65GQ7KCQ7J2EIOygnOqxsO2VmOqzoCDrqqjrkZAg7IaM66y47J6Q66GcIOuzgO2ZmO2VmOupsCDrtojsmqnslrTrpbwg7KCc6rGw7ZW067O07J6QLiDthY3siqTtirgg67aE7ISd7J2YIOydvOuwmOyggeyduCDsnpHsl4XsnbTri6QuDQpgYGB7cn0NCkNvcnB1cyA8LSBDb3JwdXMgJT4lDQogIHRtX21hcChzdHJpcFdoaXRlc3BhY2UpICU+JQ0KICB0bV9tYXAocmVtb3ZlTnVtYmVycykgJT4lDQogIHRtX21hcChyZW1vdmVQdW5jdHVhdGlvbikgJT4lDQogIHRtX21hcChjb250ZW50X3RyYW5zZm9ybWVyKHRvbG93ZXIpKSAlPiUNCiAgdG1fbWFwKHJlbW92ZVdvcmRzLCBzdG9wd29yZHMoImVuZ2xpc2giKSkNCg0Kc3Ryd3JhcChhcy5jaGFyYWN0ZXIoQ29ycHVzW1sxXV0pKQ0KYGBgDQoNClwNClwNClwNCg0KIyMjIDE1LjIuMi4gV29yZCBjbG91ZHMNCjog64uo7Ja07JeQIOuMgO2VnCDsnbzsooXsnZgg64uk67OA65+JIO2eiOyKpO2GoOq3uOueqOydtOuLpC4NCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KHdvcmRjbG91ZCkNCg0Kd29yZGNsb3VkKENvcnB1cywgbWF4LndvcmRzID0gMzAsIHNjYWxlID0gYyg4LCAxKSwNCiAgICAgICAgICBjb2xvcnMgPSB0b3BvLmNvbG9ycyhuID0gMzApLCByYW5kb20uY29sb3IgPSBUUlVFKQ0KYGBgDQrslYTsp4Eg7J2Y66+47KCE64us7J2AIOuqqO2YuO2VtOuztOydtOyngOunjCDri6jslrQg67mI64+E66W8IOu5oOultOqyjCDsi5zqsIHtmZTtlaAg7IiYIOyeiOuLpC4NCg0KXA0KXA0KXA0KDQojIyMgMTUuMi4zLiBEb2N1bWVudCB0ZXJtIG1hdHJpY2VzDQrthY3siqTtirjrp4jsnbTri53sl5DshJwg65iQIO2VmOuCmOydmCDspJHsmpTtlZwg6riw7Iig7J2AIHRmLWlkZiDrmJDripQgZHRt6rO8IOq0gOugqOuQnOuLpC4gdGYtaWRm64qUIOusuOyEnCDsoITssrTsl5DshJwg7Yq57KCVIOuLqOyWtOydmCDspJHsmpTshLHsnbQg7ZWE7JqU7ZWgIOuVjCDsgqzsmqnrkJzri6QuIERvY3VtZW50VGVybU1hdHJpeCgpIO2VqOyImOuKlCDrrLjshJwg64u5IO2VmOuCmOydmCDtlonqs7wg7Jqp7Ja0IOuLuSDtlZjrgpjsnZgg7Je066GcIGR0beydhCDsg53shLHtlZzri6QuIOuMgOu2gOu2hOydmCDrrLjshJzsl5DshJwg66qo65OgIOuLqOyWtOqwgCDri6Qg64KY7YOA64KY64qUIOqyg+ydtCDslYTri4jquLAg65WM66y47JeQLCDri6TsnYzsl5Ag67O8IOyYiOyLnOyXkOyEnCBEVE3tlonroKzsnYAg6rGw7J2YIDk4JeqwgCAw7J206rOgIOq3uOunjO2BvCDtnazrsJXtlZjquLDsl5Ag7J2Y66+46rCAIOyeiOuLpC4NCmBgYHtyfQ0KRFRNIDwtIERvY3VtZW50VGVybU1hdHJpeChDb3JwdXMsIGNvbnRyb2wgPSBsaXN0KHdlaWdodGluZyA9IHdlaWdodFRmSWRmKSkNCkRUTQ0KYGBgDQpcDQpcDQoNCuydtOygnCBEVE0g7Jik67iM7KCd7Yq47JmAIGZpbmRGcmVxVGVybSgpIO2VqOyImOuhnCB0Zi1pZGYg7KCQ7IiY6rCAIOuGkuydgCDri6jslrTrpbwg7LC+7JWE67O07J6QLg0KYGBge3J9DQpmaW5kRnJlcVRlcm1zKERUTSwgbG93ZnJlcSA9IDAuOCkgJT4lIGhlYWQoKQ0KYGBgDQoNClwNClwNCg0KRFRN7JeQ64qUIOqwgSDri6jslrTsl5Ag64yA7ZWcIHRmLWlkZiDsoJDsiJjqsIAg7Y+s7ZWo65CY7Ja0IOyeiOycvOuvgOuhnCDsnbTrpbwg7IK07Y6067O8IOyImOuPhCDsnojri6QuDQpgYGB7cn0NCkRUTSAlPiUgYXMubWF0cml4KCkgJT4lDQogIGFwcGx5KE1BUkdJTiA9IDIsIHN1bSkgJT4lDQogIHNvcnQoZGVjcmVhc2luZyA9IFRSVUUpICU+JQ0KICBoZWFkKDkpDQpgYGANCg0KXA0KXA0KDQrrmJDtlZwgZmluZEFzc29jcygpIO2VqOyImOuhnCDtirnsoJUg64uo7Ja07JmAIOuPmeydvO2VnCDrrLjshJzsl5Ag7JOw7J2064qUIOqyve2WpeydtCDsnojripQg64uo7Ja066W8IOywvuydhCDsiJjrj4Qg7J6I64ukLg0KYGBge3J9DQpmaW5kQXNzb2NzKERUTSwgdGVybXMgPSAibWF0aGVtYXRpY3MiLCBjb3JsaW1pdCA9IDAuNSkNCmBgYA0K6re4IOyYiOyLnOuhnCDsiJjtlZnsnYAgY29uY2VwdHVhbCjqsJzrhZAp7J20IOyDgeychOq2jOyXkCDrnq3tgazrkJDri6QuDQoNClwNClwNClwNCg0KIyMgMTUuMy4gSW5nZXN0aW5nIHRleHQNCg0KIyMjIDE1LjMuMS4gRVg6IFNjcmFwaW5nIHRoZSBzb25ncyBvZiB0aGUgQmVhdGxlcw0K67mE7YuA7KaI7J2YIOuFuOuemOqwgCDrgpjsl7TrkJwgd2lraXBlZGlh7J2YIOy9mO2FkOy4oOulvCDri6TsmrTroZzrk5ztlZjqs6Ag642w7J207YSw66W8IOyCtO2OtOuztOyekC4NCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KHJ2ZXN0KQ0KbGlicmFyeSh0aWR5cikNCmxpYnJhcnkobWV0aG9kcykNCg0KdXJsIDwtICJodHRwOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0xpc3Rfb2Zfc29uZ3NfcmVjb3JkZWRfYnlfdGhlX0JlYXRsZXMiDQoNCnRhYmxlcyA8LSB1cmwgJT4lDQogIHJlYWRfaHRtbCgpICU+JQ0KICBodG1sX25vZGVzKGNzcyA9ICJ0YWJsZSIpDQoNCnNvbmdzIDwtIGh0bWxfdGFibGUodGFibGVzW1s1XV0pDQoNCmdsaW1wc2Uoc29uZ3MpDQpgYGANClwNCg0K7J207KCcIOuNsOydtO2EsOulvCDsoJXrpqztlbTslbztlZzri6QuIFRpdGxlIOuzgOyImOydmCDrlLDsmLTtkZzrpbwg7KCc6rGw7ZWY6rOgIOyXsOuPhOulvCDsiKvsnpDtmJXsnLzroZwg67CU6r6466mwLCBzb25nd3JpdGVyKHMpIOuzgOyImOuKlCDqtITtmLjrpbwg7JeG7JWg67O07J6QLg0KYGBge3J9DQpzb25ncyA8LSBzb25ncyAlPiUNCiAgbXV0YXRlKFRpdGxlID0gZ3N1YignXFwiJywgIiIsIFNvbmcpLCBZZWFycmVsZWFzZWQgPSBhcy5udW1lcmljKFllYXJyZWxlYXNlZCkpICU+JQ0KICByZW5hbWUoc29uZ3dyaXRlcnMgPSBgU29uZ3dyaXRlcihzKWApDQpgYGANCg0KDQpcDQpcDQoNCuuLpOydjOydgCDruYTti4DspojsnZgg64W4656Y66W8IOuIhOqwgCDsnpHqs6HtlojripTsp4Ag7JWM7JWE67O07J6QLg0KYGBge3J9DQp0YWxseSh+c29uZ3dyaXRlcnMsIGRhdGEgPSBzb25ncykgJT4lDQogIHNvcnQoZGVjcmVhc2luZyA9IFRSVUUpICU+JQ0KICBoZWFkKCkNCmBgYA0K6rGw7J2YIOugiOuFvOqzvCDrp6TsubTtirjri4jsnbTrr4DroZwg65GQIOyduOusvOydtCDquLDsl6ztlZwg6rOh7J2YIOyImOulvCDtjIzslYXtlbTrs7TsnpAuDQoNClwNCmBgYHtyfQ0KbGVuZ3RoKGdyZXAoIk1jQ2FydG5leSIsIHNvbmdzJHNvbmd3cml0ZXJzKSkNCmxlbmd0aChncmVwKCJMZW5ub24iLCBzb25ncyRzb25nd3JpdGVycykpDQpgYGANCg0KXA0KXA0KDQrsoJXqt5zsi53snYQg7Zmc7Jqp7ZWY7JesIOuRkCDsgqzrnozsnbQg6rO164+Z7J6R7JeF7ZWcIOqzoeyXkCDrjIDtlbQg7JWM7JWE67O8IOyImOuPhCDsnojri6QuDQpgYGB7cn0NCnNvbmdzICU+JQ0KICBmaWx0ZXIoZ3JlcGwoIihNY0NhcnRuZXl8TGVubm9uKS4qKE1jQ2FydG5leXxMZW5ub24pIiwgc29uZ3dyaXRlcnMpKSAlPiUNCiAgc2VsZWN0KFRpdGxlKSAlPiUNCiAgaGVhZCgpDQpgYGANCg0KXA0KXA0KDQrruYTti4DspojripQg66y07JeH7JeQIOuMgO2VtCDrhbjrnpjtlojsnYTquYw/IOuFuOuemCDsoJzrqqnsnLzroZwgY29ycHVz66W8IOunjOuTpOqzoCDrtojsmqnslrQg7KCc6rGwIOuwjyB0Zi1pZGbroZwgZHRt7J2EIOunjOuTpOyWtOyEnCDslYzslYTrs7TslZjri6QuDQpgYGB7cn0NCnNvbmdfdGl0bGVzIDwtIFZDb3JwdXMoVmVjdG9yU291cmNlKHNvbmdzJFRpdGxlKSkgJT4lDQogIHRtX21hcChyZW1vdmVXb3Jkcywgc3RvcHdvcmRzKCJlbmdsaXNoIikpICU+JQ0KICBEb2N1bWVudFRlcm1NYXRyaXgoY29udHJvbCA9IGxpc3Qod2VpZ2h0aW5nID0gd2VpZ2h0VGZJZGYpKQ0KDQpmaW5kRnJlcVRlcm1zKHNvbmdfdGl0bGVzLCA3KQ0KYGBgDQoNClwNClwNClwNCg0KIyMjIDE1LjMuMi4gU2NyYXBpbmcgZGF0YSBmcm9tIHR3aXR0ZXINCu2KuOychO2EsOyXkOuKlCDqs6Drj4TsnZggQVBJ6rCAIOyCrOyaqeuQnOuLpC4gdHdpdHRlUiDtjKjtgqTsp4DroZwg7J2065+s7ZWcIOuNsOydtO2EsOyXkCDsl5HshLjsiqTtlaAg7IiYIOyeiOuLpC4gQVBJ7IKs7Jqp7J2AIHNldHVwX3R3aXR0ZXJfb2F1dGgoKSDtlajsiJjrpbwg7IKs7Jqp7ZWY6rOgIOqzhOygleqzvCDqsJzsnbgga2V566W8IOyEpOygle2VtOyVvCDtlZzri6QuDQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeSh0d2l0dGVSKQ0KDQpzZXR1cF90d2l0dGVyX29hdXRoKGNvbnN1bWVyX2tleSA9ICJ1MlV0aGpiSzZZSHlRU3A0c1BrNnlqc3VWIiwNCiAgICAgICAgICAgICAgICAgICAgY29uc3VtZXJfc2VjcmV0ID0gInNDNG1qZDJXTUU1bkgxRm9XZVNUdVN5N0pDUDVESGpOdFRZVTFYNkJ3UTF2UFowajN2IiwNCiAgICAgICAgICAgICAgICAgICAgYWNjZXNzX3Rva2VuID0gIjEzNjU2MDY0MTQtN3ZQZlB4U3RZTnE2a1dFQVRRbFQ4SFpCZDRHODNCQmNYNFZvUzlUIiwNCiAgICAgICAgICAgICAgICAgICAgYWNjZXNzX3NlY3JldCA9ICIwaEpxOUtZQzNlQlJ1WnpKcVNhY210SjRQTko3dE5Ma0dyUXJWbDAwSkhpcnMiKQ0KYGBgDQoNClwNCg0K7ZW07Iuc7YOc6re466W8IOyCrOyaqe2VmOyXrCDtirjsnJcg66qp66Gd7J2EIOqygOyDie2VoCDsiJgg7J6I6rKMIOuQmOyXiOuLpC4gI2RhdGFzY2llbmNl6rCAIO2PrO2VqOuQnCDth7Tqt7wgMSwwMDDqsJwg7Yq47JyX7J2EIOqygOyDie2VtOuztOyekC4NCmBgYHtyfQ0KdHdlZXRzIDwtIHNlYXJjaFR3aXR0ZXIoIiNkYXRhc2NpZW5jZSIsIGxhbmcgPSAiZW4iLCBuID0gMTAwMCwNCiAgICAgICAgICAgICAgICAgICAgICAgIHJldHJ5T25SYXRlTGltaXQgPSAxMDApDQoNCmNsYXNzKHR3ZWV0cykNCmNsYXNzKHR3ZWV0c1tbMV1dKQ0KYGBgDQoqKu2KuOychO2EsOuKlCDtirjsnJfsnYQgSlNPTiDsmKTruIzsoJ3tirjroZwg7KCc6rO17ZWY66+A66GcLCB0d2l0dGVS7J2YIHR3TGlzdFRvREYg6riw64ql7J2EIO2Gte2VtCDrjbDsnbTthLDtlITroIjsnoTsnLzroZwg67OA7ZmY7ZWc64ukLioqDQoNClwNCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQp0d2VldF9kZiA8LSB0d0xpc3RUb0RGKHR3ZWV0cykgJT4lIGFzLnRibCgpDQp0d2VldF9kZiAlPiUNCiAgc2VsZWN0KHRleHQpICU+JQ0KICBoZWFkKCkNCmBgYA0KDQpcDQpcDQoNCuqwgOyguOyYqCDtirjsnJfsnZgg66y47J6QIOyImCDrtoTtj6zrpbwg7JWM7JWE67O07JWY64ukLg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmdncGxvdChkYXRhID0gdHdlZXRfZGYsIGFlcyh4ID0gbmNoYXIodGV4dCkpKSArDQogIGdlb21fZGVuc2l0eShzaXplID0gMikgKw0KICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQgPSAxNDApICsNCiAgc2NhbGVfeF9jb250aW51b3VzKCJOdW1iZXIgb2YgQ2hhcmFjdGVycyIpDQpgYGANCg0KXA0KXA0KDQrrmJDtlZwg66as7Yq47JyXIOyImOydmCDrtoTtj6zrj4Qg67O8IOyImCDsnojri6QuDQpgYGB7cn0NCmdncGxvdChkYXRhID0gdHdlZXRfZGYsIGFlcyh4ID0gcmV0d2VldENvdW50KSkgKw0KICBnZW9tX2RlbnNpdHkoc2l6ZSA9IDIpDQpgYGANCuuMgOu2gOu2hOydmCDtirjsnJfsnbQg66as7Yq47JyX7J20IOunjuydtOuQmOyngCDslYrripQg66qo7Iq17J2064ukLg0KDQpcDQpcDQpcDQpcDQpcDQo=