【R】YouTube 「にじさんじ」のライブ配信のチャット(配信終了後)を取得してみる

「にじさんじ」は、ライブ配信を結構やってるので、配信の時のチャットの内容の抽出をしてみる。
結論からいうと、思ったより難しい。APIでサクッと取れると思ったのだが、終了した配信のチャット内容を得るAPIは存在しない。
仕方ないので、スクレイピングで抽出することにした。

前提

前回の記事で作成した関数を使う。

「にじさんじ」のライブ配信した動画の取得


検索のAPIで、eventTypeで、検索対象をブロードキャストイベントに制限出来る。

completed:完了したブロードキャストのみを含める
live:アクティブなブロードキャストのみを含める
upcoming:今後配信予定のブロードキャストのみを含める

keyword <- "にじさんじ"

res_live <- executeYdApi(
 "search",
 params = c(key=api_key, 
            part="snippet",
            q=keyword,
            type="video",
            order="viewCount",
            maxResults=10,
            eventType="completed")
)

動画ページを読み込む

Rでスクレイピングするには、rvestパッケージが便利である。
まず、1位の動画のページを読み込んでみる。

library(rvest)

# UserAgent
UA <- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"

# 1位の動画のページのURL
video_url <- sprintf("https://www.youtube.com/watch?v=%s", res_live$items$id$videoId[1])

ページのソースを見てみると、JavaScriptで作成されているようで少し面倒であるが頑張って読み解く。
JavaScriptで、ytInitialDataという変数の中にチャットの情報が含まれていて、下記のようなURLの情報を読み込んでいる。

https://www.youtube.com/live_chat_replay?continuation=<continuation>

scriptタグの中にあるytInitialDataを定義しているところを特定して抽出。
抽出したテキストをJSON形式で読み込む。
リスト形式でデータが作成されるので、構造を読み取って、continuationを抜き取る。

getInitContinuation <- function(video_url){
 res_html <- html_session(video_url, httr::user_agent(UA))
 scripts <- res_html %>%
   html_nodes("script")
 
 content <- NULL
 for(script in scripts){
   content <- script %>%
     html_text() %>%
     stringr::str_trim()
 
   if(grepl('ytInitialData', content)){
     content <- gsub('var ytInitialData = ', "", content)
     content <- gsub(";", "", content)
     break
   }
 }
 
 yt_init_data <- jsonlite::fromJSON(content)
 
 continuation <- yt_init_data$contents$twoColumnWatchNextResults$
   conversationBar$liveChatRenderer$header$liveChatHeaderRenderer$
   viewSelector$sortFilterSubMenuRenderer$subMenuItems$continuation$
   reloadContinuationData$continuation[1]
 
 sprintf("https://www.youtube.com/live_chat_replay?continuation=%s", continuation)
}
live_chat_url <- getInitContinuation(video_url)
live_chat_url

チャット情報を読み込む

次に、live_chat_replayのページから情報を抽出する。構造に関してはソースで確認。

getChatInfo <- function(live_chat_url){
 res_html <- html_session(live_chat_url, httr::user_agent(UA))
 scripts <- res_html %>%
   html_nodes("script")
 
 content <- NULL
 for (script in scripts) {
   content <- script %>%
     html_text() %>%
     stringr::str_trim()
   
   if (grepl('window\\["ytInitialData"\\]', content)) {
     content <- gsub('window\\["ytInitialData"\\] = ', "", content)
     content <- gsub(";", "", content)
     break
   }
 }
 
 jsonlite::fromJSON(content)
}
chat_info <- getChatInfo(live_chat_url)

チャットのメッセージと次のURLを作成する。

generateChatData <- function(chat_info){
 # 配信開始からの経過時間
 video_offset_time_msec <- chat_info$continuationContents$liveChatContinuation$
   actions$replayChatItemAction$videoOffsetTimeMsec
 # コメント情報
 actions <- chat_info$continuationContents$liveChatContinuation$
   actions$replayChatItemAction$actions
 
 # コメント情報のデータフレーム作成
 chat_df <- bind_rows(lapply(2:length(actions), function(i){
   x <- actions[[i]]
   if(!"addChatItemAction" %in% names(x)){
     return(NULL)
   }
   msg_renderer <- x$addChatItemAction$item$liveChatTextMessageRenderer
   authorName <- msg_renderer$authorName$simpleText
   msg <- msg_renderer$message$runs[[1]]$text
   
   if(is.null(authorName)){
     authorName <- "Unknown"
   }
   
   if (is.null(msg)) {
     msg <- ""
   }
   
   tibble(
     offset_time_msec = video_offset_time_msec[i],
     authorName = authorName,
     comment = msg)
 })) %>%
   filter(comment != "")
 
 # 次のチャット
 next_continuation <- chat_info$continuationContents$liveChatContinuation$continuations$
   liveChatReplayContinuationData$continuation[1]
 list(chat_df=chat_df, next_continuation = next_continuation)
}
chat_data <- generateChatData(chat_info)
chat_data$chat_df %>% select(!authorName)

スクリーンショット 2021-01-22 23.26.54

全体的な処理を実装する

ここまでで、部品は出来たので全体的な処理を実装する。
処理の流れを簡単に述べると、

1 動画ページを読み込み、最初のチャット情報のURLを取得
2 チャット情報のページを読み込み、チャットデータと次のURLを取得
3 次のURLがなくなるまで2を繰り返す。

generateChatDF <- function(video_url){
 # 最初のチャットURLを取得
 live_chat_url <- getInitContinuation(video_url)
 
 i <- 1
 chat_df_list <- list()
 while(T){
   try({
     chat_info <- getChatInfo(live_chat_url)
     chat_data <- generateChatData(chat_info)
     chat_df_list[[i]] <- chat_data$chat_df
   })
   
   if(is.null(chat_data$next_continuation)){
     break
   }

   next_live_chat_url <- 
     sprintf("https://www.youtube.com/live_chat_replay?continuation=%s",
             chat_data$next_continuation)
   
   if(i %% 20 == 0){
     Sys.sleep(1)
   }
   
   if(live_chat_url == next_live_chat_url){
     break
   } else {
     live_chat_url <- next_live_chat_url
     i <- i + 1
   }
 }
 
 bind_rows(chat_df_list)
}

正直、かなりエイヤで書いている。もう少し丁寧に精査したら本記事を修正したい。

実行結果

chat_df <- generateChatDF(video_url)
chat_df %>% 
 sample_n(10) %>%
 select(!authorName)

スクリーンショット 2021-01-22 23.32.17

おわりに

今回は、YouTubeのデータの中でも、APIで取得出来ない「終了したライブ配信動画のチャット]の取得を試してみた。

実行してる範囲ではデータは取れているが、スクレイピングは例外も多く、全てを網羅するのは難しい。
今回紹介した内容も私が確認した範囲では有効だが、ちょっと変わった事をやろうとすると動かないかも知れない。

とはいえ、大まかなところは使えると思うので、実際に試しながら、動かないところがあれば試行錯誤してほしい。
難しければ、ご相談頂ければ可能な限り対応したいと思っている。

この記事が気に入ったらサポートをしてみませんか?