Help us understand the problem. What is going on with this article?

Google検索結果の一覧をCSVでダウンロードするブックマークレット

2021年1月、現状のGoogle検索結果で動作するように更新しました。
次のページを取得することもできます。
また、ニュースタブでも動作します。

Googleの検索結果を手軽に一覧でCSV保存したい

Google検索はとても便利なので、日常的に利用しているのですが、
検索結果をまとめて一覧化したいことがありますよね。

検索画面に表示されるサイト名やページタイトル、URLをCSVファイルとして一覧で取得できれば、
ExcelやNumbers、Calcなどで使えて便利です。

スクレイピングやAPIで取得するなどの方法もありますが、
個人で利用するには、簡便に取得できれば十分でしたので、
ブラウザから誰でも利用できる、ブックマークレットを作りました。

ただし、最新のブラウザでなければ動作しません。

使い方

ブラウザで新しいブックマークを作り、次の内容をURL欄に貼り付けます。
ブックマークの名前は、Google検索結果CSVダウンロードなどで良いでしょう。
Googleの検索結果画面で、登録したブックマークをクリックすると動作します。

javascript:((e,t,n,o,r,i,c,d)=>{const a=(t,n=e)=>n.querySelector(t),s=(t,n=e)=>n.querySelectorAll(t),l=(t,n=e.body)=>n.appendChild(t),v=t=>e.createElement(t),p=()=>{void 0!==T&&T.remove()},u=(e,t)=>e.concat(t),f=e=>`"${e.map(e=>L((e&&"string"!=typeof e&&d in e?e[d]:e)||"",/"/g,'""')).join('","')}"`,m=e=>{let t=e;try{t=decodeURI(e)}catch(e){}return t},b=()=>{const t=new Blob([new Uint8Array([239,187,191]),h(u([f(["href","decoded","title","breadcrumb","date","description"])],I))],{type:"text/csv"}),n=v("a"),r=URL.createObjectURL(t);n.download=e.title+".csv",n.href=r,n.click(),o(()=>URL.revokeObjectURL(r),1e3)},h=e=>e.join("\n"),y=(()=>{const e=s("#rso .g");return 0===e[c]?a("#rso"):e[e[c]-1].parentNode})(),g=async()=>{const e=R(),t=[`Found: ${e[c]}`,`Total: ${e[c]+I[c]}`];I=u(I,e),await U();const n=a("#pnnext",k);if(null===n)return alert(h(t)),p(),b();t.unshift("Continue next page?"),confirm(h(t))?(await x(n.href),g()):(p(),b())},w=(e,t)=>Array.from(s(e,k)).map(t),x=t=>{void 0===T&&(T=v("iframe"),j(T,{display:"none"}),l(T));const o=v("div");return j(o,{background:"black",color:"white",left:0,padding:"8px 20px",position:"fixed",top:0,zIndex:1e3}),o[d]="Loading...",l(o),new n(n=>{T[i]("load",()=>{k=T.contentWindow.document,o.remove();const t=v("div");t[r]=a("#rso",k)[r],l(t,y),k!==e&&(a("#xjs")[r]=a("#xjs",k)[r]),n()},{once:!0}),T.src=t})},L=(e,t,n="")=>e.replace(t,n),R=()=>u(w("#rso div.rc",e=>{const t=a("div.yuRUbf a",e).href,n=a("div.IsZvec span.f",e),o=a("div.IsZvec span.aCOpRe",e);return f([t,m(t),a("div.yuRUbf h3",e),a("div.yuRUbf cite",e),n&&0===n.children[c]?L(n[d],/ . $/):"",o?n?L(o[d],n[d]).trimLeft():o[d]:""])}),w("#rso g-card div.dbsr",e=>{const t=a("div.dbsr > a",e).href;return f([t,m(t),a("[role=heading]",e),a("div.XTjFC",e),a("span.WG9SHc",e),a("div.Y3v8qd",e)])})),U=()=>new n(t=>{e.body.scrollIntoView({behavior:"smooth",block:"end"});const n=()=>{clearTimeout(r),r=o(()=>{e.removeEventListener("scroll",n),t()},100)};let r;e[i]("scroll",n,{passive:!0}),n()}),j=(e,t)=>{for(let n in t)e.style[n]=t[n]};let T,k=e,I=[];g()})(document,navigator,Promise,setTimeout,"innerHTML","addEventListener","length","innerText")

