PHPでも動的サイトをスクレイピングしたい夜

#　はじめに

これはジーズアカデミーAdvent Calendar 2020の記事です！

# 序章 -俺はスクレイピングやるぜ、PHPで。-

だいたいスクレイピングって言うと、相場は`python`って決まってます。
けど、普段PHPを触っているボクは、PHPでスクレイピングしたいんです。

で、PHPでスクレイピングって言うとだいたい`phpQuery`というものが言及されがちです。
確かに、jQueryみたいに操作できるのでかんたんです。
けど、これ**動的サイトにはうまく利用できない**んですね。

だから、他の方法が必要になってきます。

## 今回の対象。
某海外の買い物サイト（一応名前は伏せます。）
以下の様に
1. 検索
2. 一覧表示（一部だけ）
3. 下にスクロールするとスクロールした分だけ情報を取得する
という動きです。
![scraping.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307585/61756928-98bd-b4f9-1fa7-450348f9edbe.gif)



最初に、すべての情報が取得されて一覧表示されたらphpQueryでもいけるのですが、
コレだとできません。

## 対処方法
以下のツール利用で行きます。

- selenium-server-standalone
  - seleniumはのWebブラウザの操作を自動化してくれるやつ。 
- Selenium Google Chrome Driver
  - seleniumで、Chromを動かせるようにするやつ。
- facebook-webdriver
  - facebook-webdriverで上記の自動化をPHPを用いてできる様にするやつ

という感じです。

で、今回やることのイメージは、
1. seleniumのブラウザで、スクレイピングしたいサイトにアクセス。
2. 情報を取得したいページに行って、一旦一番下までスクロールさせる（スクロールすれば、全部の情報が表示されている）
3. 一番下までスクロールされた = スクレイピングしたい情報がすべて表示されているので、それをスクレイピング！！！！

*なんというハック！*

## ツールの準備

### 今回の作業用ディレクトリに移動
```Shell
$ mkdir selenium
$ cd selenium
```


### selenium-server-standaloneをインストール。
`homebrew`を利用。

```bash
brew install selenium-server-standalone
```

### Selenium Google Chrome Driverをダウンロード
[リンク](https://sites.google.com/a/chromium.org/chromedriver/)
[多分ここにあるかも↓](https://chromedriver.storage.googleapis.com/2.38/chromedriver_mac64.zip)

ダウンロードしたら、seleniumディレクトリにぶち込む。

### facebook-webdriverダウンロード
```Shell
$ php composer.phar require facebook/webdriver
```
もし、`Could not open input file: composer.phar`とかの場合は、以下の参考になるかも。

- https://www.kakistamp.com/entry/2019/07/13/003852

### サーバー起動
`$ selenium-server -port 4444 &`

```Shell
$ selenium-server -port 4444 &
[1] 33415
C02SY1XFGTFJ:selenium username$ 18:04:14.512 INFO [GridLauncherV3.launch] - Selenium build info: version: '3.11.0', revision: 'e59cfb3'
18:04:14.513 INFO [GridLauncherV3$1.launch] - Launching a standalone Selenium Server on port 4444
2018-05-24 18:04:14.624:INFO::main: Logging initialized @441ms to org.seleniumhq.jetty9.util.log.StdErrLog
18:04:14.882 INFO [SeleniumServer.boot] - Welcome to Selenium for Workgroups....
18:04:14.882 INFO [SeleniumServer.boot] - Selenium Server is up and running on port 4444
```
で、下のようになればおｋ

```Shell
$ jobs
[1]+  Running                 selenium-server -port 4444 &
```

## PHPファイルの用意

試しに、`test.php`を用意。

```php
<?php
require './vendor/autoload.php';

use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverExpectedCondition;

// 今回の買い物サイト用に準備する変数。
// 検索したいワードを$wordに入れる。この例だとamazonと検索した場合
$word = 'amazon';
$title = " ********** "; // ここはhtmlのtitleに表示される文言を記述。そのサイトによって異なるので、一旦ブラウザでアクセスして確認してください。

// chrome利用用意
$options = new ChromeOptions();
// headlessモードで対応
$options->addArguments(['--headless']);
// ブラウザのサイズを指定
$options->addArguments(["window-size=1024,2048"]);

$host = 'http://localhost:4444/wd/hub';
$capabilities = Facebook\WebDriver\Remote\DesiredCapabilities::chrome();
$capabilities->setCapability(ChromeOptions::CAPABILITY, $options);
$driver = Facebook\WebDriver\Remote\RemoteWebDriver::create($host, $capabilities);
//スクレイピングしたいアドレスを入力
$driver->get('https://www.******/');
$browserLogs = $driver->manage()->getLog('browser');
// 検索したい語をsendKeys()に入れて、submitする。ここの'q'はサイトによって異なる。
$element = $driver->findElement(WebDriverBy::name('q'));
$element->sendKeys($word);
$element->submit();

// このサイトでは、商品画像を全部表示するため、画面を少し右に移動。場合によっては不要。Y軸を動かしたい場合は、第2引数の数字を変える。
$driver->executeScript("window.scrollTo(300, 0);");
$driver->wait(15)->until(
    WebDriverExpectedCondition::titleIs($title)
);

// もし、$title（htmlの<title>が想定どおりじゃない）がなければ、なにかした失敗していると判断
if ($driver->getTitle() !== "$title") {
    throw new Exception('fail');
}

// 以下取得したい要素を指定してあげる。
// 今回は、検索結果のURL、写真、名前を取得したいので以下の用になっている。
// cssSelectorはサイトに寄って異なる。cssSelectorの中身を変えてね。
$itemUrls = $driver->findElements(WebDriverBy::cssSelector('div.c2iYAv > div.cRjKsc > a'));
$photos = $driver->findElements(WebDriverBy::cssSelector('.c5TXIP .c2iYAv .cRjKsc .c1ZEkM'));
$productNames = $driver->findElements(WebDriverBy::cssSelector('.c5TXIP .c3KeDq .c16H9d'));

$items = [];
// 商品あるかチェック
if (count($photos) < 0) {
    throw new Exception('no item.');
}

//。今回は上から１０件だけ取得して、情報を$item配列に格納する。
foreach ($itemUrls as $k => $v) {
    if ($k === 10) {
        break;
    }
    $items[$k]['$itemUrl'] = $v->getAttribute('href');
}
foreach ($photos as $k => $v) {
    if ($k === 10) {
        break;
    }
    $items[$k]['photoUrl'] = $v->getAttribute('src');
}
foreach ($productNames as $k => $v) {
    if ($k === 10) {
        break;
    }
    $items[$k]['titleName'] = $v->getText();
}

print_r($items);
echo "\n";

// ↓コメントアウト外せば、どこをスクレイピングしているかとスクリーンショットで確認できる↓
// $file = "sumple_chrome.png";
// $driver->takeScreenshot($file);
$driver->close();
```

※selenium-webdriverの使い方はこちら。
[リンク](https://gist.github.com/aczietlow/7c4834f79a7afd920d8f)

かけたら、
`$ php test.php`
で実行。

いい感じにできました。

![スクリーンショット 2020-12-30 0.06.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307585/bc8a041b-34f8-0e97-0b56-449ec2a088ee.png)

## 終了したらkillする。
```Shell
$ jobs
[1]+  Running                 selenium-server -port 4444 &  (wd: ~/selenium)
$ kill %1
$ jobs
[1]+  Exit 143                selenium-server -port 4444  (wd: ~/selenium)
```

## まとめ
PHPでも動的サイトをスクレイピングできるんだよ！