LoginSignup
5
2

More than 5 years have passed since last update.

ES2018のAsyncIterationとfor-await-ofでAPIのページング処理をする

Posted at

はじめに

ES2018について調べてたらfor文で回せるAsyncIterationを知ったので
APIのページング処理ができたらスッキリ書けるのではないかと思って実装してみた

Async Iteration

詳しくは他にわかりやすい解説記事があるのでざっくりと説明すると状態を非同期で返すイテレータそれだけ

ES2018 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つづつに対して処理をするのならかろうじて用途があるのかなって感じです

結論

便利なライブラリがあるので使いましょう

あとがき

ブログとかをページ単位でスクレイピングして保存するのなら、まだ使いみちありそうかな?

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2