Nuxt.jsとLaravelでスクレイピングアプリを作った

※スクレイピングが禁止されているサイトも存在します。

当記事を参考にされる場合は、しっかりと利用規約等を確認してから実施するようにしてください。

完成ページ

https://demo-scraping-app.mgk-design.net/

GitHub : https://github.com/gkNorth/demo-scraping-app


目的

  • Nuxt.js + Laravelの学習
  • 情報収集の効率化

環境

  • MacOS
  • Node.js v16.13.0
  • Nuxt.js v2.15.8
  • create-nuxt-app v4.0.0
  • Laravel v8.77.1
  • MAMP(api確認用)

概要

  • Laravel
    • 登録されたサイトをスクレイピング(Demo版ではQiitaの記事を取得)
    • apiで上記データのjsonを返す
  • Nuxt.js
    • axiosでapiを叩く
    • タブ形式で表示する

※Node.jsでもスクレイピングはできますが、対応できるサーバーは運用コストが高いことなどからLaravelを選択しました。

※ミニマルな設計のためVue.jsのみで十分でしたが、学習のためNuxt.jsを利用しています。

機能

  • WEBページへアクセスする度に登録したサイトの最新情報が表示される
  • サイトは無制限に登録できる

ディレクトリ構成

root/
 ├ api    //Laravel
 └ front  //Nuxt.js

※今回は学習の為、LaravelとNuxt.jsを分けていますが、LaravelにNuxt.js(vue)を追加することもできます。


使用する主要プラグイン

  • Laravel
    • Goutte
  • Nuxt.js
    • axios
    • sass

構築フロー

  1. 実装準備
  2. Laravelのインストール
    1. composer create-project laravel/laravel api
    2. composer require fabpot/goutte
  3. Nuxt.jsのインストール
    1. npx create-nuxt-app front
    2. npm install -D sass-loader@10.0.5 sass
  4. バックエンド構築
  5. スクレイピング実装
    1. ScrapingController.php
  6. ルーティング設定
    1. api.php
  7. フロントエンド構築
    1. axios実装
    2. テンプレート実装
    3. スタイル調整
    4. npm run build
    5. npm run generate

構築詳細

実装準備

Laravelのインストール

  1. composer create-project laravel/laravel api
  2. root直下にapiというディレクトリ名のLaravelプロジェクトが生成される
  3. composer require fabpot/goutte
  4. スクレイピングの為、今回はGoutte(グート)を採用した。
  5. https://github.com/FriendsOfPHP/Goutte

Nuxt.jsのインストール

  1. npx create-nuxt-app front
  2. root直下にfrontというディレクトリ名のNuxt.jsプロジェクトが生成される
  3. コマンド実行後の回答は以下の通り
    1. Programming language: JavaScript
    2. Package manager: Npm
    3. UI framework: None
    4. Nuxt.js modules: Axios - Promise based HTTP client
    5. Linting tools: 選択しないでエンター
    6. Testing framework: None
    7. Rendering mode: Single Page App
    8. Deployment target: Static (Static/Jamstack hosting)
    9. Development tools: 選択しないでエンター
    10. What is your GitHub username?: 変更せずエンター
    11. Version control system: Git
  4. npm install -D sass-loader@10.0.5 sass

webpackのバージョンとの兼ね合いでsass-loaderの最新ではエラーが出る為、エラーが出ないバージョンを指定。

バックエンド

サーバーサイドでスクレイピングし、取得したデータをjsonとして返すAPIを構築する

  1. スクレイピング実装

$php artisan make:controller ScrapingController

上記コマンドを実行し、コントローラーを生成する

コードは下記の通り

ScrapingController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Goutte\Client;
use Symfony\Component\HttpClient\HttpClient;

class Site {
  public $site_title;
  public $url;
  public $page_titles;
  public $site_links;
  public $page_links;

