28
Help us understand the problem. What are the problem?

posted at

updated at

Organization

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

2022年4月1日、現状のGoogle検索結果で動作するように更新しました。
ニュースタブ・画像タブで動作します。

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

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

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

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

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

使い方

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

javascript:((e,t,i,n,r,o,d)=>{const c=(t,i=e)=>i.querySelector(t),s=(t,i=e)=>i.querySelectorAll(t),a=(t,i=e.body)=>i.appendChild(t),l=t=>e.createElement(t),v=()=>{void 0!==k&&k.remove()},u=(e,t,i=[])=>e.concat(t).concat(i),p=e=>`"${e.map(e=>I(e&&"string"!=typeof e&&d in e?e[d]:e,[/"/g,/\n/g],['""',""])).join('","')}"`,f=e=>{let t=e;try{t=decodeURI(e)}catch(e){}return t},h=()=>{const t=new Blob([new Uint8Array([239,187,191]),y(u([p(["href","decoded","title","breadcrumb","date","description"])],C))],{type:"text/csv"}),n=l("a"),r=URL.createObjectURL(t);n.download=e.title+".csv",n.href=r,n.click(),i(()=>URL.revokeObjectURL(r),1e3)},y=e=>e.join("\n"),m=(()=>{const e=s("#rso .g");return 0===e[o]?c("#rso"):e[e[o]-1].parentNode})(),b=async()=>{const e=U();if(0===e[o])return void alert("No item found.\nIf you think this script works incorrectly contact the creator.\nThx in advance.");const t=[`Found: ${e[o]}`,`Total: ${e[o]+C[o]}`];C=u(C,e),await L();const i=c("#pnnext",j);if(null===i)return alert(y(t)),v(),h();t.unshift("Continue next page?"),confirm(y(t))?(await g(i.href),b()):(v(),h())},w=(e,t)=>Array.from(s(e,j)).map(t),g=i=>{void 0===k&&(k=l("iframe"),R(k,{display:"none"}),a(k));const o=l("div");return R(o,{background:"black",color:"white",left:0,padding:"8px 20px",position:"fixed",top:0,zIndex:1e3}),o[d]="Loading...",a(o),new t(t=>{k[r]("load",()=>{j=k.contentWindow.document,o.remove();const i=l("div");i[n]=c("#rso",j)[n],a(i,m),j!==e&&(c("#xjs")[n]=c("#xjs",j)[n]),t()},{once:!0}),k.src=i})},x=(e,t,i="")=>(e||"").replace(t,i),I=(e,t,i)=>t.reduce((e,t,n)=>x(e,t,i[n]),e),U=()=>(e=>e.filter(e=>!!e))(u(w("#rso div.g > div",e=>{const t=c("div.yuRUbf a, div.ct3b9e a",e);if(!t)return!1;const i=t.href;let n,r,s,a;return c("div.dXiKIc",e)?(n=c("div.ct3b9e h3",e),r=(r=c("div.dXiKIc div.P7xzyf",e)).firstChild[d]+" · "+r.children[1][d],s=c("div.dXiKIc div.P7xzyf span span",e),a=c("div.dXiKIc div.Uroaid",e)):(n=c("div.yuRUbf h3",e),r=c("div.yuRUbf cite",e),s=c("div.uUuwM span.wuQ4Ob span, div.IsZvec div.uo4vr span",e),a=c("div.uUuwM div.VwiC3b, div.IsZvec div.VwiC3b, div.IsZvec span.aCOpRe",e)),p([i,f(i),n,r,"string"==typeof s?s:s&&0===s.children[o]?x(s[d],/ . ?$/):"",a?s?x(a[d],s[d]).trimLeft():a[d]:""])}),w("#rso div > div > a.WlydOe",e=>{const t=e.href;return p([t,f(t),c("[role=heading]",e),c("div.CEMjEf span",e),c("div.ZE0LJd span",e),c("div.GI74Re",e)])}),w("#islrg div.isv-r",e=>{const t=c('a[rel="noopener"]',e);if(null===t)return!1;const i=t.href,n=c("img",e).src;return p([i,f(i),t.title,c("div.fxgdke",t),c("span.dP3z3e",e),n.startsWith("data:")?"":n])}))),L=()=>new t(t=>{(c("#islmp")||e.body).scrollIntoView({behavior:"smooth",block:"end"});const n=()=>{clearTimeout(o),o=i(()=>{e.removeEventListener("scroll",n),t()},500)};let o;e[r]("scroll",n,{passive:!0}),n()}),R=(e,t)=>{for(let i in t)e.style[i]=t[i]};let k,j=e,C=[];b()})(document,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) とは、ユーザーがウェブブラウザのブックマークなどから起動し、なんらかの処理を行う簡易的なプログラムのことである。携帯電話のウェブブラウザで足りない機能を補ったり、ウェブアプリケーションの処理を起動する為に使 ...

