[Node.js] PuppeteerでAmazonアソシエイトのURL生成


2020/12/02

Amazonのサイトから製品比較のサイトを構築するときには、Amazonのアソシエイトツールバーで生成したテキストリンクは概ね次のように長いクエリ文字を繋いでとても長々しいURLになってしまっています。

            
            https://www.amazon.co.jp/dp/B01AJ7DH9O/ref=as_li_ss_tl?ie=UTF8&linkCode=ll1&tag=hogepiyo000-11&linkId=0123456789abcdef0123456789abcdef&language=ja_JP
        
これだと長くて貼り付けるのが嫌だという方のために、以下のような短縮URL機能が利用できるようになっています。

            
            https://amzn.to/3fVmrD5
        
たしかにこれだと貼り付けるのも楽なのですが、URL文字列からASINも分からないようになってしまいます。

後々で「コレは何の商品リンクだっけ?」と元のリンク先の商品を手繰るにとても手間がかかります。

もっとスマートなリンクの貼り方はないものかと色々と調べたところ、Amazon.co.jpの特定の商品に対してシンプルなテキストリンクを作成したい場合、公式の
「ヘルプ > アソシエイトリンクの作成方法 > Amazon.co.jpの特定の商品へのシンプルなテキストリンクはどのように作ればよいですか。」で述べられているように以下のURLフォーマットでも良いようです。

            
            https://amazon.co.jp/dp/ASIN/ref=nosim?tag=あなたのアソシエイトID
        
これならばURL短縮形ほど短くはないものの、後々URLのリンクを見ても商品のASINが確認出来ています。

この点で、いいとこ取りのリンクフォーマットになっていると思います。

特に自分のサイトで商品紹介をする場合、amazonのアフィリエイトリンクURLと画像の取得をブラウザから手で一つ一つ張ったり保存したりするのがそろそろ野暮ったく感じるようになってくるようになります。

ここは一つ
puppeteerによるスクレイピングツールでnodeスクリプト化して自動取得してみようと思い立ちました。

今回はpuppeteerを使ってAmazonアフィリエイトURLと関連の画像と説明文を同時にローカルに保存してくれるだけのスクリプトを作成する際の勘どころを解説してみます。


Pupperteerの動作準備

以前、pupperteerの導入方法と基本的な使い方を特集しておりました。

今回もDockerコンテナ上からヘッドレスChromeを動かすので、インストール手順などを知りたい方はこちらのリンクから導入方法〜動作確認をまずご確認ください。


shelljsとargparseのインストール

今回はnodejsのスクリプトをツール風に仕立てるため、nodejs側から各種shellコマンドを呼び出せるshelljsと、node実行時にコマンドオプションとして引数を与えることができるargparseの2つユーティリティパッケージを導入します。

fs、httpなどのnodejsのネイティブなユーティリティだけでもオンライン上からのファイルを読み書きは可能ですが、画像を特定のurlから引っ張ってきてローカルにバイナリ保存するプログラムをわざわざ実装するのは面倒です。

Alpineのdockerイメージではもともとwgetが使えますので、shelljsからwgetを呼び出してファイルを取得・保存する方法を取るのがより効率的なやり方かと思います。

プロジェクトへのインストールは以下のようにするだけです。

            
            $ yarn add shelljs argparse -S
$ yarn add @types/shelljs @types/argparse -D
        
typescriptから使うので定義ファイルをdevdependenciesに追加しておきます。


Amazonの商品ページ操作例① ~ 物販系

Amazonの商品ページでは、取り扱われている商品のカテゴリーのよって微妙にhtmlの構造が違いますので、カテゴリー別にページの構造をよく解析しながらpuppeteerの要素セレクタでゴリゴリとスクレイピングしていくスタイルになります。

まずはAmazonの商品ページで一番取扱の多いであろう物販品のページフォーマットからやってみます。

ASINコードの探し方

何はともあれ商品のASINコードは最低限見つけてこないといけませんので、お目当ての商品をAmazonのページで探ります。

