Tony Wang9 分で読めますGoogle Sheets にスクレイピング — IMPORTXML から API へ
Web サイトを Google Sheets にスクレイピングする方法:実際の XPath を使う IMPORTXML、IMPORTHTML、IMPORTDATA、Apps Script の自動更新、そして Python + API パイプライン。
Web サイトを Google Sheets にスクレイピングするには 3 つの段階があり、それぞれが、下の段階が壁にぶつかったからこそ存在します。段階 1 は組み込みのインポート数式 — IMPORTHTML、IMPORTXML、IMPORTDATA — で、無料・即座・静的ページ限定です。段階 2 は UrlFetchApp と時間主導トリガーを使う Apps Script で、本物のスケジューリングと JSON 処理が手に入ります。段階 3 は、スクレイピング API を呼び出して gspread で行を書き込む短い Python スクリプトで、サイトが抵抗してくるときに手を伸ばすものです。このガイドでは 3 つすべてを、動く数式、完全なスクリプト、そしてそれぞれがどこで壊れるかの正直な説明とともに、深く掘り下げます。(データはむしろ Excel に欲しいですか? 姉妹ガイドの scrape a website to Excel が、同じパイプラインの Power Query とブック側を扱っています。)
段階 1: 組み込みのインポート数式を正しく使う
多くのチュートリアルは IMPORTXML の例を 1 つ見せて先へ進みます。標準機能はもっと評価されていい — 限界の内側で使えば、これは存在する中で最速のスクレイピングツールです。インストールするものも、認証するものも何もないからです。
IMPORTHTML — 位置でテーブルとリストを取得
IMPORTHTML(url, query, index) はページ上の N 番目の table または list 要素を取得します。データがすでに HTML テーブルに収まっているページ — Wikipedia、政府統計、スポーツの順位表 — なら、これで作業は完了です。
=IMPORTHTML("https://en.wikipedia.org/wiki/List_of_countries_by_population_(United_Nations)", "table", 1)
最初のインデックスが間違ったテーブルを返す場合、当て推量せず、列挙しましょう。このヘルパーを作業用セルに置くと、ページ上のすべてのテーブルにラベルが付き、正しいインデックスを一度で見つけられます。
=INDEX(IMPORTHTML("https://example.com/stats", "table", 2), 1, 1)
末尾の 2 を上へ変えていき、プレビューに欲しいヘッダーが出たら、本番の数式をそのインデックスに向けます。ul や ol 要素には、第 2 引数に "list" を同じように使います。
IMPORTXML — それ以外すべてには XPath
IMPORTXML(url, xpath_query) は精密機器です。ページを取得してマークアップに対して XPath クエリを実行するので、テーブル全体ではなく、ちょうど 1 つのフィールドを取り出せます。実際に使う本物のクエリはこちら。
=IMPORTXML("https://example.com/product", "//h1")
=IMPORTXML("https://example.com/product", "//span[@class='price']")
=IMPORTXML("https://example.com/blog", "//a/@href")
=IMPORTXML("https://example.com/blog", "//article//h2[contains(@class, 'title')]")
=IMPORTXML("https://example.com", "//meta[@name='description']/@content")
1 つ目はページタイトルを、2 つ目はクラス指定の価格 span を、3 つ目はページ上のすべてのリンク URL を、4 つ目はクラスが単に特定の値を含む見出しを(サイトが安定した接頭辞つきのハッシュ化クラス名を使うときに便利)、5 つ目はまったく表示されずにレンダリングされるメタデータを取得します。XPath 属性はプレーンテキストで返り、複数のノードに一致するクエリは自動的に列を下へ流れます。オプションの第 3 引数は、ソースページの数値フォーマットがシートと異なる場合にパース用のロケールを設定します。
クエリが何も返さない場合、まずブラウザでテストします。対象ページで DevTools を開き、コンソールで $x("//span[@class='price']") を実行します。DevTools はノードを見つけるのに Sheets が見つけない場合、その要素はページ読み込み後に JavaScript で生成されており — 標準機能の越えられない限界を見つけたことになります(詳しくは後述)。現実のマークアップに対して XPath を書くことがここでの中核スキルです。ページ構造が常に動く標的である理由は、HTML parsing の用語集項目 で解説しています。
IMPORTDATA — 過小評価された関数
IMPORTDATA(url) は CSV または TSV を返す任意の URL を取り込みます — XPath もインデックスも不要、構造化データがそのままセルに入ります。
=IMPORTDATA("https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.zip?format=csv")
=IMPORTDATA("https://data.cityofnewyork.us/api/views/erm2-nwe9/rows.csv?$limit=500")
政府のオープンデータポータル、金融のリファレンスフィード、そして多くの SaaS のエクスポートエンドポイントは、素の CSV URL を公開しており、IMPORTDATA はそれらをネイティブのデータソースのように扱います。XPath を書く前に、サイトがひっそり CSV エンドポイントを提供していないか確認しましょう — それは、ページをパースするのと、答えをただ読むのとの違いです。(この区別 — レンダリング済みページをパースするか、構造化エンドポイントを消費するか — こそが web scraping vs API の全テーマです。)
3 つすべてが壊れるところ
- JavaScript レンダリングなし。 数式はサーバーが最初のリクエストで返す生の HTML を読みます。コンテンツをクライアント側で埋め込むサイト — Amazon、Google の検索結果、E コマースやダッシュボードの大半 — は Sheets に空の殻を渡すので、数式は
#N/A、「Imported content is empty」、あるいは何も返しません。 - 更新は浅い。 Google のドキュメントによると、
IMPORTDATA、IMPORTHTML、IMPORTXMLはおおよそ 1 時間ごと、しかもドキュメントを開いている間だけ更新を確認します。閉じたシートは決して更新されず、単に開き直しても再取得は強制されません。 - トラフィック制限と止まった「Loading...」セル。 Google は、作成者の開いているすべてのドキュメント全体で計算されるインポート関数の使用制限を課します。よく引用される実用上の上限は、動作が悪化し始める前で 1 スプレッドシートあたり約 50 個のインポート数式で、多用すると永遠に「Loading...」と表示する悪名高いセルが生まれます — 「Loading data may take a while because of the large number of requests」というエラーに対する Google 自身の案内は、単にインポート関数を減らせ、というものです。確認できるキューも再試行ボタンもなく、失敗のかたちは沈黙です。インポート数式は
NOW()やRANDBETWEEN()のような揮発性関数の参照も拒否し(TODAY()だけが唯一の例外)、古典的なキャッシュ回避のトリックを潰します。 - Google のフェッチャーがブロックされる。 すべてのインポート数式は Google の共有インフラから取得します — 地球上のすべての Sheets ユーザーに対して同じ IP 範囲です。何らかの rate limiting やアンチボット層を持つサイトは、それらの IP を何十億回も見てきており、まとめてブロックします。だからこそ、ブラウザでは問題なく読み込めるページが、完全に静的であっても Sheets では
#N/Aを返すことがあるのです。
評価: 協力的なサイト上の静的テーブル、CSV フィード、メタデータには無敵。スケジュール、JavaScript でレンダリングされるページ、または保護されたサイトが必要になった瞬間、1 段階上げましょう。
段階 2: Apps Script — UrlFetchApp と時間主導トリガー
Apps Script は、ほとんどのスプレッドシートユーザーが決して踏み出さないエスカレーションで、4 つの壁のうち 2 つを無料で解決します。本物のスケジューリングと本物のエラー処理です。UrlFetchApp はカスタムヘッダー付きで HTTP リクエストを送り、時間主導トリガーはあなたの関数を時計仕掛けで再実行します — シートを閉じているときでさえ、どのインポート数式にもできないことです。
シートを開き、Extensions → Apps Script に移動して、この完全なスクリプトを貼り付けます。これは JSON API(ここでは Crawlora の Amazon 検索エンドポイント — どんな JSON エンドポイントでも同じように動きます)を呼び出し、ステータスコードを確認し、results という名前のタブを書き直します。
function refreshSheet() {
const API_KEY = PropertiesService.getScriptProperties()
.getProperty("CRAWLORA_API_KEY");
const url =
"https://api.crawlora.net/api/v1/amazon/search?k=" +
encodeURIComponent("mechanical keyboard");
const resp = UrlFetchApp.fetch(url, {
headers: { "x-api-key": API_KEY },
muteHttpExceptions: true, // return the response instead of throwing
});
if (resp.getResponseCode() !== 200) {
console.error("Fetch failed: HTTP " + resp.getResponseCode());
return;
}
const rows = JSON.parse(resp.getContentText()).data;
const header = ["asin", "title", "price", "rating", "review_count", "link"];
const values = rows.map((r) =>
header.map((h) => (r[h] == null ? "" : r[h]))
);
const sheet =
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("results");
sheet.clearContents();
sheet.getRange(1, 1, 1, header.length).setValues([header]);
if (values.length) {
sheet.getRange(2, 1, values.length, header.length).setValues(values);
}
}
function installTrigger() {
ScriptApp.newTrigger("refreshSheet").timeBased().everyHours(6).create();
}
セットアップの注意点が 3 つ。API キーは Project Settings → Script properties に CRAWLORA_API_KEY として保存します — シークレットをセルやコードに貼り付けないこと。エディターから installTrigger() を一度実行する(または Triggers パネルで同じことを設定する)と、Google は約 6 時間ごとに refreshSheet() を呼び出します。時間主導トリガーは cron のように振る舞いますが、Google は負荷を分散するため時間枠内の正確な分をランダムに選びます。そして muteHttpExceptions: true はそのままにしておきましょう — 失敗した取得を、午前 3 時の赤いエラーではなく、ログして処理できるステータスコードに変えてくれます。
限界を正直に。Apps Script のリクエストは 6 分後にタイムアウトし、URL は 2,082 文字に制限され、コンシューマーアカウントには 1 日あたりの UrlFetch とトリガーのクォータの上限があり、そして — これが大きい — リクエストは依然として Google の IP 範囲から発信されます。保護されたサイトに対しては、Apps Script は IMPORTXML とまったく同じようにブロックされます。ターゲットから見れば、同じクローラーがノックしているからです。Apps Script が解決するのはスケジューリングと JSON であって、アクセスではありません。だからこそ上のスクリプトは、生のページではなく API を呼び出しているのです。
段階 3: Python + スクレイピング API + gspread
ターゲットサイトが JavaScript でレンダリングする、マークアップをローテーションする、あるいはクローラーを積極的にブロックする場合、ページを取得するのをやめて、答えを取得し始めましょう。scraping API はレンダリング、プロキシのローテーション、リトライを自分の側で行い、サイトがリニューアルされても同じ正規化された JSON を返します — そして gspread はその JSON を 3 行でシートの行に変えます。これが本番との接触を生き延びるセットアップです。(代わりにスクレイパーを自作するのも正当な道です — ピラー記事 web scraping with Python が DIY ツールキット全体を扱っています — が、スプレッドシートのパイプラインには、API ルートが保守を消し去ってくれます。)
一度きりの Google セットアップ、5 分。Google Cloud Console でプロジェクトを作成し、Google Sheets API と Google Drive API を有効にし、サービスアカウント を作成し、その JSON キーをダウンロードし、スプレッドシートをサービスアカウントのメールアドレス(iam.gserviceaccount.com で終わります)と、同僚と共有するのとまったく同じように共有します。それから:
pip install requests gspread
import requests
import gspread
from datetime import datetime, timezone
API_KEY = "YOUR_API_KEY" # free tier: 2,000 credits/month, no card
resp = requests.get(
"https://api.crawlora.net/api/v1/amazon/search",
params={"k": "mechanical keyboard"},
headers={"x-api-key": API_KEY},
timeout=60,
)
resp.raise_for_status()
items = resp.json()["data"] # normalized result cards
gc = gspread.service_account(filename="service-account.json")
ws = gc.open("Keyboard price tracker").worksheet("log")
stamp = datetime.now(timezone.utc).isoformat(timespec="seconds")
rows = [
[stamp, it["asin"], it["title"], it["price"], it["rating"], it["link"]]
for it in items
]
ws.append_rows(rows, value_input_option="USER_ENTERED")
print(f"Appended {len(rows)} rows at {stamp}")
Apps Script 版との設計上の違いに注目してください。こちらは上書きではなく、タイムスタンプ付きの行を 追記 するので、シートは単なるスナップショットではなく、チャート化やピボットができる価格履歴になります。k キーワードを差し替えるか、エンドポイントごと差し替えましょう — プラットフォームカタログ が対応サイトを、docs が各エンドポイントのフィールドを一覧し、playground では 1 行書く前にブラウザでテストリクエストを撃てます。pricing は pay-on-success — 課金は成功した 2xx レスポンスに対してのみ — なので、ブロックや失敗した取得はリトライのコストであって、クレジットは減りません。cron か GitHub Actions のワークフローでスケジュールしましょう。Excel の姉妹ガイド には、ここでもそのまま動く完全なコピペ用 Actions YAML があります。そしてどんなスケジュールでも、礼儀正しく — 毎日 1 回の取得は責任ある crawling ですが、叩き続けるループは IP がブロックリストに載る道です。
どの段階を使うべきか?
| インポート数式 | Apps Script + トリガー | Python + スクレイピング API | |
|---|---|---|---|
| セットアップ時間 | 1 分未満 | 15〜30 分 | 約 15 分 |
| JavaScript の重いサイト | 不可 | API 経由でのみ | 可 |
| ボット対策のあるサイト | 不可(共有の Google IP) | 不可(同じ Google IP) | 可(API が処理) |
| シートを閉じた状態での更新 | 不可 | 可 — 時間主導トリガー | 可 — cron / Actions |
| エラーの可視性 | 止まった「Loading...」/ #N/A | ステータスコード、ログ | 完全:例外、リトライ、アラート |
| サイトリニューアルへの耐性 | なし(XPath が壊れる) | あり(API の JSON なら) | あり(正規化 JSON) |
| コスト | 無料 | 無料(クォータ制約あり) | 無料枠 2,000 クレジット/月、pay-on-success |
ペルソナ別に:
- 静的テーブルと締め切りを抱えたアナリスト:
IMPORTHTML、60 秒で完了。まず CSV エンドポイントを確認し、あればIMPORTDATAを使いましょう。 - 共有シートで 1 ページを毎日更新する必要があるオペレーション担当: 時間主導トリガー付きの Apps Script。本当にスケジュール実行できる最も安価な選択肢で、上のスクリプトがそのすべてです。
- 動的・保護された・多数のサイトをスクレイピングする人: Python + API + gspread。ブロックされたサイトの問題が誰か他の人の仕事になる唯一の段階です。
- すでに Excel で暮らしているチーム: 同じパイプライン、違うのは最後の一歩だけ — Excel ガイド が Power Query と
to_excel()を扱っています。
- データは最初のサーバー HTML に含まれているか? まず IMPORTHTML か IMPORTXML を試す — XPath は DevTools で $x() を使って確認する。
- CSV エンドポイントはあるか? 存在するなら IMPORTDATA が毎回 XPath に勝つ。
- シートを閉じている間の更新が必要か? インポート数式にはできない — Apps Script の時間主導トリガーを使う。
- サイトは JavaScript レンダリングか、Google のフェッチャーをブロックしているか? スクレイピング API 経由にして gspread で行を書き込む。
- API キーは Script properties か環境シークレットに置き、セルには絶対に置かず、取得の頻度は礼儀正しく保つ。
対応サイトを生きた Google Sheet に変える
GET リクエスト 1 回で正規化された JSON が返り、あとは Apps Script か gspread が仕上げます。毎月 2,000 クレジット無料、カード不要、pay-on-success: 課金されるのは成功した 2xx レスポンスだけです。
関連記事: このパイプラインの Power Query とブック側を扱う Excel の姉妹ガイド、DIY ツールキット全体を扱うピラー記事 web scraping with Python、そしてどちらのアプローチがいつ勝つかを解説する web scraping vs API。
よくある質問
Web サイトを Google Sheets にスクレイピングするには?
まず組み込みの関数から始めます。IMPORTHTML はページの N 番目のテーブルやリストを取得し、IMPORTXML は XPath クエリで特定の要素を抽出し、IMPORTDATA は公開の CSV または TSV の URL を取り込みます。ページが JavaScript でレンダリングされる、スケジュールが必要、または Google のフェッチャーをブロックする場合は、UrlFetchApp を使う Apps Script か、スクレイピング API を呼び出して gspread で行を書き込む Python スクリプトへとエスカレートします。
IMPORTXML が Loading... と表示したり #N/A を返したりするのはなぜ?
セルが Loading... のまま止まる場合、たいていは Google のインポート関数のトラフィック制限に達したことを意味します — 解決策は、開いているスプレッドシート全体でインポート数式を減らすことです。#N/A や空の結果は、たいてい要素がページ読み込み後に JavaScript で生成されている(Sheets は最初のサーバー HTML しか読みません)か、サイトが Google の共有フェッチャー IP をブロックしていることを意味します。
Google Sheets は JavaScript でレンダリングされるページをスクレイピングできる?
いいえ。IMPORTHTML、IMPORTXML、IMPORTDATA は、サーバーが最初のリクエストで返す生の HTML しか読まず、ブラウザによるレンダリングのステップはありません。コンテンツをクライアント側で埋め込むサイト — Amazon、Google の検索結果、最近の Web アプリの大半 — は空の結果を返します。回避策は、スクレイピング API の正規化された JSON を、代わりに Apps Script の UrlFetchApp か Python スクリプトで取得することです。
1 つの Google Sheet に IMPORT 関数はいくつ置ける?
Google は固定数ではなく、開いているすべてのドキュメント全体で計算される厳格なトラフィックベースの制限を文書化していますが、よく引用される実用上の上限は、セルが Loading... で止まり始める前で 1 スプレッドシートあたり約 50 個のインポート数式です。それを超えたら、数式をまとめるか、取得処理を Apps Script か外部スクリプトに移しましょう。
Google Sheets でスクレイピングしたデータを自動更新するには?
インポート数式はおおよそ 1 時間ごとに再チェックしますが、シートを開いている間だけです — 閉じたシートは決して更新されません。本物のスケジュールには、Apps Script の時間主導トリガー(例:ScriptApp.newTrigger().timeBased().everyHours(6))で UrlFetchApp の取得を再実行するか、Python + gspread のスクリプトを cron や GitHub Actions で走らせます。どちらもシートを閉じた状態で動きます。
IMPORTHTML、IMPORTXML、IMPORTDATA の違いは?
IMPORTHTML はページ上の位置でテーブルまたはリスト全体を取得します。IMPORTXML は XPath クエリを実行するので、特定の要素・属性・ノードの集合を抽出できます。IMPORTDATA は CSV または TSV を返す URL を取得し、そのままセルに読み込みます。まず CSV エンドポイントがないか確認しましょう — 存在する場合、IMPORTDATA は 3 つの中で最も堅牢です。
Google Sheets にスクレイピングするのに有料ツールは必要?
静的ページには不要です — インポート数式と Apps Script は無料です。JavaScript の重いサイトやボット対策のあるサイトにはスクレイピングのバックエンドが必要ですが、それも無料で始められます。Crawlora の無料枠はカード不要で月 2,000 クレジットを含み、料金は pay-on-success なので、課金されるのは成功した 2xx レスポンスに対してのみです。