RaspberryPi4でpuppeteerを使おうとしたら躓いた。


2020/03/28更新

2020-02-13にbusterが更新されましたね。
改めて「ラズパイ4(buster)+Node-RED+puppeteer」が使用できるか確認してみると、
エラー無く動きました!!!

  • OS
    • Raspbian Buster Lite
    • Version:February 2020
    • Release date:2020-02-13
    • Kernel version:4.19
  • Node.js
    • 12.16.1
  • npm
    • 6.14.4
  • Node-RED
    • 1.0.4
  • puppeteer
    • 1.20.0
  • chromium
    • 78.0.3904.108

下記は一応備忘録として残しておきます。

はじめに

Qiita初投稿です。

記事作成のきっかけ

最近RaspberryPi4を購入し、RaspberryPi3B+で行っていたスクレイピングを移行した際、
ハマりましたので、同じ悩みを持つ人に共有出来ればと思い、投稿させて頂きました。

やりたい事

  • RaspberryPi4
  • Node-RED
    • puppeteer

とにかく上記を用いてスクレイピングを行う環境を構築する。

ハマったこと

https://www.raspberrypi.org/downloads/raspbian/
 ⇒ Raspbian Buster with desktop
 ⇒ Kernel version : 4.19

上記OSを用いて、puppeteerを実行出来ない。
※厳密には実行できるが、意図しない挙動をする。
Raspbian Busterで行える方法があれば、申し訳ありません。
そしてエラーログが残ってないのでログを載せられない。

問題の推測

  • RaspberryPi3B+ stretch
  • RaspberryPiZero stretch

上記の所持しているラズパイでは問題が起きたことが無かったため、Raspbian Busterからの影響度が大きいと決め打ち。

  • nodejs
  • chromium-browser
  • puppeteerなど

上記のバージョンによる影響も大きいかもしれないが、一旦保留とし、OSを変更する作業へ移行する。

結論

そもそもRaspbian Busterが原因なのか分かりませんでしたが、
OSをRaspbian BusterからUbuntu Server19.10.1へ変更したことで、
RaspberryPi4でもpuppeteerが使えました!

下記では、その手順及び注意点を書いていきます。

1.OSの選択

Ubuntu Server 19.10からRaspberryPi4に対応した模様。
ひとまずこれを試します。

https://ubuntu.com/download/raspberry-pi
上記からubuntu-19.10.1-preinstalled-server-arm64+raspi3.img.xzをダウンロードします。
※もし、2019/12/9以前にダウンロードしたイメージがある場合、ダウンロードし直して下さい。
 USBポートが使用できない重大な不具合が含まれています。

インストール作業については割愛させて頂きます。
下記URLを参考にして下さい。
https://qiita.com/NeK/items/849c6d1e05fb05314fc6

初期設定が終わり次第次へ

2.chromium-browserインストール

なんと、chromiumubuntu19.10以降、aptでインストール行なえないみたいです。
snapコマンドを用いてインストールします。

# インストールを行なえるchromiumのバージョン確認
snap info chromium

# chromiumのインストール(stable)
snap install chromium

# インストール場所の確認(puppeteerで使用する)
which chromium

3.Node-REDインストール

https://nodered.jp/docs/getting-started/raspberrypi
公式に記載されているコードを脳死で実行します。

# npmがインストールする必要のあるバイナリコンポーネントをビルドできることを保証するため、build-essentialをインストール
sudo apt-get install build-essential

# Node-REDをインストール
bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)

# ラズパイ起動時に自動起動させる
sudo systemctl enable nodered.service

# 起動の確認
node-red

4.puppeteerインストール

https://pptr.dev/
公式ページの左上、バージョン部分をクリックするとpuppeteerの各バージョンがどのバージョンのChromiumに対応しているか見られます。

# カレントディレクトリの移動
cd ~/.node-red

# 私はchromium 79.0.3945.79 でしたが
# 今回はv1.20.0をインストール
sudo apt install [email protected]

上記でpuppeteerではなく、puppeteer-coreをインストールしているのは、
puppeteerではデフォルトでx86アーキテクチャ用のchromiumが勝手にインストールされる為、puppeteer-coreでインストールしています。

# Node-REDでpuppetter-coreを使用する為に、グローバル変数にpuppeteerを追加
nano ~/.node-red/settings.js
settings.js
    functionGlobalContext: {
        puppeteer:require('puppeteer-core')
        // os:require('os'),
        // jfive:require("johnny-five"),
        // j5board:require("johnny-five").Board({repl:false})
    },

保存後はNode-REDの再起動を行って設定を反映させましょう。

5.Node-REDでpuppeteer実行

ようやくここまで来ました。
お疲れ様でした。
Node-REDのフローの作成方法などは割愛しますが、
おまけとして、スクレイピングのサンプルを示しておきます。
動かなくても怒らないでください...
たぶん、動かないときはセレクターのところを変えたら動きます。

サンプル内容

①googleに接続
②Node-REDと検索
③最上部のページへアクセス(Node-REDの公式ページを想定)
 ⇒ 上手く動かない時があるかもしれません...
④現在のNode-REDの最新バージョンを取得

下記、functionノードの中身を記載。
特に重要な部分(?)は★で囲ってます。

// ★グローバル変数の呼び出し★
var puppeteer = new global.get('puppeteer');

// URL指定
var url = "https://www.google.com/";

(async () => {
  const browser = await puppeteer.launch({
    // ★snapでインストールしたchromiumの場所★
    executablePath: '/snap/bin/chromium', 
    headless: true
  });
    try {
        const page = await browser.newPage()
        await page.setViewport({width: 1200, height: 800})
        // chromeを開く
        await page.goto(url,{waitUntil: "networkidle2"}) // ページへ移動
        // 検索窓に入力
        await page.type('input[name=q]', 'node-red',{ delay: 5 } )
        // 検索
        await Promise.all([
            page.waitForNavigation({waitUntil: "networkidle2"}),
            await page.evaluate(() => {
                document.querySelector('input[value^="Google"]').click();
            })
        ]);
        // var result = await page.evaluate(() => document.querySelector(`#rso > div > div > div:nth-child(1) > div > div > div.r > a > h3 > span`).textContent.trim())
        var result = await page.evaluate(() => document.querySelector(`#rso > div > div > div:nth-child(1) > div > div > div.r > a > h3`).textContent.trim())
        await Promise.all([
            page.waitForNavigation({timeout: 20000, waitUntil: "networkidle2"}),
            // page.click('#rso > div > div > div:nth-child(1) > div > div > div.r > a > h3 > span')
            page.click('#rso > div > div > div:nth-child(1) > div > div > div.r > a > h3')
        ]); 
        result = await page.evaluate(() => document.querySelector('body > div.title > div > div > div > p > a > span').textContent.trim())
        // ★非同期処理の為、後続のノードへの出力はnode.send()を使用する★
        node.send({payload:result}) 
    } catch(e) {
        node.send({payload:`error:${e}`})
    }finally{
        await browser.close()
    }
})();

return msg;

あとがき

ここまで読んでくれた方ありがとうございました。
初めて書きましたが非常に疲れました...。
最後、たぶん力尽きてます。

少しでも誰かの役に立ったなら幸いですが、混乱させただけなら申し訳ありません。
また機会があれば、何か書こうかと思います。