# Spain Tourism Statistics Scraper (Exceltur Indicator Data) (`steadfetch/exceltur-indicadores-extractor`) Actor

Extract Spain tourism statistics published by Exceltur as clean JSON: annual and quarterly tourism GDP (PIB Turistico), employment and IMPACTUR regional indicator series with source attribution. For tourism research, demand monitoring and economic dashboards. HTTP and XLSX only, no browser.

- **URL**: https://apify.com/steadfetch/exceltur-indicadores-extractor.md
- **Developed by:** [Steadfetch](https://apify.com/steadfetch) (community)
- **Categories:** Travel, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 indicator series delivereds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## steadfetch/exceltur-indicadores-extractor

Extract published [Exceltur](https://www.exceltur.org) tourism indicators from its public XLSX
workbooks with [Apify](https://apify.com) pay-per-event pricing and schema-validated output.

**Data source: Exceltur**

### What it does

Exceltur publishes its tourism studies as XLSX workbooks through the public WordPress media API.
The Actor lists those public files, downloads the selected workbooks and saves one validated
dataset item per indicator series:

- the published indicator name, unit, column label and source note;
- every numeric `(period, value)` observation found in the workbook;
- the source file (`mediaId`, URL, publication timestamp and title);
- publisher attribution (`source.publisher = "Exceltur"`, source URL and retrieval timestamp).

It reads both layouts used by Exceltur:

- **Time series** workbooks such as `PIB-Anual-3T26.xlsx` and `PIB-Trimestral-3T26.xlsx`
  (years, or year plus quarter, with one PIB Turistico column);
- **Matrix** workbooks such as `Datos-IMPACTUR-jul26.xlsx` (indicators as rows, autonomous
  communities and Spain as published columns).

Hidden rows are ignored. Non-numeric cells are never emitted and nothing is inferred: if the
workbook does not publish a value, no observation is created.

### Input

Empty input reads every XLSX workbook published in the last 90 days, up to 25 indicator series.

```json
{
  "mediaTypes": ["xlsx"],
  "search": "pib",
  "sinceDate": "2026-07-23",
  "maxItems": 25
}
```

- `mediaTypes`: only `["xlsx"]` is supported; any other value is rejected.
- `search`: optional WordPress media title filter (for example `"pib"`); empty means no filter.
- `sinceDate`: only workbooks published on or after this date (`YYYY-MM-DD`); empty means
  90 days before the run.
- `maxItems`: 1 to 100 indicator series saved to the dataset (default 25; values above 100 are
  rejected).

### Example output

Real output for `PIB-Anual-3T26.xlsx` (media id 51525) on 2026-10-21:

```json
{
  "indicator": "ISTE. PIB Tur\u00edstico en constantes. Indicador Sint\u00e9tico del Turismo en Espa\u00f1a",
  "unit": "Tasa de variaci\u00f3n interanual (%)",
  "columnLabel": "PIB Tur\u00edstico",
  "sourceNote": "Fuente: Exceltur",
  "observations": [
    { "period": "2023", "value": 3.2 },
    { "period": "2024", "value": 3.1 },
    { "period": "2025", "value": 2.1 },
    { "period": "2026 (P)", "value": 2.7 }
  ],
  "file": {
    "mediaId": 51525,
    "url": "https://www.exceltur.org/wp-content/uploads/2026/10/PIB-Anual-3T26.xlsx",
    "publishedAt": "2026-10-06T16:27:22",
    "title": "PIB-Anual-3T26"
  },
  "source": {
    "publisher": "Exceltur",
    "license": "No content-use clause located on the publisher site; documented source review in docs/legal-gate.md",
    "url": "https://www.exceltur.org/wp-content/uploads/2026/10/PIB-Anual-3T26.xlsx",
    "retrievedAt": "2026-10-21T06:30:00.000Z"
  },
  "url": "https://www.exceltur.org/wp-content/uploads/2026/10/PIB-Anual-3T26.xlsx",
  "scrapedAt": "2026-10-21T06:30:00.000Z"
}
```

### Pricing

| Event | When it fires | Price |
| ----- | ------------- | ----- |
| `apify-actor-start` | once per run (synthetic, automatic) | 0.00005 USD |
| `apify-default-dataset-item` | one validated indicator series saved to the dataset (synthetic, automatic) | 0.004 USD per series = **4.00 USD per 1,000 series** |

**One result = one indicator series (a dataset item)**, with all of its observations inside.
Observations are not counted or billed separately. Only series with at least one numeric
observation are pushed to the dataset. A workbook without parseable rows delivers nothing and
bills nothing. Charging is automatic through the synthetic dataset event: there is no custom
per-item or per-observation event, so an item is never billed twice. At 4.00 USD per 1,000 series
the reference series of the example (4 observations) works out at 1.00 USD per 1,000 observations;
the mean in the local `maxItems=25` run was 5.5 observations per series, which is 0.73 USD per
1,000 observations at this price.

### Cost per 1,000 results (measured locally)

Two local measurements on 2026-10-21, with **no Apify credit spent**:

| Measurement | Value |
| ----------- | ----- |
| Fixtures: 3 workbooks, 49,131 bytes | 4 indicator series, 35 numeric observations |
| Parse + extract + forward fill | **1.98 ms per three-workbook batch** (1,000 iterations) |
| Live `apify run` with `{"search":"pib","maxItems":1}` | **6.3 s wall**, 2 HTTP requests, 9,936-byte workbook |
| Delivered by the live run | 1 series with the 4 reference observations |
| Memory ceiling | 256 MB (Actor minimum); 96 MB process RSS in the parse benchmark |

The live run includes local `tsx` startup and the mandatory 500 ms between the two HTTP requests.
At 256 MB and Apify's 0.20 USD/GB-hour reference, 6.3 s is about 0.0000875 USD per run/series:
**0.09 USD per 1,000 series** and, with four observations per reference series, **0.022 USD per
1,000 observations**. The second figure is below the 0.05 USD estimate in the specification. The
production image runs `dist/main.js` without the local `tsx` startup, so this is a conservative
upper bound. At 4.00 USD per 1,000 series the measured compute cost (0.09 USD) leaves a gross
margin of about 97.7% before payment fees. The first paid platform run will replace this local
measurement with the billed figure.

### Legal and polite access

The public review is documented in `docs/legal-gate.md`:

- `robots.txt` disallows only `/wp-admin/`; `/wp-json/` and `/wp-content/uploads/` are allowed;
- the published sitemap and WordPress page search contain no content-use or anti-scraping clause;
- only aggregate tourism indicators are read: no personal data, no login, no paywall, no CAPTCHA;
- every item carries the Exceltur attribution and the exact source workbook;
- the Actor waits at least 500 ms between requests and aborts on 401/403/429/451 or a WAF page
  instead of retrying or evading the block.

### FAQ

**Does it need an API key, a login or a proxy?**
No. The WordPress media API and the XLSX uploads are public. The Actor uses plain HTTP, no browser
and no proxy.

**Why is one worksheet sometimes split into several items?**
Each numeric indicator column (time-series layout) or indicator row (matrix layout) becomes one
item. That keeps every series attributable to its own published label and avoids mixing units.

**Why do some items have `null` for `unit` or `sourceNote`?**
Those workbooks do not print that information. The Actor reports `null`; it never invents a unit
or an attribution line that Exceltur did not publish.

**What happens if a workbook has no numeric rows?**
The Actor logs a diagnostic and pushes no item for that workbook, so the run bills nothing for it.

**Does it include PDF reports or website HTML?**
No. Version 0.1 covers published XLSX workbooks only. PDF reports and HTML pages are outside the
declared scope.

**Where does the data come from?**
Exceltur, the Spanish tourism business association, through its public WordPress media library.
Every item cites the source workbook URL and the publisher.

### Maintenance

Tested daily: a scheduled run validates the public media listing and the dataset schema every day.
Failures become issues in the Steadfetch Actors repository. `npm test`, `npm run publish:check` and
`apify-cli validate-schema` run in CI on every push.

# Actor input Schema

## `mediaTypes` (type: `array`):

File types to read. Only XLSX is supported.

## `search` (type: `string`):

Optional WordPress media title filter, for example "pib" for the PIB annual and quarterly workbooks. Empty means no title filter.

## `sinceDate` (type: `string`):

Only workbooks published on or after this date (YYYY-MM-DD). Empty means 90 days before the run.

## `maxItems` (type: `integer`):

Maximum number of indicator series saved to the dataset (1-100).

## Actor input object example

```json
{
  "mediaTypes": [
    "xlsx"
  ],
  "search": "pib",
  "maxItems": 10
}
```

# Actor output Schema

## `results` (type: `string`):

Dataset items validated against the item schema in dataset_schema.json (fields).

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "search": "pib",
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadfetch/exceltur-indicadores-extractor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "search": "pib",
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("steadfetch/exceltur-indicadores-extractor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "search": "pib",
  "maxItems": 10
}' |
apify call steadfetch/exceltur-indicadores-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadfetch/exceltur-indicadores-extractor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/WKvy3yfz9tiawXKDN/builds/rycyaQkJZzyaSon6R/openapi.json
