Google Custom Searchを使って、Google検索結果をJSON形式で取得する

2023/06/23に公開

ある日、社内の別部署からこんな相談があった。

「10000件ちょっとある会社名のリストを使って、それぞれの会社のWebサイトのURLを取りたい」

それを取得してどうすると一瞬思ったが、色々のっぴきならない事情がある様子。情シスは社内のいろんなのっぴきならなさと寄り添う仕事なので、無碍に断るわけにはいかない。また、なんかGASでなんとかなりそうなかほりがしたので、ちょっと面白そうと引き受けることにした。

その依頼をわたしなりに都合よく解釈して、以下の要件のスクリプトを書くことにした。

会社名でGoogle検索をかけて、一番最初に出てきた検索結果のページのURLを取得する

やること

一瞬、Googleの検索クエリをそのまま叩いてスクレイピングすればOKなのでは?と思ったが、そうは問屋がおろさない。429エラーが返ってきてしまう。これはGoogle検索がbotから実行されるのを防ぐためだ。

そこで色々調べたところ、Google Custom Search APIなるもの経由で実行する必要があるとのこと。
つまりはGCPのAPIキーを発行した上で、Custom Search APIを有効化させる必要があるのだけど、その手順はいったん割愛する。個人的に書くのがめんどくさいのと、そのめんどくささを引き受けて詳しく書いてくれている先人がいるからだ。

方法についてはこちらの記事を参考にさせていただきました。内容は少し古いものの、基本的な流れ(=GCPのAPIキーを取得して、Custom Search Engineを作成してIDを取得する)とクエリは同じなので十二分に参考になりました。

https://qiita.com/kingpanda/items/54043eddcf09699ceabc

コード

スプレッドシート側で関数を指定して実行させる形式を取ることにした。まずはどんなレスポンスが取れるかを確認。

function urlSearch(str) {
  const sheet = SpreadsheetApp.getActiveSheet();
    //APIキー
  const key = "xxxxxxxxxxxxxxxxxxxxxxx";
  //検索エンジンID
  const eid = 'zzzzzzzzzzzzzzzzzzzzzzz';
  const encodeWord = encodeURI(str);
  //最初の一件だけを取得する
  const displayNum = 1;
  const url = "https://www.googleapis.com/customsearch/v1?key="+key+"&cx="+eid+"&q="+encodeWord+"&num="+displayNum;
  const res = UrlFetchApp.fetch(url).getContentText('UTF-8');
  Logger.log(res)
}

なお「トヨタ自動車株式会社」を検索ワードとして上のコードを実行するとこんな感じのレスポンスが取れる。

