ゲーム開発
Unity
UnrealEngine
C++
Blender
ゲーム数学
ゲームAI
グラフィックス
サウンド
アニメーション
GBDK
制作日記
IT関連
ツール開発
フロントエンド関連
サーバサイド関連
WordPress関連
ソフトウェア設計
おすすめ技術書
音楽
DTM
楽器・機材
ピアノ
ラーメン日記
四コマ漫画
その他
おすすめアイテム
おもしろコラム
  • ゲーム開発
    • Unity
    • UnrealEngine
    • C++
    • Blender
    • ゲーム数学
    • ゲームAI
    • グラフィックス
    • サウンド
    • アニメーション
    • GBDK
    • 制作日記
  • IT関連
    • ツール開発
    • フロントエンド関連
    • サーバサイド関連
    • WordPress関連
    • ソフトウェア設計
    • おすすめ技術書
  • 音楽
    • DTM
    • 楽器・機材
    • ピアノ
  • ラーメン日記
    • 四コマ漫画
      • その他
        • おすすめアイテム
        • おもしろコラム
      1. ホーム
      2. 20240108_02_amazon_link

      【Node.js】廃止されたAmazonアソシエイト画像リンクをAmazon Product Advertising API経由で復活させる

      フロントエンド関連JavaScriptツール開発WordPress関連Node.js
      2024-01-08

      Amazon画像リンクの廃止

      マイケル
      マイケル
      みなさんこんにちは! マイケルです!
      エレキベア
      エレキベア
      こんにちクマ〜〜〜
      マイケル
      マイケル
      このブログは続けて数年になりますが、ある日Amazonから下記のようなメールが届きました。
      20240108_02_amazon_link_01
      マイケル
      マイケル
      どうやらアフィリエイトで使用しているAmazonアソシエイトの画像リンクが廃止になってしまうようです。 リンク作成機能だけならまだしも、2024年からこれまでの画像リンクすら表示されなくなってしまうとのことです。
      エレキベア
      エレキベア
      ええぇ・・・それは大変クマ・・・
      マイケル
      マイケル
      既に200記事近くAmazonの画像リンクを使ってきたので、それが全て表示されなくなるとなるとかなり痛手です・・・。
      マイケル
      マイケル
      しかし、Amazon Product Advertising API(以下、PA-API)を使えば引き続き商品情報を取得できるとのことなので、今回はこちらのAPIを使って古い画像リンクを復活させてみようかと思います。

      Amazon Product Advertising API(商品情報API)

      エレキベア
      エレキベア
      今後は自分でAPI経由で取得しろということクマか・・・ やったるクマ〜〜〜
      マイケル
      マイケル
      ちなみに今回作ったAmazonリンク変換ツールについては下記GitHubに公開しているので、同じように変換させたい方はこちらをご参照ください!

      GitHub - nodejs-amazon-link-parser

      エレキベア
      エレキベア
      Node.jsで実行できるツールを作ったのクマね

      PA-APIでの商品情報取得

      マイケル
      マイケル
      今回はNode.js、TypeScriptで開発を進めます。 まずはPA-APIで商品情報を取得して新しいHTML形式で出力する処理を実装してみます。

      商品情報を取得する

      マイケル
      マイケル
      APIの実行には用意されている paapi5-nodejs-sdk を使用するため、 下記コマンドでインストールします。
      npm install --save-dev paapi5-nodejs-sdk
      
      マイケル
      マイケル
      そしてAPI実行で取得する項目について型を定義します。 項目の詳細については下記ドキュメントをご参照ください。

      paapi5 - documentation

      /**
       * paapi5から返却されるレスポンス
       */
      export namespace amazonPaApiData {
          // GetItems
          // https://webservices.amazon.com/paapi5/documentation/get-items.html
          export type getItems = {
            ItemsResult: ItemsResult;
          };
        
          type ItemsResult = {
            Items: Item[];
          };
          type Item = {
            ASIN: string;
            DetailPageURL: string;
            Images: ItemImages;
            ItemInfo: ItemInfo;
            Offers: Offers;
          };
        
          // 画像情報
          type ItemImages = {
            Primary: ItemImagesPrimary;
          };
          type ItemImagesPrimary = {
            Large: ItemImagePrimaryLarge;
          };
          type ItemImagePrimaryLarge = {
            URL: string;
            Height: 500;
            Width: 396;
          };
        
          // タイトル情報
          type ItemInfo = {
            Title: ItemInfoTitle;
          };
          type ItemInfoTitle = {
            DisplayValue: string;
            Label: string;
            Locale: string;
          };
        
          // 価格等の情報
          type Offers = {
            Listings: OfferListItem[];
          };
          type OfferListItem = {
            Id: string;
            Price: OfferListItemPrice;
            ViolatesMAP: boolean;
          };
          type OfferListItemPrice = {
            Amount: number;
            Currency: string;
            DisplayAmount: string;
          };
        }
        
        /**
         * アプリケーション側で受け取るレスポンス形式
         */
        export namespace amazonPaApiResponse {
          export type Item = {
            asinId: string;
            pageUrl: string;
            imageUrl: string;
            displayPrice: string;
            displayTitle: string;
          };
        }
        
      
      ▲取得する商品情報の型定義
      エレキベア
      エレキベア
      取得したいAPIに対応するレスポンスを定義するクマね
      マイケル
      マイケル
      そして下記のような形でAPIを実行します。 今回は商品のASINを受け取って、 'ItemInfo.Title', 'Images.Primary.Large', 'Offers.Listings.Price' を実行する形で実装しました。
      import ProductAdvertisingAPIv1 from 'paapi5-nodejs-sdk';
      import { AmazonPaApiSettings } from '../settings';
      import { amazonPaApiData, amazonPaApiResponse } from '../type/amazon-pa-api';
      
      /**
       * GetItemsAPI実行
       * awaitできるようにPromiseでラップして定義
       * @param request
       * @returns
       */
      async function requestGetItems(
        request: any,
      ): Promise<amazonPaApiData.getItems | undefined> {
        // ビルド時に大量アクセスとなるのを防ぐため少し待つ
        await new Promise((resolve) =>
          setTimeout(resolve, AmazonPaApiSettings.WaitExecuteApiMs),
        );
      
        // リクエスト実行
        return new Promise((resolve) => {
          const api = new ProductAdvertisingAPIv1.DefaultApi();
          api.getItems(request, (error: any, data: any, response: any) => {
            // 取得エラー時
            if (error) {
              // console.log(error);
              resolve(undefined);
              return;
            }
      
            // レスポンス取得
            const responseJson: amazonPaApiData.getItems =
              ProductAdvertisingAPIv1.GetItemsResponse.constructFromObject(data);
            resolve(responseJson);
          });
        });
      }
      
      /**
       * Amazon商品アイテム情報を取得
       * @param asidIds
       * @param callback
       * @returns
       */
      export async function getAmazonItemInfo(
        asidIds: string[],
      ): Promise<amazonPaApiResponse.Item[]> {
        const resultItems: amazonPaApiResponse.Item[] = [];
      
        // 設定値を取得
        const partnerTag = AmazonPaApiSettings.AmazonPaApiPartnerTag;
        const accessKey = AmazonPaApiSettings.AmazonPaApiAccessKey;
        const secretKey = AmazonPaApiSettings.AmazonPaApiSecretKey;
        if (!partnerTag || !accessKey || !secretKey) {
          console.error(`not found amazon paapi!! => ${asidIds}`);
          return resultItems;
        }
      
        const defaultClient = ProductAdvertisingAPIv1.ApiClient.instance;
        const getItemsRequest = new ProductAdvertisingAPIv1.GetItemsRequest();
      
        defaultClient.accessKey = accessKey;
        defaultClient.secretKey = secretKey;
        defaultClient.host = 'webservices.amazon.co.jp';
        defaultClient.region = 'us-west-2';
      
        getItemsRequest['PartnerTag'] = partnerTag;
        getItemsRequest['PartnerType'] = 'Associates';
      
        // 取得したい製品情報を指定
        getItemsRequest['ItemIds'] = asidIds;
        getItemsRequest['Resources'] = [
          'ItemInfo.Title',
          'Images.Primary.Large',
          'Offers.Listings.Price',
        ];
      
        // リクエスト実行
        const responseJson = await requestGetItems(getItemsRequest);
        if (!responseJson) {
          console.error(`error response amazon paapi!! => ${asidIds}`);
          return resultItems;
        }
        const responseItems = responseJson.ItemsResult?.Items;
        if (!Array.isArray(responseItems) || responseItems.length <= 0) {
          console.error(`no response amazon paapi!! => ${asidIds}`);
          return resultItems;
        }
      
        console.error(`success amazon paapi!! => ${asidIds}`);
      
        // レスポンス形式に変換して返却
        for (const responseItem of responseItems) {
          resultItems.push({
            asinId: responseItem.ASIN,
            pageUrl: responseItem.DetailPageURL,
            imageUrl: responseItem.Images?.Primary?.Large?.URL,
            displayPrice: responseItem.Offers?.Listings[0]?.Price?.DisplayAmount,
            displayTitle: responseItem.ItemInfo?.Title?.DisplayValue,
          });
        }
        return resultItems;
      }
      
      
      ▲PA-APIによる商品情報取得
      エレキベア
      エレキベア
      この処理を呼び出せば商品情報が取得できるのクマね

      商品情報からHTMLを作成する

      マイケル
      マイケル
      次に、取得した商品情報から新しいHTML形式を作成する処理を作成します。 こちらは各々カスタマイズしたい形式に変換して問題ないですが、今回は下記のような形で定義しました。
      /**
       * AmazonアイテムHTML変換設定
       */
      export namespace AmazonParseSettings {
        /**
         * 商品アイテム
         */
        export const AmazonItemDataId = 'data-asin-id';
        export const AmazonItemRegax = /data-asin-id="(.+)">/;
        export function CraeteAmazonItemHtmlString(
          item: amazonPaApiResponse.Item,
        ): string {
          return `<div class="amazon-pa-api-item" ${
            AmazonParseSettings.AmazonItemDataId
          }="${item.asinId}">
          <div class="amazon-logo-img-wrapper">
              <img class="amazon-logo-img" src="/img/amazon/amazon-logo.png">
          </div>
          <a class="amazon-pa-api-item-link" href="${item.pageUrl}" target="_blank">
              <div class="amazon-pa-api-item-img-wrapper">
                  <img class="amazon-pa-api-item-img" src="${item.imageUrl}">
              </div>
              <div class="amazon-pa-api-item-text-area">
                  <div class="amazon-pa-api-item-text-title">${SubStringTitle(
                    item.displayTitle,
                  )}</div>
                  <div class="amazon-pa-api-item-text-price">${
                    item.displayPrice ? SubStringPrice(item.displayPrice) : ''
                  }</div>
              </div>
          </a>
      </div>`;
        }
      
        /**
         * 商品画像リンク
         */
        export const AmazonImageLinkDataId = 'data-img-link-asin-id';
        export const AmazonImageLinkRegax = /data-img-link-asin-id="(.+)">/;
        export function CreateAmazonImageLinkHtmlString(
          item: amazonPaApiResponse.Item,
        ): string {
          return `<a href="${item.pageUrl}" target="_blank" rel="noopener" ${AmazonParseSettings.AmazonImageLinkDataId}="${item.asinId}">
        <img class="amazon-pa-api-img" ${AmazonParseSettings.AmazonImageDataId}="${item.asinId}" src="${item.imageUrl}" decoding="async">
      </a>`;
        }
      
        /**
         * 商品画像
         */
        export const AmazonImageDataId = 'data-img-asin-id';
        export const AmazonImageRegax = /data-img-asin-id="(.+)">/;
        export function CreateAmazonImageHtmlString(
          item: amazonPaApiResponse.Item,
        ): string {
          return `<img class="amazon-pa-api-img" src="${item.imageUrl}" decoding="async" border="0" ${AmazonParseSettings.AmazonImageDataId}="${item.asinId}">`;
        }
      
        /**
         * 商品タイトル
         */
        export function CreateAmazonImageTitleHtmlString(
          item: amazonPaApiResponse.Item,
        ): string {
          return `<a href="${item.pageUrl}">${SubStringTitle(item.displayTitle)}</a>`;
        }
      
        /**
         * テキストの最大表示文字数
         */
        const MaxDisplayTitleCount = 40;
        const SubStringTitle = (value: string) => {
          if (value?.length <= MaxDisplayTitleCount) {
            return value;
          }
          return value.substring(0, MaxDisplayTitleCount) + '...';
        };
        const SubStringPrice = (value: string) => {
          if (!value.includes(' (')) {
            return value;
          }
          return value.split(' (')[0];
        };
      }
      
      
      ▲出力するHTMLテンプレート
      エレキベア
      エレキベア
      最終的にここで定義したHTMLに変換できればいいということクマね
      マイケル
      マイケル
      先ほど定義したAPIを使用して、受け取ったASINから商品情報を取得しHTMLに変換する処理を実装します。
      import { getAmazonItemInfo } from '../api/amazon-paapi';
      import { AmazonParseSettings } from '../settings';
      
      /**
       * ASINからコンソール出力用HTMLを作成する
       * @param htmlText
       * @returns
       */
      export async function CreateAmazonPrintHtml(asinIds: string[]) {
        // ASINから商品情報を取得
        const itemInfoArray = await getAmazonItemInfo(asinIds);
      
        let result = '\n';
      
        // 商品アイテムHTMLを出力
        for (const asinId of asinIds) {
          const itemInfo = itemInfoArray.find(
            (itemInfo) => itemInfo.asinId === asinId,
          );
          if (itemInfo) {
            result += `${AmazonParseSettings.CreateAmazonImageLinkHtmlString(
              itemInfo,
            )}\n\n`;
            result += `${AmazonParseSettings.CreateAmazonImageTitleHtmlString(
              itemInfo,
            )}\n\n`;
          } else {
            console.error('not exist asinId item info => ' + asinId);
          }
        }
        result += '==============================\n\n';
        for (const asinId of asinIds) {
          const itemInfo = itemInfoArray.find(
            (itemInfo) => itemInfo.asinId === asinId,
          );
          if (itemInfo) {
            result += `${AmazonParseSettings.CraeteAmazonItemHtmlString(
              itemInfo,
            )}\n\n`;
          } else {
            console.error('not exist asinId item info => ' + asinId);
          }
        }
      
        return result;
      }
      
      ▲商品アイテムHTMLの出力
      import { CreateAmazonPrintHtml } from './parser/amazon-new-link-parser';
      
      /**
       * 指定されたASIDのAmazonリンクをコンソール出力する
       * @param asinIdArray
       */
      async function PrintLinkAsync(asinIdArray: string[]) {
        const amazonHtml = await CreateAmazonPrintHtml(asinIdArray);
        console.log(amazonHtml);
      }
      
      async function MainAsync() {
        const argv = process.argv;
        if (argv.length <= 2) {
          console.error('prease input ASIN IDs (「,」split).');
          return;
        }
      
        // 引数からファイルパスを取得して読み込む
        const paramAsinIds = argv[2];
        const asinIdArray = paramAsinIds.split(',');
        PrintLinkAsync(asinIdArray);
      }
      MainAsync();
      
      
      ▲メイン処理
      マイケル
      マイケル
      ここまででASINから新しい形式の商品アイテムHTMLを出力する処理が実装できました。 下記のように実行するとコンソールにHTMLが出力されます。
      # ASINからAmazon商品のHTMLを出力
      $ npx ts-node --files src/main-print-link.ts B0B1LN6QSL
      
      <a href="https://www.amazon.co.jp/dp/B0B1LN6QSL?tag=afelekibear-22&linkCode=ogi&th=1&psc=1" target="_blank" rel="noopener" data-img-link-asin-id="B0B1LN6QSL">
        <img class="amazon-pa-api-img" data-img-asin-id="B0B1LN6QSL" src="https://m.media-amazon.com/images/I/512RtSmh-RL._SL500_.jpg" decoding="async">
      </a>
      
      <a href="https://www.amazon.co.jp/dp/B0B1LN6QSL?tag=afelekibear-22&linkCode=ogi&th=1&psc=1">日清食品 日清チキンラーメン 小分け1食パック インスタント袋麺 85g×10個</a>
      
      ==============================
      
      <div class="amazon-pa-api-item" data-asin-id="B0B1LN6QSL">
          <div class="amazon-logo-img-wrapper">
              <img class="amazon-logo-img" src="/img/amazon/amazon-logo.png">
          </div>
          <a class="amazon-pa-api-item-link" href="https://www.amazon.co.jp/dp/B0B1LN6QSL?tag=afelekibear-22&linkCode=ogi&th=1&psc=1" target="_blank">
              <div class="amazon-pa-api-item-img-wrapper">
                  <img class="amazon-pa-api-item-img" src="https://m.media-amazon.com/images/I/512RtSmh-RL._SL500_.jpg">
              </div>
              <div class="amazon-pa-api-item-text-area">
                  <div class="amazon-pa-api-item-text-title">日清食品 日清チキンラーメン 小分け1食パック インスタント袋麺 85g×10個</div>
                  <div class="amazon-pa-api-item-text-price">¥1,323</div>
              </div>
          </a>
      </div>
      
      ▲実行するとHTMLが出力される
      20240108_02_amazon_link_02
      ▲出力されたAmazon商品のHTML例

      エレキベア
      エレキベア
      (なんでチキンラーメン・・・) これでAmazon画像リンク作成の代わりに生成できるようになったクマね

      古い画像リンクの一括置換処理

      マイケル
      マイケル
      次に、廃止されて無効になってしまった画像リンクを一括置換で復活させる処理を実装してみます。 前提になりますが、これまでAmazonの画像リンク、商品アイテムリンクは下記のように定義されていました。
      
      ・・・略・・・
      
      <p><a rel="noopener" href="https://www.amazon.co.jp/HLSL-%E3%82%B7%E3%82%A7%E3%83%BC%E3%83%80%E3%83%BC%E3%81%AE%E9%AD%94%E5%B0%8E%E6%9B%B8-%E3%82%B7%E3%82%A7%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E3%81%AE%E5%9F%BA%E7%A4%8E%E3%81%8B%E3%82%89%E3%83%AC%E3%82%A4%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%B3%E3%82%B0%E3%81%BE%E3%81%A7-%E6%B8%85%E5%8E%9F-%E9%9A%86%E8%A1%8C/dp/4798164283?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&amp;crid=2BUOFINNA898F&amp;keywords=HLSL&amp;qid=1677426321&amp;sprefix=hlsl%2Caps%2C201&amp;sr=8-1&amp;linkCode=li3&amp;tag=afelekibear-22&amp;linkId=6a0984b4d086ce545846c42d0602e833&amp;language=ja_JP&amp;ref_=as_li_ss_il" target="_blank"><img decoding="async" border="0" src="//ws-fe.amazon-adsystem.com/widgets/q?_encoding=UTF8&amp;ASIN=4798164283&amp;Format=_SL250_&amp;ID=AsinImage&amp;MarketPlace=JP&amp;ServiceVersion=20070822&amp;WS=1&amp;tag=afelekibear-22&amp;language=ja_JP"></a><img decoding="async" loading="lazy" src="https://ir-jp.amazon-adsystem.com/e/ir?t=afelekibear-22&amp;language=ja_JP&amp;l=li3&amp;o=9&amp;a=4798164283" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;"></p>
      <p><a href="https://amzn.to/3ZkDLa7">HLSL シェーダーの魔導書 シェーディングの基礎からレイトレーシングまで</a></p>
      
      ・・・略・・・
      
      <p><iframe sandbox="allow-popups allow-scripts allow-modals allow-forms allow-same-origin" style="width:120px;height:240px;" marginwidth="0" marginheight="0" scrolling="no" frameborder="0" src="//rcm-fe.amazon-adsystem.com/e/cm?lt1=_blank&amp;bc1=000000&amp;IS2=1&amp;bg1=FFFFFF&amp;fc1=000000&amp;lc1=0000FF&amp;t=afelekibear-22&amp;language=ja_JP&amp;o=9&amp;p=8&amp;l=as4&amp;m=amazon&amp;f=ifr&amp;ref=as_ss_li_til&amp;asins=4798164283&amp;linkId=d06ee3e24f80fb7320f916aa77662abe"></iframe></p>
      
      エレキベア
      エレキベア
      このimgタグのsrcとiframeが機能しなくなったクマね
      マイケル
      マイケル
      当ブログでは、古い記事はHTML形式のテキストファイルで管理しています。 テキストからHTMLを読み込んで操作するため、今回は cheerio というjQueryライクに操作、変換できるライブラリを使用しました。
      npm install --save-dev cheerio
      
      マイケル
      マイケル
      そして下記のようにテキストを読み込んで、 1. 各タグのプロパティからASINを取得 2. ASINから商品情報を取得 3. 商品情報から新しい形式のHTMLに変換して要素を置換 といった手順で置換を行います。
      import * as cheerio from 'cheerio';
      import { getAmazonItemInfo } from '../api/amazon-paapi';
      import { AmazonParseSettings } from '../settings';
      
      /**
       * iframeタグのsrc値からasinIdを取得
       * 設定例 => //rcm-fe.amazon-adsystem.com/e/cm?lt1=_blank&amp;bc1=000000&amp;IS2=1&amp;bg1=FFFFFF&amp;fc1=000000&amp;lc1=0000FF&amp;t=afelekibear-22&amp;language=ja_JP&amp;o=9&amp;p=8&amp;l=as4&amp;m=amazon&amp;f=ifr&amp;ref=as_ss_li_til&amp;asins=4798164283&amp;linkId=d06ee3e24f80fb7320f916aa77662abe
       * @param src
       * @returns
       */
      const GetAsinIdFromIframeSrc = (src: string) => {
        if (
          src &&
          src.indexOf('amazon-adsystem.com') >= 0 &&
          src.indexOf('&asins=') >= 0
        ) {
          for (const param of src.split('&')) {
            if (param.indexOf('asins=') >= 0) {
              return param.replace('asins=', '');
            }
          }
        }
        return null;
      };
      
      /**
       * imgタグのsrc値からasinIdを取得
       * 設定例 => //ws-fe.amazon-adsystem.com/widgets/q?_encoding=UTF8&amp;ASIN=4798164283&amp;Format=_SL250_&amp;ID=AsinImage&amp;MarketPlace=JP&amp;ServiceVersion=20070822&amp;WS=1&amp;tag=afelekibear-22&amp;language=ja_JP
       * @param src
       * @returns
       */
      const GetAsinIdFromImgSrc = (src: string) => {
        if (
          src &&
          src.indexOf('amazon-adsystem.com') >= 0 &&
          src.indexOf('&ASIN=') >= 0
        ) {
          for (const param of src.split('&')) {
            if (param.indexOf('ASIN=') >= 0) {
              return param.replace('ASIN=', '');
            }
          }
        }
        return null;
      };
      
      /**
       * HtmlText内のAmazonリンクを作り直す
       * @param htmlText
       * @returns
       */
      export async function ParseOldAmazonLinkAsync(htmlText: string) {
        const $ = cheerio.load(htmlText);
      
        // HTMLからasinIdを抽出
        let asinIds: string[] = [];
        $('iframe').each((index: any, elem: any) => {
          // 商品画像リンク
          const asinId = GetAsinIdFromIframeSrc(elem.attribs?.src);
          if (asinId) {
            asinIds.push(asinId);
          }
        });
        $('img').each((index: any, elem: any) => {
          // 商品画像単体
          const asinId = GetAsinIdFromImgSrc(elem.attribs?.src);
          if (asinId) {
            asinIds.push(asinId);
          }
        });
      
        if (!asinIds || asinIds.length <= 0) {
          return htmlText;
        }
      
        // 重複削除
        asinIds = Array.from(new Set(asinIds));
        const itemInfoArray = await getAmazonItemInfo(asinIds);
      
        // 商品アイテム情報からHTMLを生成して差し替える
        $('iframe').each((index: any, elem: any) => {
          // 商品画像リンク
          const asinId = GetAsinIdFromIframeSrc(elem.attribs?.src);
          if (asinId) {
            const itemInfo = itemInfoArray.find(
              (itemInfo) => itemInfo.asinId === asinId,
            );
            // pタグ - iframeタグの順で格納されている場合、親要素ごと差し替える
            const targetElement =
              $(elem.parent).prop('tagName') === 'P' ? elem.parent : elem;
            if (itemInfo) {
              $(targetElement).replaceWith(
                `\n${AmazonParseSettings.CraeteAmazonItemHtmlString(itemInfo)}\n`,
              );
            } else {
              $(targetElement).remove(); // 商品がなければ削除する
              console.error('remove element => ' + asinId);
            }
          }
        });
        $('img').each((index: any, elem: any) => {
          // 商品画像単体
          const asinId = GetAsinIdFromImgSrc(elem.attribs?.src);
          if (asinId) {
            // srcを差し替えてidを付与する
            const itemInfo = itemInfoArray.find(
              (itemInfo) => itemInfo.asinId === asinId,
            );
            if (itemInfo) {
              $(elem).replaceWith(
                `\n${AmazonParseSettings.CreateAmazonImageHtmlString(itemInfo)}\n`,
              );
            }
          }
        });
      
        return $.html();
      }
      
      
      ▲古いAmazonリンクの一括置換
      マイケル
      マイケル
      あとはこの処理を変換したい記事テキストファイル全てに行えば完了です!
      import path from 'path';
      import { FileUtil } from './common/FileUtil';
      import { ParseOldAmazonLinkAsync } from './parser/amazon-old-link-parser';
      import { FileSettings } from './settings';
      
      /**
       * HTMLテキスト記事の古いAmazonリンク(iframe、img)を独自のHTML形式に変換する
       * @param fileDir
       * @param fileName
       */
      async function ParseAsync(fileDir: string, fileName: string) {
        // ファイル読み込み
        const readPath = path.join(fileDir, fileName);
        const postContent = FileUtil.tryReadFileSync(readPath);
      
        // Amazonリンクを変換する
        const convertPostContent = await ParseOldAmazonLinkAsync(postContent);
      
        // 変更した内容を書き込む
        FileUtil.writeFileSync(readPath, convertPostContent);
      }
      
      async function MainAsync() {
        // HTMLテキストフォルダ内の全てのファイルに対して行う
        const postTextFilePathList = FileUtil.readDirSync(
          FileSettings.PostTextFileDir,
        );
        for (const postTextFile of postTextFilePathList) {
          console.log('\n[' + postTextFile + ']');
          await ParseAsync(FileSettings.PostTextFileDir, postTextFile);
        }
      }
      MainAsync();
      
      
      ▲メイン処理
      
      ・・・略・・・
      
      <p><a rel="noopener" href="https://www.amazon.co.jp/HLSL-%E3%82%B7%E3%82%A7%E3%83%BC%E3%83%80%E3%83%BC%E3%81%AE%E9%AD%94%E5%B0%8E%E6%9B%B8-%E3%82%B7%E3%82%A7%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E3%81%AE%E5%9F%BA%E7%A4%8E%E3%81%8B%E3%82%89%E3%83%AC%E3%82%A4%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%B3%E3%82%B0%E3%81%BE%E3%81%A7-%E6%B8%85%E5%8E%9F-%E9%9A%86%E8%A1%8C/dp/4798164283?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&amp;crid=2BUOFINNA898F&amp;keywords=HLSL&amp;qid=1677426321&amp;sprefix=hlsl%2Caps%2C201&amp;sr=8-1&amp;linkCode=li3&amp;tag=afelekibear-22&amp;linkId=6a0984b4d086ce545846c42d0602e833&amp;language=ja_JP&amp;ref_=as_li_ss_il" target="_blank">
      <img class="amazon-pa-api-img" src="https://m.media-amazon.com/images/I/51wA+X0eYvS._SL500_.jpg" decoding="async" border="0" data-img-asin-id="4798164283">
      </a><img decoding="async" loading="lazy" src="https://ir-jp.amazon-adsystem.com/e/ir?t=afelekibear-22&amp;language=ja_JP&amp;l=li3&amp;o=9&amp;a=4798164283" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;"></p>
      <p><a href="https://amzn.to/3ZkDLa7">HLSL シェーダーの魔導書 シェーディングの基礎からレイトレーシングまで</a></p>
      
      ・・・略・・・
      
      <div class="amazon-pa-api-item" data-asin-id="4798164283">
          <div class="amazon-logo-img-wrapper">
              <img class="amazon-logo-img" src="/img/amazon/amazon-logo.png">
          </div>
          <a class="amazon-pa-api-item-link" href="https://www.amazon.co.jp/dp/4798164283?tag=afelekibear-22&linkCode=ogi&th=1&psc=1" target="_blank">
              <div class="amazon-pa-api-item-img-wrapper">
                  <img class="amazon-pa-api-item-img" src="https://m.media-amazon.com/images/I/51wA+X0eYvS._SL500_.jpg">
              </div>
              <div class="amazon-pa-api-item-text-area">
                  <div class="amazon-pa-api-item-text-title">HLSL シェーダーの魔導書 シェーディングの基礎からレイトレーシングまで</div>
                  <div class="amazon-pa-api-item-text-price">¥5,060</div>
              </div>
          </a>
      </div>
      
      ▲正常に変換された
      エレキベア
      エレキベア
      おお〜〜〜これでまた画像リンクが機能するようになったクマ
      マイケル
      マイケル
      注意点として、ファイルごとにAPIを実行することになりますが、 実行間隔を短くしすぎるとエラーになってしまう場合があったため、実行ごとに一定秒待ち時間を設けた方がよさそうです。
      エレキベア
      エレキベア
      むやみに一括で変換しようとすると危ないクマね

      画像リンクの商品情報更新処理

      マイケル
      マイケル
      以上で画像リンクを復活させることができましたが、商品の情報は埋め込み文字列になっているためリアルタイムに更新されません。 そのため手動で最新の情報に更新してあげる必要があります。
      エレキベア
      エレキベア
      なるほどクマ 商品が価格が前の状態のままになってしまう事態も起こりそうクマね
      マイケル
      マイケル
      新しい形式のHTMLを一括で更新する処理は下記のように実装しました。 テキストを上から一行ずつチェックしていき、ASINが設定されているタグに対して再商品情報を取得、HTMLを再生成するようにしています。
      
      ・・・略・・・
      
      /**
       * テキスト行内のAmazonアイテムからasinIdを取得
       * @param line
       * @returns
       */
      const GetItemAsinIdFromMarkdownLine = (line: string) => {
        const match = line.match(AmazonParseSettings.AmazonItemRegax);
        if (match) {
          return match[1];
        }
      };
      
      /**
       * テキスト行内のAmazon画像リンクからasinIdを取得
       * @param line
       * @returns
       */
      const GetImageLinkAsinIdFromMarkdownLine = (line: string) => {
        const match = line.match(AmazonParseSettings.AmazonImageLinkRegax);
        if (match) {
          return match[1];
        }
      };
      
      /**
       * テキスト行内のAmazon画像リンクからasinIdを取得
       * @param line
       * @returns
       */
      const GetImageAsinIdFromMarkdownLine = (line: string) => {
        const match = line.match(AmazonParseSettings.AmazonImageRegax);
        if (match) {
          return match[1];
        }
      };
      
      /**
       * テキスト内のAmazonリンクを更新する
       * @param htmlText
       * @returns
       */
      export async function UpdateAmazonLinkAsync(postContent: string) {
        // MarkdownテキストからasinIdを取得
        let asinIds: string[] = [];
        for (const line of postContent.split('\n')) {
          let asinId = GetItemAsinIdFromMarkdownLine(line);
          if (asinId) {
            asinIds.push(asinId);
          }
          asinId = GetImageLinkAsinIdFromMarkdownLine(line);
          if (asinId) {
            asinIds.push(asinId);
          }
        }
        if (!asinIds || asinIds.length <= 0) {
          return postContent;
        }
      
        // 重複削除
        asinIds = Array.from(new Set(asinIds));
        const itemInfoArray = await getAmazonItemInfo(asinIds);
        console.log(asinIds);
      
        // 商品アイテム情報からHTMLを生成して差し替える
        let isDoParseAmazonItem = false;
        let isDoParseAmazonImageLink = false;
        let parsePostContent = '';
        for (const line of postContent.split('\n')) {
          // 商品アイテムParse中の場合
          // 次の終了タグが来るまでの間、スキップする
          if (isDoParseAmazonItem) {
            if (line === '</div>') {
              isDoParseAmazonItem = false;
            }
            continue;
          }
          if (isDoParseAmazonImageLink) {
            if (line === '</a>') {
              isDoParseAmazonImageLink = false;
            }
            continue;
          }
      
          // 商品アイテムのParse
          let asinId = GetItemAsinIdFromMarkdownLine(line);
          if (asinId) {
            const itemInfo = itemInfoArray.find(
              (itemInfo) => itemInfo.asinId === asinId,
            );
            if (itemInfo) {
              parsePostContent += '\n';
              parsePostContent += `${AmazonParseSettings.CraeteAmazonItemHtmlString(
                itemInfo,
              )}`;
              isDoParseAmazonItem = true;
              continue;
            } else {
              console.error('not exist asinId item info => ' + asinId);
            }
          }
      
          // 商品画像リンクのParse
          asinId = GetImageLinkAsinIdFromMarkdownLine(line);
          if (asinId) {
            const itemInfo = itemInfoArray.find(
              (itemInfo) => itemInfo.asinId === asinId,
            );
            if (itemInfo) {
              parsePostContent += '\n';
              parsePostContent += `${AmazonParseSettings.CreateAmazonImageLinkHtmlString(
                itemInfo,
              )}`;
              isDoParseAmazonImageLink = true;
              continue;
            } else {
              console.error('not exist asinId item info => ' + asinId);
            }
          }
      
          // 商品リンクのParse
          asinId = GetImageAsinIdFromMarkdownLine(line);
          if (asinId) {
            const itemInfo = itemInfoArray.find(
              (itemInfo) => itemInfo.asinId === asinId,
            );
            if (itemInfo) {
              parsePostContent += '\n';
              parsePostContent += `${AmazonParseSettings.CreateAmazonImageHtmlString(
                itemInfo,
              )}`;
              continue;
            } else {
              console.error('not exist asinId item info => ' + asinId);
            }
          }
      
          // 上記以外の場合、元の行をそのまま追加
          if (parsePostContent != '') {
            parsePostContent += '\n';
          }
          parsePostContent += line;
        }
        return parsePostContent;
      }
      
      
      ▲HTMLの商品情報更新処理
      エレキベア
      エレキベア
      これで完璧クマ〜〜〜

      おわりに

      マイケル
      マイケル
      というわけで今回は廃止されたAmazon画像リンクを新しい形式に置換してみました! どうだったかな??
      エレキベア
      エレキベア
      独自形式だとCSSスタイルも自由に書けるからデザインの統一感は生まれそうクマね でもわざわざAPI実行して取得となるとエンジニア以外は中々厳しそうクマ
      マイケル
      マイケル
      同じように画像リンクを多用していた人もいると思うから、中々な事態だよね・・・ 似たような状態の方がいれば、今回の記事をぜひ参考にしてください!
      マイケル
      マイケル
      それでは今日はこの辺で! アデューーー!!
      エレキベア
      エレキベア
      クマ〜〜〜〜〜

      【Node.js】廃止されたAmazonアソシエイト画像リンクをAmazon Product Advertising API経由で復活させる 〜完〜


      フロントエンド関連JavaScriptツール開発WordPress関連Node.js
      2024-01-08

      関連記事
      【Unity】Timeline × Excelでスライドショーを効率よく制作する
      2024-10-31
      【ゲーム数学】第九回 p5.jsで学ぶゲーム数学「フーリエ解析」
      2024-05-12
      【都会のエレキベア】ブログを大幅リニューアル!WordPressからNext.jsに移行するまでの流れをまとめる
      2024-01-01
      【Next.js】第四回 WordPressブログをNext.jsに移行する 〜サーバ移行・SEO・広告設定編〜
      2023-12-31
      【Next.js】第三回 WordPressブログをNext.jsに移行する 〜Markdown執筆環境構築編〜
      2023-12-31
      【Next.js】第二回 WordPressブログをNext.jsに移行する 〜WordPressデータの移行・表示編〜
      2023-12-31
      【Next.js】第一回 WordPressブログをNext.jsに移行する 〜全体設計、環境構築編〜
      2023-12-31
      【Electron × Vue3】カテゴリ情報のCSVデータを操作するツールを作る
      2023-12-31