Xplate avatar
Xplate

Pricing

Pay per usage

Go to Store
Xplate

Xplate

Developed by

DevFlexi

Maintained by Community

0.0 (0)

Pricing

Pay per usage

0

Monthly users

1

Runs succeeded

>99%

Last modified

a month ago

.actor/Dockerfile

1# Specify the base Docker image. You can read more about
2# the available images at https://docs.apify.com/sdk/js/docs/guides/docker-images
3# You can also use any other image from Docker Hub.
4FROM apify/actor-node:20 AS builder
5
6# Check preinstalled packages
7RUN npm ls crawlee apify puppeteer playwright
8
9# Copy just package.json and package-lock.json
10# to speed up the build using Docker layer cache.
11COPY package*.json ./
12
13# Install all dependencies. Don't audit to speed up the installation.
14RUN npm install --include=dev --audit=false
15
16# Next, copy the source files using the user set
17# in the base image.
18COPY . ./
19
20# Install all dependencies and build the project.
21# Don't audit to speed up the installation.
22RUN npm run build
23
24# Create final image
25FROM apify/actor-node:20
26
27# Check preinstalled packages
28RUN npm ls crawlee apify puppeteer playwright
29
30# Copy just package.json and package-lock.json
31# to speed up the build using Docker layer cache.
32COPY package*.json ./
33
34# Install NPM packages, skip optional and development dependencies to
35# keep the image small. Avoid logging too much and print the dependency
36# tree for debugging
37RUN npm --quiet set progress=false \
38    && npm install --omit=dev --omit=optional \
39    && echo "Installed NPM packages:" \
40    && (npm list --omit=dev --all || true) \
41    && echo "Node.js version:" \
42    && node --version \
43    && echo "NPM version:" \
44    && npm --version \
45    && rm -r ~/.npm
46
47# Copy built JS files from builder image
48COPY --from=builder /usr/src/app/dist ./dist
49
50# Next, copy the remaining files and directories with the source code.
51# Since we do this after NPM install, quick build will be really fast
52# for most source file changes.
53COPY . ./
54
55# Create and run as a non-root user.
56RUN adduser -h /home/apify -D apify && \
57    chown -R apify:apify ./
58USER apify
59
60# Run the image.
61CMD npm run start:prod --silent

.actor/actor.json

1{
2    "actorSpecification": 1,
3    "name": "xplate-license-plate-scraper",
4    "title": "Xplate License Plate Scraper",
5    "description": "An Apify Actor that scrapes license plate data from Xplate using a provided page number input. It extracts images, prices, durations, and details (emirate, character, number) from the specified page.",
6    "version": "0.0",
7    "meta": {
8        "templateId": "ts-start"
9    },
10    "input": "./input_schema.json",
11    "dockerfile": "./Dockerfile"
12}

.actor/input_schema.json

1{
2  "title": "Scrape data from Xplate",
3  "type": "object",
4  "schemaVersion": 1,
5  "properties": {
6    "pageNumber": {
7      "title": "Page Number",
8      "type": "integer",
9      "description": "The page number to scrape from Xplate.",
10      "editor": "number",
11      "default": 1
12    }
13  },
14  "required": ["pageNumber"]
15}

src/main.ts