  // constructor
  public function __construct($site_title, $domain, $path, $parent, $child, $link_type) {

    $client = new Client(HttpClient::create(['verify_peer' => false, 'verify_host' => false]));

    $this->site_title = $site_title;
    $this->domain = $domain;
    $this->path = $path;
    $this->parent = $parent;
    $this->child = $child;
    $this->link_type = $link_type;

    $crawler = $client->request('GET', $domain.$path);

    $titles = $crawler->filter($parent)->each(function ($li) {
      if ($li->filter($this->child)->count()) {
        $title = $li->filter($this->child)->text();
        return $title;
      }
    });

    $links = $crawler->filter($parent)->each(function ($li) {
        if ($li->filter($this->child)->count()) {
          $link = $li->filter($this->child)->attr('href');
          if ($this->link_type == 1) {
            $link = $this->domain.$link;
          }
          return $link;
        }
    });

    $this->page_titles = $titles;
    $this->page_links = $links;
    $this->site_links = $domain.$path;
  }
}

class ScrapingController extends Controller
{
  public function scraping()
  {
      $sites = [
        new Site('Qiita(Trends)', 'https://qiita.com', '', 'article.css-18gxgni', 'h2.css-1fhgjcy a', 0),
        new Site('Qiita(JS)', 'https://qiita.com/tags/javascript', '', 'article', 'h2.css-1fhgjcy a', 0),
        new Site('Qiita(CSS)', 'https://qiita.com/tags/css', '', 'article', 'h2.css-1fhgjcy a', 0),
        new Site('Qiita(Node.js)', 'https://qiita.com/tags/node.js', '', 'article', 'h2.css-1fhgjcy a', 0),
      ];

      return $sites;
  }
}

  1. ルーティング設定

api.phpにルーティングを設定する

api.php

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/scraping', 'App\Http\Controllers\ScrapingController@scraping');

以上でバックエンド側の実装は完了です。

MAMP等を利用しhttp://localhost:8888/api/scrapingにアクセスするとjsonが返されると思います。

フロントエンド

Laravelで構築したAPIのjsonを取得し、SPAで表示する

  1. テンプレート・スタイル・axios実装
<template>
  <div class="item-container">
    <ul class="title-list">
      <li :class="[`tab-${index+1}`, {active: isActiveTab===index+1}]"
          v-for="(site, index) in sites"
          :key="index">
          <a href="#" @click="isActiveTab=index+1">{{ site.site_title }}</a>
      </li>
    </ul>
    <div class="contents-container">
      <div :class="`contents-${index + 1}`"
          v-for="(site, index) in sites"
          :key="`p-${index}`"
          v-show="isActiveTab === index+1">
        <ul class="item-list">
          <li class="item"
              v-for="(item, i) in site.page_titles"
              :key="i">
            <a :href="site.page_links[i]" target="_blank" rel="nofollow noopener noreferrer">{{item}}</a>
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script>
	export default {
		async asyncData({ $axios }) {
			const url = 'http://localhost:8888/api/scraping';
			const response = await $axios.$get(url);
			return {
				sites: response,
        isActiveTab: 1
			};
		}
	};
</script>

<style lang="scss">
* {
  box-sizing: border-box;
}
body {
  background: #d5d5d5;
}
ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
.title-list {
  display: flex;
  overflow: auto;
  width: 100%;
  li {
    margin-right: 0.3rem;
    min-width: calc(100% / 5);
    a {
      align-items: center;
      background: #43546b;
      border-radius: 10px 10px 0 0;
      color: #fff;
      display: flex;
      justify-content: center;
      min-height: 5rem;
      padding: 0.2rem 0.5rem;
      text-align: center;
      transition: background-color 0.1s;
      width: 100%;
    }
    &.active {
      a {
        background: #284061;
      }
    }
  }
}
.contents-container {
  background: #f5f5f5;
}
.item {
  border-bottom: 1px solid #ccc;
  padding: 1rem;
}
</style>

構築した所感

全サイトの新着記事を表示するタブも作りたかった。

更新日の取得処理を共通化するのに時間がかかりそうだったので断念。

時間がある時にまたチャレンジします。

実際に似たようなサービスを作るとなると、CORSやセキュリティの知見を深めるべきだと感じた。