{
  "kind": "customsearch#search",
  "url": {
    "type": "application/json",
    "template": "https://www.googleapis.com/customsearch/v1?q={searchTerms}&num={count?}&start={startIndex?}&lr={language?}&safe={safe?}&cx={cx?}&sort={sort?}&filter={filter?}&gl={gl?}&cr={cr?}&googlehost={googleHost?}&c2coff={disableCnTwTranslation?}&hq={hq?}&hl={hl?}&siteSearch={siteSearch?}&siteSearchFilter={siteSearchFilter?}&exactTerms={exactTerms?}&excludeTerms={excludeTerms?}&linkSite={linkSite?}&orTerms={orTerms?}&relatedSite={relatedSite?}&dateRestrict={dateRestrict?}&lowRange={lowRange?}&highRange={highRange?}&searchType={searchType}&fileType={fileType?}&rights={rights?}&imgSize={imgSize?}&imgType={imgType?}&imgColorType={imgColorType?}&imgDominantColor={imgDominantColor?}&alt=json"
  },
  "queries": {
    "request": [
      {
        "title": "Google Custom Search - トヨタ自動車株式会社",
        "totalResults": "27000000",
        "searchTerms": "トヨタ自動車株式会社",
        "count": 1,
        "startIndex": 1,
        "inputEncoding": "utf8",
        "outputEncoding": "utf8",
        "safe": "off",
        "cx": "zzzzzzzzzzzzzzzzzz"
      }
    ],
    "nextPage": [
      {
        "title": "Google Custom Search - トヨタ自動車株式会社",
        "totalResults": "27000000",
        "searchTerms": "トヨタ自動車株式会社",
        "count": 1,
        "startIndex": 2,
        "inputEncoding": "utf8",
        "outputEncoding": "utf8",
        "safe": "off",
        "cx": "zzzzzzzzzzzzzzzz"
      }
    ]
  },
  "context": {
    "title": "xxxxx"
  },
  "searchInformation": {
    "searchTime": 0.405014,
    "formattedSearchTime": "0.41",
    "totalResults": "27000000",
    "formattedTotalResults": "27,000,000"
  },
  "items": [
    {
      "kind": "customsearch#result",
      "title": "トヨタ自動車株式会社 公式企業サイト",
      "htmlTitle": "\u003cb\u003eトヨタ自動車株式会社\u003c/b\u003e 公式企業サイト",
      "link": "https://global.toyota/",
      "displayLink": "global.toyota",
      "snippet": "企業情報、投資家情報、ニュースルーム、モビリティ、サステナビリティ、採用情報等、トヨタの企業情報全般を提供するトヨタ自動車の公式企業サイトです。",
      "htmlSnippet": "企業情報、投資家情報、ニュースルーム、モビリティ、サステナビリティ、採用情報等、トヨタの企業情報全般を提供する\u003cb\u003eトヨタ自動車\u003c/b\u003eの公式企業サイトです。",
      "cacheId": "8sZa090KyrkJ",
      "formattedUrl": "https://global.toyota/",
      "htmlFormattedUrl": "https://global.toyota/",
      "pagemap": {
        "cse_thumbnail": [
          {
            "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANdfaljkasdasdljklgjljlkgjslkjjlkjjjljlllllsjadl",
            "width": "275",
            "height": "183"
          }
        ],
        "metatags": [
          {
            "og:image": "https://global.toyota/pages/_system/image/tmb_corporate.png",
            "copyright": "(C) TOYOTA MOTOR CORPORATION. All Rights Reserved.",
            "og:type": "website",
            "twitter:card": "summary_large_image",
            "og:site_name": "トヨタ自動車株式会社 公式企業サイト",
            "author": "TOYOTA MOTOR CORPORATION.",
            "og:title": "トヨタ自動車株式会社 公式企業サイト",
            "og:description": "企業情報、投資家情報、ニュースルーム、モビリティ、サステナビリティ、採用情報等、トヨタの企業情報全般を提供するトヨタ自動車の公式企業サイトです。",
            "twitter:site": "@TOYOTA_PR",
            "viewport": "width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=2, shrink-to-fit=no",
            "cms-page-type": "default",
            "og:locale": "ja_JP",
            "sitename": "トヨタ自動車株式会社 公式企業サイト",
            "og:url": "https://global.toyota/jp/index.html",
            "format-detection": "telephone=no"
          }
        ],
        "cse_image": [
          {
            "src": "https://global.toyota/pages/_system/image/tmb_corporate.png"
          }
        ]
      }
    }
  ]
}

これをみた瞬間思わずニヤッとしてしまった。このように綺麗なjsonが取れたらもうこっちのもんだ。最終的に以下のような形にして、URLのみが取れるようにした。

function urlSearch(str) {
  const sheet = SpreadsheetApp.getActiveSheet();
    //APIキー
  const key = "xxxxxxxxxxxxxxxxxxxxxxx";
  //検索エンジンID
  const eid = 'zzzzzzzzzzzzzzzzzzzzzzz';
  const encodeWord = encodeURI(str);
  //最初の一件だけを取得する
  const displayNum = 1;
  const url = "https://www.googleapis.com/customsearch/v1?key="+key+"&cx="+eid+"&q="+encodeWord+"&num="+displayNum;
  const res = JSON.parse(UrlFetchApp.fetch(url).getContentText('UTF-8'));
  const targetUrl = res.items[0].link;
  return targetUrl;
}

まさかの落とし穴、そして道は続く

これはチョロかったなと思って、スプレッドシート側でオートフィルをしたところ...

"message": "Quota exceeded for quota metric 'Queries' and limit 'Queries per day' of service

なんやて!? クオータが存在するなんて聞いてないぞ!

調べているとこちらのページに欲しい情報があった。

カスタム検索 JSON API では、1 日あたり 100 件の検索クエリを無料で利用できます。他にも必要な場合は、API Console でお支払いに登録できます。追加リクエストの料金は、クエリ 1,000 件あたり 5 ドルで、1 日あたり 10,000 クエリまでとなります。

無料枠は100件!しかも一日10000クエリ!こっちは10000件ちょっとあるんだぞ!

全クエリ実行に想定される費用は50ドル、いまだと7000円。しかたない課金して日を跨いでちまちま実行するか...と思ったが、最近会社の購買周りが超絶めんどくさくなったこともあり、その労力で7000円超えるんじゃないかと思い直し、なんとかタダで同じようなことをする方策を取ることにした。

つづく(!)

Discussion