---
title: "IMPORTXML Google Sheets: Scrape a Website | Scrape.ly"
description: "IMPORTXML Google Sheets guide: tested XPath formulas, IMPORTHTML, IMPORTDATA, refresh limits, Apps Script, and scheduled scrapes written to your sheet."
url: https://scrape.ly/blog/scrape-website-to-google-sheets
markdown: https://scrape.ly/blog/scrape-website-to-google-sheets.md
---

[Guides](https://scrape.ly/blog/category/guides.md)

# IMPORTXML + Google Sheets: How to Scrape a Website Into a Spreadsheet (and Beyond)

How to scrape a website into Google Sheets: IMPORTXML, IMPORTHTML, IMPORTDATA and IMPORTFEED with exact syntax and tested XPath on books.toscrape.com, Google's refresh rules, an Apps Script fetcher, and scheduled scrapes that write straight into your sheet.

[Scrape.ly Team The team behind Scrape.ly](https://scrape.ly/blog/author/scrapely-team.md)

- Published

  Oct 23, 2026

- Reading time

  11 min read

![Pixel-art robot with an orange >\_ on its terminal head carries a row of data from a web page on the left to a spreadsheet marked with the green Google Sheets logo on the right](https://scrape.ly/media/scrape-website-to-google-sheets.png?w=1080)

The quickest way to scrape a website into a spreadsheet is IMPORTXML: Google Sheets fetches the page and returns whatever your XPath query points at, one value per cell. Its siblings IMPORTHTML, IMPORTDATA and IMPORTFEED do the same for HTML tables and lists, CSV files and RSS feeds.

This guide gives the exact syntax of all four from Google's help pages (checked on 23 October 2026), formulas for the [books.toscrape.com](https://books.toscrape.com/) sandbox shop with XPath we tested, how often Google refreshes them, and an Apps Script for pages the formulas can't handle. Then the route that scales: a scraper that runs on a schedule and writes its results into your sheet.

## Four ways to import data from a website to Google Sheets

|                           | IMPORTXML             | IMPORTHTML             | IMPORTDATA        | IMPORTFEED          | Apps Script                           | Scrape.ly + Sheets integration       |
| ------------------------- | --------------------- | ---------------------- | ----------------- | ------------------- | ------------------------------------- | ------------------------------------ |
| Reads                     | Any element via XPath | One HTML table or list | A CSV or TSV file | An RSS or Atom feed | Anything you code                     | Any public site, through a scraper   |
| Setup                     | One formula           | One formula            | One formula       | One formula         | A script and a trigger                | Pick a scraper, share the sheet      |
| Pages built by JavaScript | No                    | No                     | n/a               | n/a                 | Only if the data has its own JSON URL | Yes (`useChrome` in the Web Scraper) |
| Many pages                | One URL per formula   | One URL per formula    | One file          | One feed            | Loops in code                         | Link following built in              |
| Refresh                   | Hourly, per Google    | Hourly                 | Hourly            | Hourly              | Time-driven trigger                   | Your schedule                        |
| Bot protection            | None                  | None                   | None              | None                | None                                  | Proxies and retries built in         |

## The four import functions: exact syntax

From Google's Docs Editors Help:

- **IMPORTXML** "imports data from any of various structured data types including XML, HTML, CSV, TSV, and RSS and ATOM XML feeds": `IMPORTXML(url, xpath_query, locale)`. The `locale` is optional; without it, the spreadsheet's locale is used ([IMPORTXML](https://support.google.com/docs/answer/3093342?hl=en)).
- **IMPORTHTML** "imports data from a table or list within an HTML page": `IMPORTHTML(url, query, index)`, where `query` is `"table"` or `"list"` and `index` starts at 1. Tables and lists are counted separately ([IMPORTHTML](https://support.google.com/docs/answer/3093339?hl=en)).
- **IMPORTDATA** imports a `.csv` or `.tsv` file: `IMPORTDATA(url)` ([IMPORTDATA](https://support.google.com/docs/answer/3093335?hl=en)).
- **IMPORTFEED** "imports a RSS or ATOM feed": `IMPORTFEED(url, [query], [headers], [num_items])`. `query` defaults to `"items"`; `"items title"`, `"items url"`, `"items created"` and `"items summary"` return one attribute ([IMPORTFEED](https://support.google.com/docs/answer/3093337?hl=en)).

In every function, the URL must include the protocol (`https://`) and be either in quotation marks or a reference to a cell that holds it.

## Scrape books.toscrape.com with IMPORTXML: Google Sheets formulas that work

books.toscrape.com is a fake bookshop built for scraping practice. Each book on its home page sits in an `<article class="product_pod">` element, which gives us clean XPath. We ran every query below against the live page with Python's lxml on 23 October 2026; each one returned 20 values, one per book on the page.

Put each formula in row 1 of its own column:

```plaintext
A1  =IMPORTXML("https://books.toscrape.com/", "//article[@class='product_pod']/h3/a/@title")
B1  =IMPORTXML("https://books.toscrape.com/", "//article[@class='product_pod']//p[@class='price_color']")
C1  =IMPORTXML("https://books.toscrape.com/", "//article[@class='product_pod']/p[contains(@class,'star-rating')]/@class")
D1  =IMPORTXML("https://books.toscrape.com/", "//article[@class='product_pod']/h3/a/@href")
```

What comes back:

| Column    | XPath returns                                                                                | First three values in our test                         |
| --------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| A, title  | The `title` attribute of each book link (the full title; the visible link text is cut short) | A Light in the Attic · Tipping the Velvet · Soumission |
| B, price  | The price paragraph                                                                          | £51.77 · £53.74 · £50.10                               |
| C, rating | The rating's class name                                                                      | star-rating Three · star-rating One · star-rating One  |
| D, link   | The relative link to the book page                                                           | catalogue/a-light-in-the-attic_1000/index.html · …     |

Two small fixes make the output usable. Strip the \`star-rating \` prefix, and turn the relative links into full URLs:

```plaintext
C1  =ARRAYFORMULA(SUBSTITUTE(IMPORTXML("https://books.toscrape.com/", "//article[@class='product_pod']/p[contains(@class,'star-rating')]/@class"), "star-rating ", ""))
D1  =ARRAYFORMULA("https://books.toscrape.com/" & IMPORTXML("https://books.toscrape.com/", "//article[@class='product_pod']/h3/a/@href"))
```

**Use a cell for the URL.** Put `https://books.toscrape.com/catalogue/page-2.html` in cell `F1` and write `=IMPORTXML(F1, "//article[@class='product_pod']/h3/a/@title")`. Change the page number and the column follows. The shop has 50 pages of 20 books; the total is on the page, and `=IMPORTXML("https://books.toscrape.com/", "//form[@class='form-horizontal']/strong[1]")` returns `1000`.

**Finding the XPath.** In Chrome, right-click the element, choose **Inspect**, then right-click the highlighted HTML and use **Copy → Copy XPath**. That gives a path to one element, such as `/html/body/.../article/h3/a`. To get every item, rewrite it around a class name the items share, as in the queries above, and test it in the Elements panel's search box (Ctrl+F or Cmd+F accepts XPath).

## IMPORTHTML, IMPORTDATA and IMPORTFEED examples

Each book page on books.toscrape.com has a "Product Information" table, the only table on the page, so its index is 1:

```plaintext
=IMPORTHTML("https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html", "table", 1)
```

In our lxml check, that table has seven rows with two columns: UPC `a897fe39b1053632`, Product Type `Books`, Price (excl. tax) `£51.77`, Price (incl. tax) `£51.77`, Tax `£0.00`, Availability `In stock (22 available)` and Number of reviews `0`.

IMPORTDATA takes any public CSV. Google's own example is a US Census file, which we checked still answers with `text/csv`:

```plaintext
=IMPORTDATA("https://www2.census.gov/programs-surveys/popest/datasets/2010-2019/national/totals/nst-est2019-popchg2010_2019.csv")
```

IMPORTFEED reads a news feed. This returns the five newest post titles from Google's blog feed, without a header row:

```plaintext
=IMPORTFEED("https://blog.google/rss/", "items title", FALSE, 5)
```

## How often the import functions refresh

Google documents this in its spreadsheet settings help: functions that pull data from outside the spreadsheet recalculate at fixed intervals, "ImportHtml, ImportFeed, ImportData, ImportXml: 1 hour" (and IMPORTRANGE every 30 minutes) ([calculation settings](https://support.google.com/docs/answer/58515?hl=en)). There is no setting for "every 5 minutes", and no way to choose the time of day.

Google also recommends keeping data inside the spreadsheet where you can: referencing data on the same sheet "is faster than Import functions" such as IMPORTDATA, IMPORTXML and IMPORTHTML, because each import is a round trip over the internet ([performance tips](https://support.google.com/docs/answer/12159115?hl=en)). A sheet with hundreds of import formulas gets slow.

## Where IMPORTXML stops working

The formulas above work because books.toscrape.com sends its data in the HTML. Many sites don't:

- **Pages built by JavaScript.** The quotes on [quotes.toscrape.com/js](https://quotes.toscrape.com/js/) are written into the page by a script after it loads. The HTML the server sends contains none of them: our lxml check found 10 quotes on the plain version of the site and 0 on the `/js/` version. Formulas that read the fetched page come back empty, as this [Stack Overflow answer](https://stackoverflow.com/questions/74237688/scraping-data-to-google-sheets-from-a-website-that-uses-javascript) explains.
- **Sites that block bots.** Every request comes from Google's servers, with no proxy rotation and no way to solve a challenge page. Sites with bot protection answer with an error or a challenge instead of data.
- **Many pages.** One formula reads one URL. A 50-page catalogue means 50 formulas per column, all refreshing on Google's schedule.
- **Detail pages.** The book's UPC and stock count live on its own page, not on the listing. Getting them for 1,000 books means 1,000 IMPORTHTML calls.
- **History.** A formula always shows the current value. To track prices over time, you need rows that are added, not overwritten.

## Apps Script: fetch JSON with UrlFetchApp

Sometimes a JavaScript page loads its data from a JSON URL you can call directly. Open your browser's developer tools, go to the **Network** tab, filter by **Fetch/XHR** and reload: quotes.toscrape.com's infinite-scroll version, for example, loads `https://quotes.toscrape.com/api/quotes?page=1`. Apps Script can read that and write rows. In your sheet, open **Extensions → Apps Script**, paste this, and run `importQuotes` (it asks for permission the first time):

```javascript
function importQuotes() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Quotes');
  const rows = [['text', 'author', 'tags']];
  let page = 1;
  let hasNext = true;
  while (hasNext && page <= 3) {
    const response = UrlFetchApp.fetch('https://quotes.toscrape.com/api/quotes?page=' + page);
    const data = JSON.parse(response.getContentText());
    for (const quote of data.quotes) {
      rows.push([quote.text, quote.author.name, quote.tags.join(', ')]);
    }
    hasNext = data.has_next;
    page++;
  }
  sheet.clearContents();
  sheet.getRange(1, 1, rows.length, rows[0].length).setValues(rows);
}
```

It needs a tab named `Quotes`. We ran the same logic outside Google with stand-ins for `UrlFetchApp` and `SpreadsheetApp` on 23 October 2026: three pages, 30 quotes, 31 rows including the header. To refresh it on a timetable, open **Triggers** (the clock icon) in the Apps Script editor and add a time-driven trigger, or create one in code with `ScriptApp.newTrigger('importQuotes').timeBased().everyHours(6).create()`.

Apps Script has quotas. For a gmail.com account, Google lists 20,000 URL Fetch calls a day (100,000 on Google Workspace), 6 minutes per script execution and 90 minutes a day of total trigger runtime ([Apps Script quotas](https://developers.google.com/apps-script/guides/services/quotas)). And you're now maintaining code: when the site changes its JSON, the script breaks, and it still can't get past bot protection.

## The scalable route: a scraper that writes into your sheet

When the formulas run out, let a scraper collect the data and Scrape.ly deliver it to the sheet. The [Scrape.ly Store](https://scrape.ly/store.md) has 568 ready-made scrapers for specific sites, from Google Maps to Amazon, and the [Web Scraper](https://scrape.ly/store/querystone/web-scraper.md) for everything else. It runs a short JavaScript function on each page and saves what it returns, fetching over HTTP by default or in headless Chromium with `useChrome` when the content only appears after scripts run.

Here's the detail-page job IMPORTXML can't do. On 23 October 2026 we pointed the Web Scraper at the Mystery category of books.toscrape.com, told it to follow each book link, and pulled four fields from every book page. The input:

```json
{
  "startUrls": [{ "url": "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html" }],
  "linkSelector": "article.product_pod h3 a",
  "globs": [{ "glob": "https://books.toscrape.com/catalogue/*/index.html" }],
  "pageFunction": "async function pageFunction(context) {\n    const $ = context.jQuery;\n    if (!$('div.product_main').length) return null; // listing page: only follow links\n    return {\n        url: context.request.url,\n        title: $('div.product_main h1').text().trim(),\n        price: $('div.product_main p.price_color').text().trim(),\n        inStock: $('div.product_main p.availability').text().includes('In stock'),\n        upc: $('table.table-striped td').first().text().trim(),\n    };\n}",
  "maxResultsPerCrawl": 5,
  "respectRobotsTxtFile": true,
  "proxyConfiguration": { "useScrapelyProxy": true }
}
```

The run took 12.4 seconds, fetched 5 pages through the Scrape.ly proxy and saved 4 book records, for a total charge of $0.0029:

| title                | price  | inStock | upc              |
| -------------------- | ------ | ------- | ---------------- |
| Sharp Objects        | £47.82 | true    | e00eb4fd7b871a48 |
| In a Dark, Dark Wood | £19.63 | true    | 19ed25f4641d5efd |
| The Past Never Ends  | £56.50 | true    | 5ee94540d0749ea0 |
| A Murder in Time     | £16.64 | true    | f733e8c19d40ec2e |

Raise `maxResultsPerCrawl` and the same input covers the whole category, or the whole shop.

### Send the results to Google Sheets

Scrape.ly writes the dataset items of each successful run into a Google Sheet. There's no Google sign-in: Scrape.ly writes as its own service account, and you share only the sheets you choose ([integrations docs](https://scrape.ly/docs/integrations.md)).

1. On the scraper's **Automations** tab, click **New automation** and pick **Send results to Google Sheets**.
2. Copy the email shown under **Share your sheet with**. In Google Sheets, click **Share**, add that email and give it **Editor** access.
3. Paste the sheet's link, and optionally the **Tab** to write to.
4. Choose **On each run**: **Add rows** puts new results below the existing ones, **Replace the tab** clears it and writes the latest results.
5. Save and send a test. A `Scrapely connected ✓` row appears; the first real run writes its header there.

The first row is a header. Nested fields become columns with dots, such as `price.amount`, and arrays are written as JSON text. Values are written as plain text and numbers, so text that starts with `=` is never run as a formula. **Max rows per run** (1 to 10,000, default 1,000) caps each delivery.

### Put it on a schedule

To refresh the sheet on your timetable instead of Google's hourly cycle, create an automation with **On a schedule** as its trigger (**Hourly**, **Daily**, **Weekly**, **Monthly** or a cron expression), the scraper as its run, and **Google Sheets** as a delivery ([Automations docs](https://scrape.ly/docs/automations.md)). Turn on the **Only new items since the last run** filter with `url` as the key field, and each run appends only books that weren't there last time. That's the price history a formula can't keep. Add a **Max cost per run** and no run can spend more than you set.

Limits to plan for: each Google Sheets automation writes at most 30 times an hour, and an account makes at most 500 exports a day across Google Sheets, S3 and GitHub. A cell holds at most 50,000 characters.

### Why not IMPORTDATA on the API?

Every run's results are also available as CSV from the API, at `https://api.scrape.ly/v2/actor-runs/RUN_ID/dataset/items?format=csv`. It's tempting to wrap that in IMPORTDATA. It won't work, and the workarounds are worse:

- The API reads your key only from the `Authorization` header. A `?token=` in the URL is ignored, so the request counts as unauthenticated ([API overview](https://scrape.ly/docs/api-overview.md)), and IMPORTDATA can't send headers.
- Even if a URL could carry a key, a formula is visible to everyone who can open the sheet, can be copied with it, and URLs end up in logs and browser history. An account key in a sheet is a key you've published.

So use the integration: Scrape.ly pushes the rows, nothing secret lives in the sheet, and you revoke access by removing the service account from the sheet's sharing settings. For a one-off hand-over, a signed download link that expires is the safe way to share a run's results ([results docs](https://scrape.ly/docs/results.md)). If you prefer files, the same results export as CSV or Excel; our guide to [scraping data from a website into Excel](https://scrape.ly/blog/scrape-data-from-website-to-excel.md) covers that route.

## How Scrape.ly does it

- **Ready-made scrapers.** 568 Actors for specific sites, plus the Web Scraper for any public site. No XPath to maintain in a cell.
- **Sheets as a destination, not a source.** Results are written into your sheet after every successful run, appended or replacing the tab, with no Google sign-in and no key in the sheet. See all destinations on the [integrations page](https://scrape.ly/integrations.md).
- **Your schedule, only new rows.** Schedules from hourly to monthly, a filter for new items, and an optional AI summary sent to Slack or email.
- **Pages formulas can't reach.** Link following, proxies and retries are built in, and the Web Scraper's `useChrome` option renders pages whose content only appears after scripts run.
- **Predictable cost.** The Web Scraper costs $0.50 per 1,000 results; our run cost $0.0029. The free plan includes $5 of usage every month with no credit card, and a maximum charge per run caps every job.

New to the topic? Start with [what web scraping is](https://scrape.ly/blog/what-is-web-scraping.md) and how it differs from using an API.

## FAQ

### How do I scrape a website to Google Sheets without code?

Use IMPORTXML with an XPath query, or IMPORTHTML for a table or list. For sites that load data with JavaScript, block bots or span many pages, run a ready-made scraper and send its results to your sheet with the Scrape.ly Google Sheets integration.

### Why does IMPORTXML return nothing or an error?

Usually the XPath matches nothing in the HTML the server sends. Either the query is wrong (test it in Chrome's Elements search), or the page builds its content with JavaScript after loading, as on quotes.toscrape.com/js. In the second case, look for a JSON URL in the Network tab or use a scraper.

### How often does IMPORTXML refresh?

Google's help says IMPORTXML, IMPORTHTML, IMPORTDATA and IMPORTFEED recalculate every hour. You can't set a shorter interval. For a fixed timetable, use an Apps Script time-driven trigger or a scheduled scraper.

### Can IMPORTXML scrape multiple pages?

One formula reads one URL. Put page URLs in cells and point a formula at each, which works for a few pages. For a whole catalogue or for detail pages, a scraper with link following is far less work.

### Can Google Sheets scrape a website that uses JavaScript?

Not with the import functions alone. If the page fetches its data from a JSON URL, Apps Script's `UrlFetchApp` can call it. Otherwise you need a scraper that renders the page, such as the Scrape.ly Web Scraper with `useChrome` turned on, and the Google Sheets integration to write the results to your sheet.

### Is it legal to scrape a website into Google Sheets?

Collecting publicly available data is generally lower risk, but the site's terms and privacy law still apply, and personal data needs a lawful basis under laws such as the GDPR. Scrape.ly Store scrapers only collect publicly available data any visitor can see. This is not legal advice.

Scrape.ly Store Scrape any site into Google Sheets Run one page function across a site, then send the results to your sheet after every run. $0.50 per 1,000 results.

[Try it free](https://scrape.ly/store/querystone/web-scraper.md) [Browse the Store](https://scrape.ly/store.md)

About the author

[Scrape.ly Team](https://scrape.ly/blog/author/scrapely-team.md) The team behind Scrape.ly

Scrape.ly is the web data platform from TeraShift GmbH in Switzerland, powered by Evomi: ready-made scrapers in the Scrape.ly Store, a cloud runtime, an API with JavaScript and Python SDKs, proxies, a hosted anti-bot browser and an MCP server for AI agents.

- [Website](https://scrape.ly/index.md)

Keep reading

## More in Guides

- [![Pixel-art robot with an orange >\_ on its terminal head carries a row of data from a web page on the left into a table on the right that is filling up row by row](https://scrape.ly/media/what-is-web-scraping.png?w=750)](https://scrape.ly/blog/what-is-web-scraping.md)
  ### [What Is Web Scraping? How It Works and What It Is Used For](https://scrape.ly/blog/what-is-web-scraping.md)
  [What is web scraping? The automated collection of data from websites into structured records. See how it works step by step with tested Python code, what teams use it for, how it differs from crawling, APIs and screen scraping, and how to do it without code.](https://scrape.ly/blog/what-is-web-scraping.md)
- [![Pixel-art robot with an orange >\_ on its terminal head pulls a data card out of a monitor showing the blue and yellow Python logo beside lines of code, and drops it into a crate](https://scrape.ly/media/web-scraping-python.png?w=750)](https://scrape.ly/blog/web-scraping-python.md)
  ### [Web Scraping With Python: A Complete 2026 Guide](https://scrape.ly/blog/web-scraping-python.md)
  [A complete guide to web scraping with Python: requests and BeautifulSoup, CSS selectors, pagination, CSV and JSON export, polite delays and retries, async httpx, Playwright for JavaScript pages, Scrapy, and how to run it all in the cloud. Every script tested on 23 October 2026.](https://scrape.ly/blog/web-scraping-python.md)
- [![Pixel-art robot with an orange >\_ on its terminal head stands before a pair of balanced golden scales of justice, holding out a data card](https://scrape.ly/media/is-web-scraping-legal.png?w=750)](https://scrape.ly/blog/is-web-scraping-legal.md)
  ### [Is Web Scraping Legal? US, EU and UK Law, Court Cases and a Checklist (2026)](https://scrape.ly/blog/is-web-scraping-legal.md)
  [Is web scraping legal? What US, EU and UK law actually says in 2026, from Van Buren and hiQ v. LinkedIn to Meta and X v. Bright Data, GDPR, database rights, robots.txt and the newest SerpApi rulings, plus a practical checklist.](https://scrape.ly/blog/is-web-scraping-legal.md)

## The internet is full of data. Start owning yours.

Scrape.ly gives you the infrastructure, the tooling, and the team to extract, process, and operationalize web data at any scale. Setup takes minutes. The advantage lasts forever.

[Get Started Free](https://my.scrape.ly/register) [Chat with Support](mailto:support@scrape.ly?subject=Question%20about%20Scrape.ly\&body=Hi%2C%20I%20have%20a%20question%20about%20Scrape.ly.)

No credit card. No setup calls.
