8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PuppeteerとLambdaとAPI Gatewayで、ahrefsのUR DRを毎日自動で取得する

Last updated at Posted at 2019-09-11

ahrefsのレーティングを日時取得できて、それをどこかにためたい。

下記のUR DRがとりたい。SEOと密接な関係があると言われている。
スクリーンショット 2019-09-11 18.39.52.png

背景

会社の偉い人に
「DR URを取ってきて、それを貯めつつレポーティングしてほしい」
と言われたのがきっかけ。

毎日ahrefsにログインして見て、どこかに入力するのは面倒だから自動化することに。

やり方

ahrefsはAPIを提供している。やすいプランでも$500(だった気がする)。
UR DRを取得するだけだと流石に高い。

ダウンロード.png

じゃあ、お金がかからない感じがする、クローリングで行きましょう。

今までクローリンングやってきたけど、

コードの可読性、拡張性等が皆無&一々EC2で実行して、CSVを渡すツライ運用

してきていて、自分の精神の安定運用が課題だったので、せっかくなら運用しやすいようにやる。

開発環境

ahrefsクローリングするにはヘッドレスブラウザ必要だよね。じゃあ使い慣れたPuppeteer使っとこ。 -> node

Puppeteer使ってクローリングするだけならLambdaでいいよね。レーティングを見たいドメインの数って10サイトくらいか。出力先は、DBでもSlackでもスプレッドシートでもいいように、API作っちゃお。 -> Lambda × API Gateway

httpリクエスト起点でLambda上のクローラーを実行して、UR DRを取得する感じでいくことに。
LambdaとAPI連携は下記の記事がわかりやすかった。

API GatewayとLambdaでAPI作成のチュートリアル

できたもの

ソースコード

Lambda Layer

$ mkdir nodejs && cd nodejs
$ npm -i chrome-aws-lambda puppeteer-core
$ cd ..
$ zip -r modules.zip nodejs

上記のようにして、出来上がったzipファイルをLambdaのLayerとして定義。
簡単に説明すると、ライブラリなどの共通コンテンツを切り出しておけます。

詳しくはこの記事

Lambda関数

index.jsが外部のライブラリに依存してないのが良い感じ。
受け取ったパラメーターを使って、レーティングのデータを取ってきて返却することだけに関心を持つように。こうすることで、出力先が、ファイルでもメモリでも、DBでも問題なし。
最後の方集中力切れて、puppeteerHandler.closeBrowser() をしちゃってるのは良くない。「コネクションをきる」といったような抽象に対してプログラミングした方が良かった。

index.js
const DomainRepository = require('./domainRepository')
const PuppeteerHandler = require('./puppeteerHandler')

exports.handler = async (event, context) => {
  const domains = event['domains'].split(',')
  let results = [];
  
  const puppeteerHandler = new PuppeteerHandler()
  await puppeteerHandler.launchBrowser()
  
  const domainRepository = new DomainRepository(puppeteerHandler)
  
  try {
    for(let i = 0; i < domains.length; i++) {
      results.push(await domainRepository.fetch(domains[i]))
    }
  } catch (error) {
    return context.fail(error);
  } finally {
    await puppeteerHandler.closeBrowser()
  }
  
  return results;
};

ただのドメイン層のクラス
これは要らなかったかな。

domain.js
class Domain {
    constructor(name, ur, dr) {
        this.name = name
        this.ur = ur
        this.dr = dr
    }
    
    name() {
        return this.name
    }
    
    ur() {
        return this.ur
    }
    
    dr() {
        return this.dr
    }
}

module.exports = Domain;

割と大きくなっちゃているクラス。ahrefsの仕様に対してかなり具体的な実装をしている。
レビューがほしい部分。
とはいえ、今後継続的に運用していくにあたり、ahrefsの仕様が変わってもここを修正すればokなので、メンテしやすい。

pupeteerHandler.js
const chromium = require('chrome-aws-lambda');
const puppeteer = require('puppeteer-core');

class PuppeteerHandler {
    constructor() {
    }
    
    async fetch(domain) {
        const url = `https://ahrefs.com/site-explorer/overview/v2/prefix/live?target=${domain}`
        await this.page.goto(url, { waitUntil: 'networkidle2' });
        const ur = await this.page.$eval('#UrlRatingContainer span', item => {
            return item.textContent;
        });
        const dr = await this.page.$eval('#DomainRatingContainer span', item => {
            return item.textContent;
        });
        return { 'domain': domain, 'ur': ur, 'dr': dr }
    }
    
    async launchBrowser() {
        const browser = await puppeteer.launch({
          args: chromium.args,
          defaultViewport: chromium.defaultViewport,
          executablePath: await chromium.executablePath,
          headless: chromium.headless,
        });
        this.browser = browser
        this.page = await browser.newPage()
        await this._login('https://ahrefs.com/user/login', process.env.EMAIL, process.env.PASS)
    }
    
    async closeBrowser() {
        if(this.browser !== null) {
            await this.browser.close();
        }
    }
    
    async _login(url, id, pass) {
        await this.page.goto(url, { waitUntil: 'networkidle2' });
        await this.page.waitFor('form#formLogin', {timeout: 5000});
        await this.page.type("#email_input", id);
        await this.page.type('input[name="password"]', pass);
        const buttonElement = await this.page.$('#SignInButton');
        await buttonElement.click();
        await this.page.waitFor('body#dashboard', {timeout: 5000});
    }
}

module.exports = PuppeteerHandler;

これは渡すhandlerを変えれば、APIだろうが、スクレイピングだろうが、DBだろうが対応できるようになってる(気がする)。Repositoryパターンかどうかはちょっと指摘が欲しい。

domainRepository.js
const Domain = require('./domain');

class DomainRepository {
    constructor(handler) {
        this.handler = handler
    }
    
    async fetch(domain) {
        const data = await this.handler.fetch(domain)
        return new Domain(data['domain'], data['ur'], data['dr'])
    }
}

module.exports = DomainRepository;

エンドポイント

https://hogehoge.amazonaws.com/ahrefs/ratings?domains=qiita.com,facebook.com
domainsにafrefsレーティングを知りたいドメインを入れる。

レスポンスにDR URが入ってるので幸せ。

これで、レポーティング先が、スプレッドシートでもSlackでも良くなったので、急なレポートライン変更にも耐えうるようになった。
以前よりは、安定運用できる環境を整えられたのかなって思う。

8
6
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
8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?