# Trade Fair Exhibitor Extractor (`harunaakira/webscrape`) Actor

Extracts exhibitor companies from trade fair directories — name, country, website and a corporate email — and refuses to deliver rows it cannot verify. Rejects archived and cancelled events before crawling, and never passes off the fair organiser’s address as an exhibitor’s.

- **URL**: https://apify.com/harunaakira/webscrape.md
- **Developed by:** [NguyenNhatHao](https://apify.com/harunaakira) (community)
- **Categories:** Developer tools, Lead generation, Open source
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Trade Fair Exhibitor Extractor

Extracts exhibitor company data from trade fair directories — company name, country, website and a corporate email address — and refuses to deliver rows it cannot verify.

### Why this exists

Two things go wrong when scraping fair directories, and neither of them raises an error.

**Archived events look identical to upcoming ones.** Fair websites keep past editions online for years. A search-driven crawl picks up the 2024 edition, extracts it perfectly cleanly, and hands you a thousand exhibitors from an event that finished eighteen months ago.

**The "contact by email" link usually belongs to the organiser.** On many directories that control points at the fair's own address, not the exhibitor's. Take it at face value and every row in your file carries the same valid, useless address — and it parses, validates and exports without complaint.

This Actor is built around rejecting both.

### What it does

- **Checks the event dates before crawling.** If the fair has ended, was cancelled, or falls outside the target month, the run stops immediately — zero requests spent.
- **Verifies every email against the company's own domain.** An address that doesn't match the exhibitor's website is withheld, whatever page it appeared on.
- **Decodes obfuscated addresses.** Directories often base64-encode the exhibitor's real address to defeat naive scrapers while leaving the organiser's in plain text. This Actor reads the encoded one.
- **Prefers role mailboxes** (`info@`, `sales@`) over named individuals, which keeps the output on the safer side of EU data-protection rules.
- **Reports its yield.** Every run returns how many rows were delivered, how many were withheld, and why.

### Input

```json
{
  "startUrls": [
    { "url": "https://www.example-fair.com/en/exhibitors/company-name-123" }
  ],
  "fairName": "GaLaBau 2026",
  "fairDateText": "15.09. - 18.09.2026",
  "targetMonth": "2026-09",
  "country": "Germany",
  "maxRequestsPerCrawl": 100
}
```

| Field | Required | Meaning |
|---|---|---|
| `startUrls` | yes | Exhibitor pages to process |
| `fairDateText` | no | Event dates. If given, the fair is checked before any crawling |
| `targetMonth` | no | `YYYY-MM`. Defaults to the current month |
| `fairName` | no | Label used in logs and the rejection record |
| `country` | no | Applied to every row when the directory doesn't publish it |
| `organiserDomains` | no | Hosts belonging to the fair. Defaults cover the major European venues |
| `roleMailboxesOnly` | no | Default `true`. Set `false` to accept named work addresses |
| `allowBrandMatch` | no | Default `true`. Treats `wuerth.com` and `wuerth.de` as the same company |

### Output

One dataset row per verified exhibitor:

```json
{
  "company": "4F Maschinentechnik GmbH",
  "country": "Germany",
  "website": "https://4-f.de",
  "email": "info@4-f.de",
  "domain": "4-f.de",
  "sourceUrl": "https://www.example-fair.com/en/exhibitors/4f-maschinentechnik-gmbh"
}
```

`domain` is the deduplication key. Company names drift between editions — `ACO GmbH`, `ACO Gmbh`, `ACO Group SE` — while the registrable domain stays put.

The run also stores a `RUN_SUMMARY`:

```json
{ "delivered": 27, "withheld": 23, "total": 50, "yieldPct": 54,
  "reasons": { "personal_mailbox": 12, "third_party_email": 11 } }
```

### What gets withheld, and why

Rows are never silently dropped. Each one is logged with a reason:

| Reason | Meaning |
|---|---|
| `organiser_email` | The only address on the listing belonged to the fair |
| `third_party_email` | The address didn't match the company's own domain |
| `personal_mailbox` | Only a named individual's inbox was published |
| `no_website` | The directory didn't publish a company website |
| `no_corporate_email` | No address found at all |

A visible gap is more useful than a plausible address that goes nowhere.

### Expected yield

Measured on 72 real exhibitor pages from a live directory:

| Rule set | Delivered |
|---|---|
| Role mailboxes only, exact domain match | **54–64%** |
| Plus brand matching across TLDs | **60–68%** |
| Plus named individuals at the company domain | **77–84%** |

Most of the remainder are companies that publish a parent company's or a distributor's address. Those aren't recoverable by rule — expect a practical ceiling around 80%.

### Limitations

- **Directory-specific selectors.** The default handler looks for the first external link and any `mailto:` on the page. Layouts vary; some directories need adjusting.
- **No JavaScript rendering.** This uses Cheerio, so directories that render exhibitor lists client-side need a browser-based crawler instead.
- **Cancellation detection is keyword-based.** It catches explicit wording; a quietly cancelled event will still pass the date check.
- **It doesn't make outreach lawful.** Verify your basis for contacting the companies you collect.

### Tests

```bash
npm test
```

41 tests, no network required. Most are built from data captured from a live directory, including the exhibitor page whose only visible email belongs to the organiser.

# Actor input Schema

## `startUrls` (type: `array`):

Exhibitor detail pages from a fair directory. Each page should show one company.

## `fairName` (type: `string`):

Label used in the log and in the rejection record. Optional.

## `fairDateText` (type: `string`):

Event dates as published by the organiser, e.g. "15.09. - 18.09.2026". If set, the fair is checked before any page is crawled — a past, cancelled or out-of-window event costs zero requests. Leave empty to skip the check.

## `targetMonth` (type: `string`):

YYYY-MM. Fairs outside this month are rejected. Defaults to the current month.

## `country` (type: `string`):

Applied to every row when the directory does not publish a country per exhibitor.

## `roleMailboxesOnly` (type: `boolean`):

Accept only company addresses such as info@ or sales@, not named individuals. Safer under EU data-protection rules, but lowers yield by roughly 20 percentage points.

## `allowBrandMatch` (type: `boolean`):

Treat wuerth.com and wuerth.de as the same company. Recovers large firms that publish one domain and send mail from another.

## `organiserDomains` (type: `array`):

Hosts belonging to the fair itself. Any address from these is refused — exhibitor pages routinely expose the organiser's address instead of the exhibitor's. Defaults cover the major European venues.

## `maxRequestsPerCrawl` (type: `integer`):

Upper bound on pages fetched in one run.

## `useProxy` (type: `boolean`):

Route requests through Apify Proxy. Off by default — most fair directories do not require it.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.galabau-messe.com/en/exhibitors/4f-maschinentechnik-gmbh-2613555"
    }
  ],
  "fairName": "GaLaBau 2026",
  "fairDateText": "15.09. - 18.09.2026",
  "targetMonth": "2026-09",
  "roleMailboxesOnly": true,
  "allowBrandMatch": true,
  "organiserDomains": [
    "nuernbergmesse.de",
    "galabau-messe.com",
    "messefrankfurt.com",
    "koelnmesse.de"
  ],
  "maxRequestsPerCrawl": 100,
  "useProxy": false
}
```

# Actor output Schema

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

No description

# 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 = {
    "startUrls": [
        {
            "url": "https://www.galabau-messe.com/en/exhibitors/4f-maschinentechnik-gmbh-2613555"
        }
    ],
    "fairName": "GaLaBau 2026",
    "fairDateText": "15.09. - 18.09.2026",
    "targetMonth": "2026-09",
    "organiserDomains": [
        "nuernbergmesse.de",
        "galabau-messe.com",
        "messefrankfurt.com",
        "koelnmesse.de"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("harunaakira/webscrape").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 = {
    "startUrls": [{ "url": "https://www.galabau-messe.com/en/exhibitors/4f-maschinentechnik-gmbh-2613555" }],
    "fairName": "GaLaBau 2026",
    "fairDateText": "15.09. - 18.09.2026",
    "targetMonth": "2026-09",
    "organiserDomains": [
        "nuernbergmesse.de",
        "galabau-messe.com",
        "messefrankfurt.com",
        "koelnmesse.de",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("harunaakira/webscrape").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 '{
  "startUrls": [
    {
      "url": "https://www.galabau-messe.com/en/exhibitors/4f-maschinentechnik-gmbh-2613555"
    }
  ],
  "fairName": "GaLaBau 2026",
  "fairDateText": "15.09. - 18.09.2026",
  "targetMonth": "2026-09",
  "organiserDomains": [
    "nuernbergmesse.de",
    "galabau-messe.com",
    "messefrankfurt.com",
    "koelnmesse.de"
  ]
}' |
apify call harunaakira/webscrape --silent --output-dataset

```

## MCP server setup

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

```

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/351BuJ4zSUCgn4bxJ/builds/EO8JdhxbWKX1Jxlla/openapi.json
