ahrefsのレーティングを日時取得できて、それをどこかにためたい。
下記のUR DRがとりたい。SEOと密接な関係があると言われている。
背景
会社の偉い人に
「DR URを取ってきて、それを貯めつつレポーティングしてほしい」
と言われたのがきっかけ。
毎日ahrefsにログインして見て、どこかに入力するのは面倒だから自動化することに。
やり方
ahrefsはAPIを提供している。やすいプランでも$500(だった気がする)。
UR DRを取得するだけだと流石に高い。
じゃあ、お金がかからない感じがする、クローリングで行きましょう。
今までクローリンングやってきたけど、
コードの可読性、拡張性等が皆無&一々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() をしちゃってるのは良くない。「コネクションをきる」といったような抽象に対してプログラミングした方が良かった。
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;
};
ただのドメイン層のクラス
これは要らなかったかな。
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なので、メンテしやすい。
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パターンかどうかはちょっと指摘が欲しい。
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でも良くなったので、急なレポートライン変更にも耐えうるようになった。
以前よりは、安定運用できる環境を整えられたのかなって思う。