対象ブラウザ

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

動作確認結果

2022年4月1日時点

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

ソースコード

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

ここをクリックしてソースコードを表示
ソースコード
((document, 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, more = []) => base.concat(append).concat(more)

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

  const decode = href => {
    let out = href

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

    return out
  }

  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 filter = array => array.filter(item => !!item)

  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()

    if (found[size] === 0) {
      alert('No item found.\nIf you think this script works incorrectly contact the creator.\nThx in advance.')
      return
    }

    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 replaceArr = (string, from, to) => from.reduce((prev, pattern, index) => replace(prev, pattern, to[index]), string)

  const scan = () => filter(concat(
    // All tab
    mapElements('#rso div.g > div', item => {
      const a = $('div.yuRUbf a, div.ct3b9e a', item)
      if (!a) {
        return false
      }

      const href = a.href
      let title
      let breadcrumb
      let date
      let desc

      if ($('div.dXiKIc', item)) {
        // YouTube
        title = $('div.ct3b9e h3', item)
        breadcrumb = $('div.dXiKIc div.P7xzyf', item)
        breadcrumb = breadcrumb.firstChild[text] + ' · ' + breadcrumb.children[1][text]
        date = $('div.dXiKIc div.P7xzyf span span', item)
        desc = $('div.dXiKIc div.Uroaid', item)
      } else {
        title = $('div.yuRUbf h3', item)
        breadcrumb = $('div.yuRUbf cite', item)
        date = $('div.uUuwM span.wuQ4Ob span, div.IsZvec div.uo4vr span', item)
        desc = $('div.uUuwM div.VwiC3b, div.IsZvec div.VwiC3b, div.IsZvec span.aCOpRe', item)
      }

      return convert([
        href, // href
        decode(href), // decoded
        title, // title
        breadcrumb, // breadcrumb
        typeof date === 'string' // date
          ? date
          : date && date.children[size] === 0 ? replace(date[text], / . ?$/) : '',
        desc // description
          ? (
            date
              ? replace(desc[text], date[text]).trimLeft()
              : desc[text]
            )
          : ''
      ])
    }),

    // News tab
    mapElements('#rso div > div > a.WlydOe', item => {
      const href = item.href

      return convert([
        href, // href
        decode(href), // decoded
        $('[role=heading]', item), // title
        $('div.CEMjEf span', item), // breadcrumb
        $('div.ZE0LJd span', item), // date
        $('div.GI74Re', item) // description
      ])
    }),

    // Images tab
    mapElements('#islrg div.isv-r', item => {
      const link = $('a[rel="noopener"]', item)
      if (link === null) {
        return false
      }
      const href = link.href
      const src = $('img', item).src

      return convert([
        href, // href
        decode(href), // decoded
        link.title, // title
        $('div.fxgdke', link), // breadcrumb
        $('span.dP3z3e', item), // date
        src.startsWith('data:') ? '' : src // description
      ])
    })
  ))

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

      const listener = () => {
        clearTimeout(timer)
        timer = setTimeout(() => {
          document.removeEventListener('scroll', listener)
          resolve()
        }, 500)
      }
      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, Promise, setTimeout, 'innerHTML', 'addEventListener', 'length', 'innerText')

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

免責

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

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
Sign upLogin
28
Help us understand the problem. What are the problem?