ページをブックマークしておき、編集画面を選んだ後、URL欄にペーストするのがコツです。

動作イメージ

Google Chromeのブックマークツールバーを表示させている場合のイメージです。

Google検索結果画面でブックマークをクリックすると、ブックマークレットが実行され、ダイアログが表示されます。
OK をクリックすると、次のページ以降の検索結果を取得します。
キャンセル をクリックすると、CSVファイルがダウンロードされます。

次のページがない場合は OK ボタンのみ表示され、クリックするとすぐにCSVファイルがダウンロードされます。

検索結果画面でブックマークを選択.png

2021年1月最新版では、ダウンロードされるCSVファイル名が、検索したキーワードとなります。

ダウンロードしたCSVファイルはそのまま開けます。
CSVの表示.png

保存されるCSV

UTF-8でエンコードされています。
Excel、Numbers、CalcやGoogle Spreadsheetでそのまま開けます。

ヘッダ 説明 空文字可能性 具体例
href URL NO https://ja.wikipedia.org/wiki/%E3%83%96%E3%83%83%E3%82%AF%E3%83%9E%E3%83%BC%E3%82%AF%E3%83%AC%E3%83%83%E3%83%88
decoded decodeURI()したURL NO https://ja.wikipedia.org/wiki/ブックマークレット
title サイト名 or ページ名 YES ブックマークレット - Wikipedia
breadcrumb パンくずの表示 YES https://ja.wikipedia.org/wiki/ブックマークレット
date 更新日 YES 2019/1/15
description ページの概要 YES ブックマークレット (Bookmarklet) とは、ユーザーがウェブブラウザのブックマークなどから起動し、なんらかの処理を行う簡易的なプログラムのことである。携帯電話のウェブブラウザで足りない機能を補ったり、ウェブアプリケーションの処理を起動する為に使 ...

対象ブラウザ

最新のブラウザを利用してください。

動作確認結果

2021年1月7日時点

ブラウザ バージョン 動作
Google Chrome 87.0.4280.88 OK
Firefox 84.0.1 OK
Safari 14.0.2 OK
Edge (Blink) 87.0.664.66 OK
Edge (EdgeHTML) 動作未確認 今後も対応しません。
Internet Explorer 11.1082.18362.0 NG
今後も対応しません。

ソースコード

ブックマークレット用に変換する前のソースコードです。


