Nuxt.js|スクレイピング

instagramの情報をスクレイピングして自動で整理できないかと目論んで、調べてみた。

結果としてinstagramのウェブページからはスクレイピングはできなかったが、スクレイピングの手法だけでもメモとして残す。

Nuxtプロジェクト作成

スクレイピングテスト用のnuxtのプロジェクトを作成

create-nuxt-appの初期設定の中で、modulesにAxiosを明示的に指定しておく。

? Nuxt.js modules: Axios

puppeteer

node.jsではpuppetterというライブラリを使用するのがいいらしい。apiでヘッドレスでブラウザを呼び出してウェブページからその内容を取得するイメージ。

$ npm i puppeteer

API実装

Apiを実装するためのサーバーミドルウェアとしてExpressを使用する。

実は書いててよくわかってはいないけれど、APIを呼び出したりなどサーバとのやり取りを担当する仕組みの部分が必要で、NuxtだとExpressをいうライブラリを介してAPIを実現する。

nuxt.config.jsに以下を追加

export default {
  ...
  ,
  serverMiddleware: {
    '/api': '~/api'
  }
}

上記により/apiを叩いたとき~/api/index.jsを参照するようになる。

api/index.jsと、実処理を担当するapi/scraping.jsを作成。

$ mkdir api
$ touch api/index.js api/scraping.js

それぞれのファイルの中身は以下の通り。

api/index.js(参考サイトのソースほぼそのまま)

const app = require('express')()
const scraping = require('./scraping')

app.get('/get_posts', async(req, res) => {
  const image = await scraping.getPosts()
  res.send(image)
})

module.exports = {
  path: '/api',
  handler: app
}

api/scraping.js(このブログの記事のタイトル一覧を取得するよう記述)

const puppeteer = require('puppeteer')

async function getPosts() {
  const browser = await puppeteer.launch({
    args: [
      '--no-sandbox',
      '--disable-dev-shm-usage'
    ]
  })
  const page = await browser.newPage()
  await page.goto("http://blog.wald-grun.biz",{waitUntil: 'domcontentloaded'})
  const image = await page.evaluate(() => {
    const posts = document.getElementsByClassName("c-entries__item")
    const listcount = posts.length
    const list = []
    for(i=0;i<listcount;i++) {
        list.push(posts[i].getElementsByClassName("c-entry-summary__title")[0].textContent)
    }
    return list
  })
  return image
}

module.exports = {
  getPosts
}

page.evaluateでページの内容を取得している。

取得にあたっては通常のjsの記述を使う。今回は.c-entries__itemを持った要素(1記事)のリストを取得し、その後にループでタイトル一覧の配列にして返している。

この辺は初見で適当に書いたけど、要素を取得して配列にする記述とかありそうだから要勉強。

フロント側実装

index.vue

<template>
  <div class="container">
    <ul>
      <li v-for="(item,index) in src" :key="index">
        {{ item }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      src: ""
    }
  },
  methods: {
    async showBird() {
      this.src = await this.$axios.$get("/api/get_posts")
    }
  },
  mounted() {
    this.showBird()
  }

}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}

.title {
  font-family:
    'Quicksand',
    'Source Sans Pro',
    -apple-system,
    BlinkMacSystemFont,
    'Segoe UI',
    Roboto,
    'Helvetica Neue',
    Arial,
    sans-serif;
  display: block;
  font-weight: 300;
  font-size: 100px;
  color: #35495e;
  letter-spacing: 1px;
}

.subtitle {
  font-weight: 300;
  font-size: 42px;
  color: #526488;
  word-spacing: 5px;
  padding-bottom: 15px;
}

.links {
  padding-top: 15px;
}
</style>

api/get_postsをaxiosでgetし、取得した値をv-forで展開。

無事に表示されました。

instagramはうまくいかない

さて、上記のやり方でinstagramの内容を取得しようとしたところ、まずログインが必要でした。

調べたところ、まずログインの手順を踏んでcookieを取得し、そのcookieを使ってログイン状態としてページを参照する、という流れらしい。

cookie取得

ログインしてcookieを取得するまでは試しました。(成功してませんが)

dotenvの導入

まず事前に、環境変数的にプロジェクトごとの.envで定数を管理するためのライブラリを導入。

$npm install @nuxtjs/dotenv

.envファイルを用意して設定値を記述

.env(インスタのアカウント等々)

ACCOUNT_NAME=kng_s
ACCOUNT_PASS=XXXXXXXXX
LOGIN_URL=https://www.instagram.com/accounts/login/

cookieを取得するjsを用意。

getCookie.js

require('dotenv').config();

const puppeteer = require('puppeteer');
const fs = require('fs');

const ACCOUNT_NAME = process.env.ACCOUNT_NAME;
const ACCOUNT_PASS = process.env.ACCOUNT_PASS;
const LOGIN_URL = process.env.LOGIN_URL;

const requireEnvs = [
    ACCOUNT_NAME,
    ACCOUNT_PASS,
    LOGIN_URL
];

console.log(requireEnvs)

const COOKIES_PATH = 'token/cookies.json';

// entry point
(async () => {
    for (let requireEnv of requireEnvs) {
        if (requireEnv == undefined) {
            console.log('local env is not set.');
            return;
        }
    }
    console.log('try to get cookie...');

    const browser = await puppeteer.launch({ headless: true });
    const page = await browser.newPage();

    // パスワードを使ってログイン
    await page.goto(LOGIN_URL, {waitUntil: 'networkidle0'});
    await page.type('input[name="username"]', ACCOUNT_NAME);
    await page.type('input[name="password"]', ACCOUNT_PASS);
    page.click('button[type="submit"]');
    await page.waitForNavigation({timeout: 60000, waitUntil: 'domcontentloaded'});

    // Cookieをローカルに保存
    const afterCookies = await page.cookies();
    fs.writeFileSync(COOKIES_PATH, JSON.stringify(afterCookies));

    browser.close();
    console.log('success!');
})();

こちらをnodeで実行するとcookieが取れるはず…なのですが、instagramだとログインできず。うまくいきませんでした。

$ node getCookie.js

inputoのNodeが存在しないというエラーになるのですが、instagramは基本的にすべて動的にコンテンツ生成がされており、微妙なタイミングなどでNodeが取得できなかったりする?のかと解釈しています。

こちらも要勉強です。

取得後の流れは参考サイトを参照。

参考にしたページ