例えば会議用のwebカメラを紹介してみたい場合、商品ページのURLアドレスの
dp/以降にある10桁くらいの文字列がASINコードとなります。(以下の図参照。)

合同会社タコスキングダム|蛸壺の技術ブログ

ここからこの商品のASINコードが
B08HQXVL9Yと分かります。

ASINコードが分かると、puppeteerでアクセスする商品ページのアドレスも決まり、この例では
https://amazon.co.jp/dp/B08HQXVL9Yから色々とスクレイピングできるようになります。

商品タイトルを取得

まずは商品ページ(https://amazon.co.jp/dp/B08HQXVL9Y)にあるタイトルだけ取得して、テキストファイルに保存させましょう。

取得させたい要素の解析はブラウザ上で行います。今回はChromeを使いますが、どのブラウザでも基本的には同様のことが行えると思います。

まず、puppeteerで取り込むターゲット要素の上で右クリックし、
[検証]を選択すると開発者ツールのElementタブに選択したい要素の詳細は表示されます。

合同会社タコスキングダム|蛸壺の技術ブログ

特に、要素にid属性があるとセレクターの識別クエリの選択は楽になります。

商品のタイトルは全て
id="productTitle"のspan要素ですので、今回は#productTitleとして中身が引っこ抜けます。

では
前回と同様に、プロジェクトのフォルダ構造を再利用してコーディングしていきます。

            
            $ tree -I node_modules
.
├── Dockerfile
├── docker-compose.yml
├── package.json
├── src
│   └── index.ts
├── tsconfig.json
└── yarn.lock
        
まずはsrc/index.tsからコードを実装していきます。

            
            import puppeteer from 'puppeteer';
import * as argparse from 'argparse';
import * as shell from 'shelljs';

(async () => {
    //👇コマンドとして実行時に引数として設定される
    const parser = new argparse.ArgumentParser({
        description: 'Amazon Affiliate Link Generator'
    });
    parser.add_argument('-a', '--asin', {
        type: 'str',
        help: 'Targeted ASIN code what you want.'
    });
    const args_ = parser.parse_args();

    //👇テキストや画像の一時保管先
    // '/usr/src/app/tmp'というフォルダ内にリリースを保存
    const _cwd = '/usr/src/app/tmp';
    shell.rm('-rf', _cwd);
    shell.mkdir(_cwd);

    //👇シェルスクリプトでの echo '...文字列' > ${_cwd}/output.txt 相当
    //shelljsを使うとテキストの保存もワンライナーで作成できる
    //ちなみにAmazonアフィリエイトのユーザーIDは一例のためhoge000-00としています
    shell.echo(`https://amazon.co.jp/dp/${args_.asin}/ref=nosim?tag=hoge000-00`).to(`${_cwd}/output.txt`);

    const browser = await puppeteer.launch({
        executablePath: '/usr/bin/chromium-browser',
        args: ['--disable-dev-shm-usage', '--no-sandbox']
    });

    try {
        const page = await browser.newPage();
        await page.goto(`https://www.amazon.co.jp/dp/${args_.asin}`);

        //👇①商品のタイトル要素を抽出
        const title_ = await page.$eval('#productTitle', el => el.innerHTML);
        //👇テキストファイルにタイトルを書き込み
        //$ echo "${title_}" >> ${_cwd}/output.txt 相当のコマンド
        shell.echo(`${title_}`.replace(/^\n/gm, '')).toEnd(`${_cwd}/output.txt`);

        //👇②商品説明の要素を抽出
        //コードは後述

        //👇③価格情報の要素を抽出
        //コードは後述

        //👇④商品画像の習得
        //コードは後述

    } catch (e) {
        throw e;
    } finally {
        await browser.close();
    }
})();
        
これでひとまずプログラムを走らせてみます。

            
            #👇src/index.tsがビルドされる
$ yarn build
#👇-aもしくは--asinオプションに商品のASINコードを指定
$ yarn tap -a B08HQXVL9Y
        
上手くプログラムが走ると、ルートのtmpフォルダ内にoutput.txtが生成され、以下の中身が取得されているはずです。

            
            https://amazon.co.jp/dp/B08HQXVL9Y/ref=nosim?tag=hoge000-00
GOPPA ウェブカメラ オートフォーカス機能搭載 フルHD 200万画素 1920×1080対応 マイク内蔵 GP-UCAM2FA/E
        

商品紹介を取得

商品紹介の部分は、ul要素内の複数のli内に箇条書きのリスト項目で表示されている形式のため、内容文をすべて引っこ抜くためには、ul要素のinnerHtmlをそのまま習得するか、各liを全て個別に抜き取るなどの方法がかんがえられます。

合同会社タコスキングダム|蛸壺の技術ブログ

puppeteerにはセレクターで複数マッチさせるのに使えるメソッドとして、

            
            $$
$$eval
evaluate + querySelectorALL
        
の3つが用意されています。

この3つは微妙に使い方と作用が違います。

使い分けの詳細については
こちらのかたの記事が詳しいとおもいますので、参考にしてください。

では先程の
index.tsに以下の項目を追加してみよう。

            
            //...中略
        //👇②商品説明の要素を抽出
        const feature_ = await page.$$eval('#feature-bullets > ul > li > span',
            elms => elms.map(el => el.innerHTML)
        );
        for (const elm of feature_) {
            shell.echo(`${elm}`.replace(/^\n/gm, '')).toEnd(`${_cwd}/output.txt`);
        }
//...以下略
        
再度、ビルドしてスクリプトを実行すると、output.txtファイルに商品紹介文が抽出されていたら成功です。

            
            https://amazon.co.jp/dp/B08HQXVL9Y/ref=nosim?tag=hoge000-00
GOPPA ウェブカメラ オートフォーカス機能搭載 フルHD 200万画素 1920×1080対応 マイク内蔵 GP-UCAM2FA/E

<span id="replacementPartsFitmentBulletInner"> <a class="a-link-normal hsx-rpp-fitment-focus" href="#">モデル番号を入力してください</a>
<span>これが適合するか確認:</span>
</span>

GOPPA製「GP-UCAM2FA」は、テレワークやビデオ配信に最適な200万画素WEBカメラです。オートフォーカスによる鮮明な映像、30FPSのなめらか動き、デジタルマイク内蔵など必要な機能をコンパクトに搭載していますので、ZoomやGoogle Meet、TeamsなどのWeb会議やオンライン飲み会でご利用いただけます。

有効画素数:有効画素数、色数:有効画素数、最大解像度:1920×1080、最大フレームレート:30 FPS

フォーカス:オートフォーカス、オートフォーカス:5cm〜∞、F値:F2.2、視野角度:75度

対応OS:Windows 10、Windows 8、Windows 7、macOS 10.12 Sierra 以降

保証期間:6か月
        
ただし、抽出した結果をみていただくと、li要素の中身に更にspan要素が入れ子になっている場合もあります。

完全にキレイな文として抜き出せるかどうかは、サイトがきれいな構造設計をされいるかに依るところが大きいです。

ここらへんはある程度文を抽出できたら自分で細かい内容を修正するように妥協する他ありません。

商品の値段情報を取得

値段情報は商品ページの複数ヶ所に散らばっていたり、商品ページによって不規則に変化しているようです。

今回のやり方もあくまでも目安ですが、値段は
id="priceblock_ourprice"のspan要素、発送条件はid="price-shipping-message"のspan要素内のb要素にあるようです。

合同会社タコスキングダム|蛸壺の技術ブログ

ということで
index.tsには以下のコードを追加します。

            
            //...中略
        //👇③価格情報の要素を抽出
        const price_ = await page.$eval('#priceblock_ourprice', el => el.innerHTML);
        const shipment_ = await page.$eval('#price-shipping-message > b', el => el.innerHTML);
        shell.echo(`${price_}`.replace(/^\n/gm, '') + ` ${shipment_}`).toEnd(`${_cwd}/output.txt`);
//...以下略
        
これをビルド・実行すると、output.txtに以下の価格情報が追加されていると思います。

            
            ¥5,506 通常配送無料
        

画像の取得

商品の見出しとなる画像を一つurlリンクを辿って取得し、ローカルに保存もしたいときがあると思います。

商品ページの見出し画像のリンクは、
id="imgTagWrapperId"のdiv要素直下のimg要素にあるsrc属性から取得できます。

合同会社タコスキングダム|蛸壺の技術ブログ

リソース画像の保存先が分かれば、あとはwgetでローカルに取得できます。

以下がコーディングの一例です。

            
            //...中略
        //👇④商品画像の習得
        const imgUrl_ = await page.$eval('#imgTagWrapperId > img', el => el.getAttribute('src'));
        shell.exec(`wget ${imgUrl_} -O ${_cwd}/amazon.jpg`);
//...以下略
        
ここでのポイントはshelljsによって外部のwgetが実行されているのですが、当然ながらシステムにwgetがインストールされて使えるようになっていないと使えません。


Amazonの商品ページ操作例② ~ 電子書籍(Kindle)系

物販品以外での例も取り上げてみます。

商品カテゴリーが違うとhtml構造もガラッと違うため、AmazonアフィリエイトURLの自動生成を行う場合は、スクリプトツールもカテゴリー別にして管理したほうが良いかもしれません。

特に電子書籍やソフトウェアは発送情報はないので値段だけを抽出するところもあり、物販とは販売方法も異なります。

まず以下に電子書籍の商品ページの取得コード例を修正するポイントだけを載せています。

            
            //...中略
        //👇①商品のタイトル要素を抽出
        const title_ = await page.$eval('#productTitle', el => el.innerHTML);
        shell.echo(`${title_}`.replace(/^\n/gm, '')).toEnd(`${_cwd}/output.txt`);

        //👇②商品説明の要素を抽出
        const elementHandle: any = await page.$('#bookDesc_iframe_wrapper > iframe');
        const frame = await elementHandle.contentFrame();
        subtitleSelector = '#iframeContent';
        await frame.waitForSelector(subtitleSelector);
        const feature_ = await frame.$eval(subtitleSelector, (el: any) => el.innerHTML);
        shell.echo(`${feature_}`.replace(/^\n/gm, '')).toEnd(`${_cwd}/output.txt`);

        //👇③価格情報の要素を抽出
        const price_ = await page.$eval('#tmmSwatches > ul > li:nth-child(1) > span > span:nth-child(1) > span > a', el => el.innerHTML);
        shell.echo(`${price_}`.replace(/^\n/gm, '')).toEnd(`${_cwd}/output.txt`);

        //👇④商品画像の習得
        const imgUrl_ = await page.$eval('#ebooksImgBlkFront', el => el.getAttribute('src'));
        shell.exec(`wget ${imgUrl_} -O ${_cwd}/amazon.jpg`);
//...以下略
        
電子書籍の商品ページの各要素取得に関しては、の商品説明を抽出する時に、iframe要素を操作する必要があります。

合同会社タコスキングダム|蛸壺の技術ブログ

サイトにもよりますが、iframeの中身はメインのhtmlが読み込みが完了してから実行されるjsスクリプトによってレンダリングされる仕組みがあります。

きちんと読み込み完了後にiframe要素を取り込むようにpuppeteerへ知らせないとiframeの中身は取得できません。

そこでiframe要素を取り出し、
contentFrameメソッドを利用して、iframe内の要素にアクセスするようにpuppeteerに教えて挙げないといけません。

iframe要素内で目的の要素がレンダリング完了となるまで、
waitForSelectorで待つことができますので、これも上手く利用して、商品説明文を取得できるようになります。


まとめ

今回はpuppeteerを利用して、AmazonアフィリエイトのURLに関連するリソースを含めてを自動生成する手段について考察してみました。

Amazonの商品ページも商品カテゴリーごとにhtmlの構造が違うので、自動抽出ツールもコレ1つ使っていればOK、というものにはならないかも知れませんが、自前でスクリプトを作成する指針にしていただければ幸いです。