1import axios from 'axios';
2import * as cheerio from 'cheerio';
3import { Actor } from 'apify';
4
5await Actor.init();
6
7interface Input {
8    pageNumber: number;
9}
10
11// Get input (expecting a JSON object like: { "pageNumber": 3 })
12const input = await Actor.getInput<Input>();
13if (!input) throw new Error("Input is missing! Please provide a JSON object with a 'pageNumber' property.");
14const pageNumber: number = input.pageNumber || 1;
15console.log(`Scraping page number: ${pageNumber}`);
16
17// Construct the URL using the provided page number.
18const targetUrl = `https://xplate.com/en/numbers/license-plates?page=${pageNumber}`;
19
20// Define your selectors configuration.
21const XPLATES_SELECTORS = {
22    SOURCE_NAME: 'xplate',
23    ERROR_MESSAGE_SELECTOR: 'div.alert.alert-warning.text-center.m-0.rounded-3',
24    ALL_PLATES: 'div[class="number-card"]',
25    PLATE_PRICE: 'span.custom-red.dm-white',
26    PLATE_DURATION: 'div.d-flex.align-items-center.meta > div > span',
27    PLATE_LINK: 'a[class="p-0 m-0 lower-part default-dark-btn px-1 text-center bordered dm-bordered"]',
28    URL: targetUrl,
29    SKIP_CONFIGURATION: {
30        CALL_FOR_PRICE: 'Call For Price',
31        FEATURED: 'featured',
32        CHARACTER_HAS_NOC: 'noc',
33    },
34};
35
36interface Plate {
37    image: string;
38    price: string;
39    duration: string;
40    emirate: string;
41    character: string;
42    number: string;
43    source: string;
44}
45
46// Fetch the HTML content of the page using axios.
47const response = await axios.get(targetUrl, {
48    headers: {
49        // Include cookies if required by the site.
50        'Cookie': 'XSRF-TOKEN=...; xplate_session=...'
51    }
52});
53const html = response.data;
54const $ = cheerio.load(html);
55
56// Check for an error message on the page.
57if ($(XPLATES_SELECTORS.ERROR_MESSAGE_SELECTOR).length) {
58    console.error('Error message found on the page, aborting.');
59    await Actor.exit();
60}
61
62// Select all plate elements (skipping the first element if necessary).
63const plateElements = Array.from($(XPLATES_SELECTORS.ALL_PLATES)).slice(1);
64console.log(`Found ${plateElements.length} plate elements.`);
65
66const plates: Plate[] = [];
67
68// Extract information for each plate element.
69for (const plateEl of plateElements) {
70    const plateElement = $(plateEl);
71    const imgSrc = plateElement.find('img').attr('data-src') || '';
72    const price = plateElement.find(XPLATES_SELECTORS.PLATE_PRICE).text().trim() || '';
73    const duration = plateElement.find(XPLATES_SELECTORS.PLATE_DURATION).text().trim() || '';
74    const url = plateElement.find(XPLATES_SELECTORS.PLATE_LINK).attr('href') || '';
75
76    // Use regex to extract additional details from the URL.
77    const emirateMatch = url.match(/\/(\d+)-(.+?)-code-/);
78    const characterMatch = url.match(/-code-(.+?)-plate-number-/);
79    const numberMatch = url.match(/plate-number-(\d+)/);
80    const emirate = emirateMatch ? emirateMatch[2] : '';
81    const character = characterMatch ? characterMatch[1] : '';
82    const number = numberMatch ? numberMatch[1] : '';
83
84    const newPlate: Plate = {
85        image: imgSrc,
86        price,
87        duration,
88        emirate,
89        character,
90        number,
91        source: XPLATES_SELECTORS.SOURCE_NAME,
92    };
93
94    plates.push(newPlate);
95}
96
97// Log the extracted plates.
98console.log(JSON.stringify(plates, null, 2));
99
100// Save the results to the default dataset.
101await Actor.pushData(plates);
102
103await Actor.exit();

.dockerignore

1# configurations
2.idea
3.vscode
4
5# crawlee and apify storage folders
6apify_storage
7crawlee_storage
8storage
9
10# installed files
11node_modules
12
13# git folder
14.git
15
16# dist folder
17dist

.editorconfig

1root = true
2
3[*]
4indent_style = space
5indent_size = 4
6charset = utf-8
7trim_trailing_whitespace = true
8insert_final_newline = true
9end_of_line = lf

.eslintrc

1{
2    "root": true,
3    "env": {
4        "browser": true,
5        "es2020": true,
6        "node": true
7    },
8    "extends": [
9        "@apify/eslint-config-ts"
10    ],
11    "parserOptions": {
12        "project": "./tsconfig.json",
13        "ecmaVersion": 2020
14    },
15    "ignorePatterns": [
16        "node_modules",
17        "dist",
18        "**/*.d.ts"
19    ]
20}

.gitignore

1# This file tells Git which files shouldn't be added to source control
2
3.idea
4.vscode
5storage
6apify_storage
7crawlee_storage
8node_modules
9dist
10tsconfig.tsbuildinfo
11storage/*
12!storage/key_value_stores
13storage/key_value_stores/*
14!storage/key_value_stores/default
15storage/key_value_stores/default/*
16!storage/key_value_stores/default/INPUT.json

package.json

1{
2    "name": "ts-start",
3    "version": "0.0.1",
4    "type": "module",
5    "description": "This is an example of an Apify actor.",
6    "engines": {
7        "node": ">=18.0.0"
8    },
9    "dependencies": {
10        "apify": "^3.2.6",
11        "axios": "^1.5.0",
12        "cheerio": "^1.0.0-rc.12"
13    },
14    "devDependencies": {
15        "@apify/eslint-config-ts": "^0.3.0",
16        "@apify/tsconfig": "^0.1.0",
17        "@typescript-eslint/eslint-plugin": "^7.18.0",
18        "@typescript-eslint/parser": "^7.18.0",
19        "eslint": "^8.50.0",
20        "tsx": "^4.6.2",
21        "typescript": "^5.3.3"
22    },
23    "scripts": {
24        "start": "npm run start:dev",
25        "start:prod": "node dist/main.js",
26        "start:dev": "tsx src/main.ts",
27        "build": "tsc",
28        "test": "echo \"Error: oops, the actor has no tests yet, sad!\" && exit 1"
29    },
30    "author": "It's not you it's me",
31    "license": "ISC"
32}

tsconfig.json

1{
2    "extends": "@apify/tsconfig",
3    "compilerOptions": {
4        "module": "NodeNext",
5        "moduleResolution": "NodeNext",
6        "target": "ES2022",
7        "outDir": "dist",
8        "noUnusedLocals": false,
9        "skipLibCheck": true,
10        "lib": ["DOM"]
11    },
12    "include": [
13        "./src/**/*"
14    ]
15}

Pricing

Pricing model

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.