← All posts

Scrape Data Into Google Sheets: IMPORTXML, Apps Script, and APIs

Quick answer: To scrape data into Google Sheets, start with the built-in IMPORTXML and IMPORTHTML formulas for simple, static pages. When those break on JavaScript-heavy sites or you need scheduling and cleanup, switch to Apps Script, and for pages that block basic requests, have Apps Script call a fetch API that returns clean content. Move to real code once your needs outgrow a spreadsheet.

Google Sheets is a surprisingly capable scraping tool for small jobs. You get formulas, a scripting layer, and scheduling without leaving the browser. The trick is knowing which layer to use, because each one has a ceiling you will hit sooner than you expect.

Layer 1: IMPORTXML and IMPORTHTML

These formulas pull data straight into a cell. No code, no setup.

IMPORTHTML grabs whole tables or lists by index:

=IMPORTHTML("https://en.wikipedia.org/wiki/List_of_countries_by_population", "table", 1)

That reads the first table on the page. Change "table" to "list" and the index to target a different element.

IMPORTXML is more surgical. It takes a URL and an XPath query:

=IMPORTXML("https://example.com/product", "//h1")
=IMPORTXML("https://example.com/product", "//span[@class='price']")

The first pulls the page's main heading; the second grabs the price by its CSS class. XPath looks intimidating but you only need a few patterns: //tag for any element of a type, //tag[@class='x'] to filter by attribute, and //div[@id='x']//a to reach nested links.

This works beautifully right up until it does not.

Where the formulas fall down

Limitation What happens
JavaScript-rendered pages Formula returns empty; the data loads after the HTML does
Anti-bot protection Site returns a block page or CAPTCHA instead of content
Rate limits / caching Sheets caches aggressively and refreshes on its own schedule
Fragile selectors A layout tweak on the site breaks every formula
Volume Too many IMPORT formulas make the whole sheet slow and flaky
Cleanup No good place to transform, dedupe, or reshape the data

The most common failure is the first one. A huge share of modern sites render their content with JavaScript, and IMPORTXML only sees the initial HTML. You get #N/A or a blank cell and no explanation. When that happens, formulas have taken you as far as they can.

Layer 2: Apps Script

Apps Script is JavaScript that runs inside Google Sheets. It gives you three things the formulas lack: real logic, scheduling, and the ability to call any HTTP API.

Open Extensions > Apps Script and you get a code editor bound to your sheet.

A basic fetch with the built-in UrlFetchApp:

function scrapeSimple() {
  const response = UrlFetchApp.fetch("https://example.com");
  const html = response.getContentText();
  const sheet = SpreadsheetApp.getActiveSheet();
  sheet.getRange("A1").setValue(html.length + " chars fetched");
}

UrlFetchApp fetches raw HTML, so it hits the exact same wall as IMPORTXML: JavaScript pages come back empty, and protected pages return block responses. It also has no HTML parser, so you are back to regex on markup, which is miserable.

Layer 3: Apps Script + a Fetch API

This is where Sheets becomes genuinely powerful. Instead of fetching raw HTML yourself, you call an API that handles rendering, proxies, and parsing, and hands back clean content you can drop into cells.

Store your key in Script Properties (Project Settings > Script Properties) so it never sits in the code, then:

function fetchClean(url) {
  const apiKey = PropertiesService.getScriptProperties().getProperty("LINKSC_API_KEY");
  const response = UrlFetchApp.fetch("https://api.link.sc/v1/fetch", {
    method: "post",
    contentType: "application/json",
    headers: { "x-api-key": apiKey },
    payload: JSON.stringify({ url: url, format: "markdown" }),
    muteHttpExceptions: true,
  });
  const data = JSON.parse(response.getContentText());
  return data.content;
}

// Use it as a custom function directly in a cell:
//   =FETCHCLEAN("https://example.com/product")
function FETCHCLEAN(url) {
  return fetchClean(url);
}

Now =FETCHCLEAN("https://example.com") works in a cell like a built-in formula, but it returns clean markdown even for JavaScript-heavy pages, because link.sc does the rendering and parsing server-side. The key lives in x-api-key and stays in Script Properties, never in the sheet.

For search-driven collection, call the search endpoint the same way:

function searchWeb(query) {
  const apiKey = PropertiesService.getScriptProperties().getProperty("LINKSC_API_KEY");
  const response = UrlFetchApp.fetch("https://api.link.sc/v1/search", {
    method: "post",
    contentType: "application/json",
    headers: { "x-api-key": apiKey },
    payload: JSON.stringify({ q: query, engine: "google" }),
    muteHttpExceptions: true,
  });
  return JSON.parse(response.getContentText());
}

Note the search field is q, not query.

Scheduling refreshes

Custom functions only recalculate when their inputs change, which is fine for on-demand lookups. For a monitor that updates on its own, write a function that loops over a column of URLs and set a time-driven trigger.

function refreshAll() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const urls = sheet.getRange("A2:A50").getValues();
  urls.forEach((row, i) => {
    if (!row[0]) return;
    const content = fetchClean(row[0]);
    // pull out a value, e.g. first price-looking token, or store a snippet
    sheet.getRange(i + 2, 2).setValue(content.slice(0, 500));
  });
}

In the Apps Script editor, open Triggers, add a trigger for refreshAll, and pick a time interval (hourly, daily). Your sheet now updates itself.

When to Graduate to Real Code

Sheets is the right tool for dozens or a few hundred rows, one-off research, and dashboards a non-engineer will maintain. You should move to a standalone script or service when:

  • You are scraping thousands of pages and the sheet gets slow or times out.
  • You need reliable scheduling beyond what triggers offer.
  • You want to store results in a database, not cells.
  • Multiple people or systems consume the data and a spreadsheet becomes a bottleneck.

The nice part is that the API call barely changes. The x-api-key header and the /v1/fetch body you used in Apps Script are the same in Python, Node, or anything else, so graduating is mostly moving the same request into a real program. If you would rather stay no-code a while longer, our no-code web scraping guide covers other tools in the same spirit, and when you do jump to code, how to export scraped data to CSV, JSON, or a database picks up where the spreadsheet leaves off.

A Quick Comparison

Approach Setup Handles JS pages Scheduling Best for
IMPORTXML / IMPORTHTML None No Sheets' own refresh Simple static tables and values
Apps Script + UrlFetchApp Low No Triggers Logic on simple pages
Apps Script + fetch API Low Yes Triggers Real scraping inside Sheets
Standalone code Higher Yes Full control Scale, storage, pipelines

Start at the top and only move down when a layer stops carrying its weight. Most people who think they need a scraping framework actually need one Apps Script function and a trigger.


Want clean content in your spreadsheet without fighting raw HTML? Get a free link.sc key, 500 credits a month, and paste the Apps Script above to start pulling data into Sheets today.