はじめに
ES2018について調べてたらfor文で回せるAsyncIterationを知ったので
APIのページング処理ができたらスッキリ書けるのではないかと思って実装してみたが
Async Iteration
詳しくは他にわかりやすい解説記事があるのでざっくりと説明すると状態を非同期で返すイテレータそれだけ
YouTube Data API
APIはなんでもいいですが、よくあるnextPageTokenなどを使う複雑なYouTubeDataAPI
完了状態がわかりやすい再生リストの動画を取得するPlaylistItems: list を使う
googleapisなどのライブラリを使ってもいいが
AsyncIteratorに相性が良さそうなnode-fetchを使う
APIの仕様
https://www.googleapis.com/youtube/v3/playlistItems
に必要なクエリを載せてGETリクエストを送信
必要なAPIキーを指定するkey
とクエリは欲しいデータを示すpart
と再生リストを指定するplaylistId
、
さらに効率よく取得するためにmaxResults
で1度に帰ってくる動画の数を最大の50にします
すべて取得したい場合は帰ってきたデータにあるpageTokenを指定して再度リクエスト
これをひたすら繰り返し、pageTokenがなくなったら終了
詳しくは公式ドキュメントを参照(日本語)
PlaylistItems: list | YouTube Data API (v3) | Google Developers
実装
APIキーは環境変数APIKEY
に入ってるものとして実装していきます
環境変数を設定するか適宜書き換えてください
APIキーの取得方法はこちら
再生リストはなんでもいいですが、ページング処理の恩恵が分かるように動画数が多い再生リストを
探した結果、GoogleDevelopersにちょうどいいプレイリストがあったのでこれを使います
Google I/O 2018 - All Sessions
https://www.youtube.com/playlist?list=PLOU2XLYxmsIInFRc3M44HUTQc3b_YJ4-Y
playlist?list=
の後に続く文字列が再生リストのIDですね
まずは普通にnode-fetchを使って取得してみます
すべてを表示してしまうとあっという間に文字列だらけになって
見づらくなるのでサンプルは配列の長さのみ出力します
普通に取得
const fetch = require('node-fetch')
const querystring = require('querystring')
const qs = '?' + querystring.stringify({
part: 'snippet',
playlistId: 'PLOU2XLYxmsIInFRc3M44HUTQc3b_YJ4-Y',
maxResults: 50,
key: process.env.APIKEY,
})
fetch('https://www.googleapis.com/youtube/v3/playlistItems' + qs)
.then(res => res.json())
.then(data => console.log(data.items.length))
.catch(err => console.error(err))
これで最初の50件を取得できる
今まではこれ以上必要な場合は以下のように取得する部分を関数で囲って再帰的に実行していました
再帰的に実行して取得する
const fetch = require('node-fetch')
const querystring = require('querystring')
function getListItems(callback, items = [], pageToken) {
const qs = '?' + querystring.stringify({
part: 'snippet',
playlistId: 'PLOU2XLYxmsIInFRc3M44HUTQc3b_YJ4-Y',
maxResults: 50,
pageToken: pageToken,
key: process.env.APIKEY,
})
fetch('https://www.googleapis.com/youtube/v3/playlistItems' + qs)
.then(res => res.json())
.then(data => {
items = items.concat(data.items)
if (!data.nextPageToken) callback(null, items)
else getListItems(callback, items, data.nextPageToken)
})
.catch(err => callback(err))
}
getListItems((err, items) => {
if (err) console.error(err)
else console.log(items.length)
})
再帰的に実行されると知っていれば読めるものの、ぱっと見どう動いてるのかよくわからないので
本題であるfor文(for-await-of)で綺麗に書けそうなAsyncIteratorで実装していきます
asyncIterable & for-await-of で取得する
const fetch = require('node-fetch')
const querystring = require('querystring')
const asyncIterable = {
[Symbol.asyncIterator]() {
let pageToken = ''
const qs = '?' + querystring.stringify({
part: 'snippet',
playlistId: 'PLOU2XLYxmsIInFRc3M44HUTQc3b_YJ4-Y',
maxResults: 50,
pageToken: pageToken,
key: process.env.APIKEY,
})
return {
async next() {
const data = await fetch('https://www.googleapis.com/youtube/v3/playlistItems' + qs)
.then(res => res.json())
.catch(err => console.error(err))
pageToken = data.nextPageToken
return { value: data.items, done: pageToken }
},
}
},
}
!(async () => {
const items = []
for await (const item of asyncIterable) {
items = items.concat(item)
}
console.log(items.length)
})()
for-await-of文で1ページづつ同期的に回せるようになったものの
ページごとになにか処理でもしない限り特に嬉しいことはないですね
なので、取得できたらメモリ上に置いといて、for文で1つづつとりだし
それがなくなったら再度取得するようにしてみます
asyncIterable & for-await-of で1つづつ回す
const fetch = require('node-fetch')
const querystring = require('querystring')
const asyncIterable = {
[Symbol.asyncIterator]() {
let list = []
let pageToken = ''
const qs = '?' + querystring.stringify({
part: 'snippet',
playlistId: 'PLOU2XLYxmsIInFRc3M44HUTQc3b_YJ4-Y',
maxResults: 50,
pageToken: pageToken,
key: process.env.APIKEY,
})
return {
async next() {
if (!list.length && pageToken) {
const data = await fetch('https://www.googleapis.com/youtube/v3/playlistItems' + qs)
.then(res => res.json())
.catch(err => console.error(err))
list = data.items
pageToken = data.nextPageToken
}
return { value: list.shift(), done: pageToken || list.length }
},
}
},
}
!(async () => {
for await (const item of asyncIterable) {
console.log(item.title)
}
})()
動画1つづつに対して処理をするのならかろうじて用途があるのかなって感じです
結論
便利なライブラリがあるので使いましょう
あとがき
ブログとかをページ単位でスクレイピングして保存するのなら、まだ使いみちありそうかな?