ここをクリックしてソースコードを表示
ソースコード
((document, navigator, Promise, setTimeout, html, on, size, text) => {
  const $ = (query, doc = document) => doc.querySelector(query)

  const $$ = (query, doc = document) => doc.querySelectorAll(query)

  const appendChild = (child, parent = document.body) => parent.appendChild(child)

  const createElement = tag => document.createElement(tag)

  const cleanup = () => {
    if (iframe !== undefined) {
      iframe.remove()
    }
  }

  const concat = (base, append) => base.concat(append)

  const convert = row => `"${row.map(s => replace((s && typeof s !== 'string' && text in s ? s[text] : s) || '', /"/g, '""')).join('","')}"`

  const decode = href => {
    let ret = href

    try {
      ret = decodeURI(href)
    } catch (e) {}

    return ret
  }

  const download = () => {
    const blob = new Blob([
      new Uint8Array([0xEF, 0xBB, 0xBF]),
      joinLines(concat([
        convert([
          'href',
          'decoded',
          'title',
          'breadcrumb',
          'date',
          'description'
        ])
      ], ret))
    ], {
      type: 'text/csv'
    })

    const a = createElement('a')
    const url = URL.createObjectURL(blob)

    a.download = document.title + '.csv'
    a.href = url

    a.click()

    setTimeout(() => URL.revokeObjectURL(url), 1000)
  }

  const joinLines = array => array.join('\n')

  const last = (() => {
    const g = $$('#rso .g')

    if (g[size] === 0) {
      return $('#rso')
    } else {
      return g[g[size] - 1].parentNode
    }
  })()

  const main = async () => {
    const found = scan()
    const message = [
      `Found: ${found[size]}`,
      `Total: ${found[size] + ret[size]}`
    ]

    ret = concat(ret, found)

    await scroll()

    const pnnext = $('#pnnext', doc)

    if (pnnext === null) {
      alert(joinLines(message))

      cleanup()

      return download()
    }

    message.unshift('Continue next page?')

    if (confirm(joinLines(message))) {
      await next(pnnext.href)

      main()
    } else {
      cleanup()
      download()
    }
  }

  const mapElements = (query, callback) => Array.from($$(query, doc)).map(callback)

  const next = href => {
    if (iframe === undefined) {
      iframe = createElement('iframe')

      setStyle(iframe, {
        display: 'none'
      })

      appendChild(iframe)
    }

    const progress = createElement('div')

    setStyle(progress, {
      background: 'black',
      color: 'white',
      left: 0,
      padding: '8px 20px',
      position: 'fixed',
      top: 0,
      zIndex: 1000
    })

    progress[text] = 'Loading...'

    appendChild(progress)

    return new Promise(resolve => {
      iframe[on]('load', () => {
        doc = iframe.contentWindow.document
        progress.remove()

        const wrapper = createElement('div')
        wrapper[html] = $('#rso', doc)[html]
        appendChild(wrapper, last)

        if (doc !== document) {
          $('#xjs')[html] = $('#xjs', doc)[html]
        }

        resolve()
      }, {
        once: true
      })

      iframe.src = href
    })
  }

  const replace = (string, from, to = '') => string.replace(from, to)

  const scan = () => concat(
    mapElements('#rso div.rc', rc => {
      const href = $('div.yuRUbf a', rc).href
      const date = $('div.IsZvec span.f', rc)
      const desc = $('div.IsZvec span.aCOpRe', rc)

      return convert([
        href,
        decode(href),
        $('div.yuRUbf h3', rc),
        $('div.yuRUbf cite', rc),
        date && date.children[size] === 0 ? replace(date[text], / . $/) : '',
        desc ? (date ? replace(desc[text], date[text]).trimLeft() : desc[text]) : ''
      ])
    }),
    mapElements('#rso g-card div.dbsr', dbsr => {
      const href = $('div.dbsr > a', dbsr).href

      return convert([
        href,
        decode(href),
        $('[role=heading]', dbsr),
        $('div.XTjFC', dbsr),
        $('span.WG9SHc', dbsr),
        $('div.Y3v8qd', dbsr)
      ])
    })
  )

  const scroll = () => {
    return new Promise(resolve => {
      document.body.scrollIntoView({
        behavior: 'smooth',
        block: 'end'
      })

      const listener = () => {
        clearTimeout(timer)
        timer = setTimeout(() => {
          document.removeEventListener('scroll', listener)
          resolve()
        }, 100)
      }
      let timer

      document[on]('scroll', listener, {
        passive: true
      })
      listener()
    })
  }

  const setStyle = (target, style) => {
    for (let key in style) {
      target.style[key] = style[key]
    }
  }

  let doc = document
  let iframe
  let ret = []

  main()
})(document, navigator, Promise, setTimeout, 'innerHTML', 'addEventListener', 'length', 'innerText')

※ Minifierはこちらを使用しています。
※ Minify後の文字数をなるべく削減するために、少々トリッキーなコーディングをしています。

免責

Google検索結果画面の仕様が変わると、動作しなくなることが予想されます。
その際にはご容赦ください。

tabian
画像認識技術を用いて、新しいポスター体験を提供する「ポ写」を開発している全員副業のスタートアップです! エンジニアが自由に、でも真面目に技術に向き合える環境、それは最新技術にとことんこだわれることだと思って日々Qiitaの更新頑張ってます。
https://tabian.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away