Expired Domains Scraper avatar

Expired Domains Scraper

Try for free

No credit card required

Go to Store
Expired Domains Scraper

Expired Domains Scraper

martin1080p/expired-domains-scraper
Try for free

No credit card required

The Expired Domains Scraper automates finding valuable expired domains from expireddomains.com, offering filters and sorting by SEO metrics and auction details for efficient domain acquisition.

.actor/Dockerfile

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

.actor/actor.json

1{
2	"actorSpecification": 1,
3	"name": "expired-domains-scraper",
4	"title": "Expired Domains Scraper",
5	"description": "Expired Domains Scraper is a robust tool designed to automate the process of identifying and extracting information about expired domain names. This tool is ideal for digital marketers, domain investors, SEO specialists, and businesses looking to acquire valuable domains that have lapsed in registration. By leveraging this scraper, users can gain a competitive edge in acquiring high-quality domains that may still have significant SEO value, backlinks, or brand potential.",
6	"version": "0.0",
7	"meta": {
8		"templateId": "ts-crawlee-cheerio"
9	},
10	"input": "./input_schema.json",
11	"dockerfile": "./Dockerfile",
12	"storages": {
13        "dataset": {
14            "actorSpecification": 1,
15            "fields": {},
16            "views": {
17                "overview": {
18                    "title": "Overview",
19                    "transformation": {
20						"fields": [
21							"name",
22							"link",
23							"enddate",
24							"tld",
25							"godaddyPrice",
26							"godaddyBid",
27							"auctionType",
28							"rdapRegistration",
29							"waybackFirstYear",
30							"waybackCount",
31							"searchVolumeVolume",
32							"searchVolumeCpc",
33							"googleStatusIsBlocked",
34							"googleStatusIsFaillisted",
35							"googleStatusIsAdult",
36							"mozDomainAuthority",
37							"mozPageAuthority",
38							"majesticCitationflow",
39							"majesticTrustflow",
40							"majesticExtbacklinks",
41							"ownranksBrandrank",
42							"ownranksTldsaturation",
43							"ownranksRadiotest"
44						]
45					},
46                    "display": {
47						"component": "table",
48						"properties": {
49							"name": {
50								"label": "Name",
51								"format": "text"
52							},
53							"link": {
54								"label": "Link",
55								"format": "link"
56							},
57							"enddate": {
58								"label": "End date",
59								"format": "date"
60							},
61							"tld": {
62								"label": "TLD",
63								"format": "text"
64							},
65							"godaddyPrice": {
66								"label": "GoDaddy price",
67								"format": "number"
68							},
69							"godaddyBid": {
70								"label": "N# of bids",
71								"format": "number"
72							},
73							"auctionType": {
74								"label": "Auction type",
75								"format": "text"
76							},
77							"rdapRegistration": {
78								"label": "Whois first registration",
79								"format": "number"
80							},
81							"waybackFirstYear": {
82								"label": "Wayback first year",
83								"format": "number"
84							},
85							"waybackCount": {
86								"label": "Wayback count",
87								"format": "number"
88							},
89							"searchVolumeVolume": {
90								"label": "Search volume",
91								"format": "number"
92							},
93							"searchVolumeCpc": {
94								"label": "Search volume CPC",
95								"format": "number"
96							},
97							"googleStatusIsBlocked": {
98								"label": "Google status is blocked",
99								"format": "boolean"
100							},
101							"googleStatusIsFaillisted": {
102								"label": "Google status is faillisted",
103								"format": "boolean"
104							},
105							"googleStatusIsAdult": {
106								"label": "Google status is adult",
107								"format": "boolean"
108							},
109							"mozDomainAuthority": {
110								"label": "Moz domain authority",
111								"format": "number"
112							},
113							"mozPageAuthority": {
114								"label": "Moz page authority",
115								"format": "number"
116							},
117							"majesticCitationflow": {
118								"label": "Majestic citation flow",
119								"format": "number"
120							},
121							"majesticTrustflow": {
122								"label": "Majestic trust flow",
123								"format": "number"
124							},
125							"majesticExtbacklinks": {
126								"label": "Majestic external backlinks",
127								"format": "number"
128							},
129							"ownranksBrandrank": {
130								"label": "Ownranks brand rank",
131								"format": "number"
132							},
133							"ownranksTldsaturation": {
134								"label": "Ownranks TLD saturation",
135								"format": "number"
136							},
137							"ownranksRadiotest": {
138								"label": "Ownranks radio test",
139								"format": "number"
140							}
141						}
142					}
143                }
144            }
145        }
146    }
147}

.actor/input_schema.json

1{
2    "title": "Expired Domains Scraper",
3    "type": "object",
4    "schemaVersion": 1,
5    "required": ["numDomains", "sortBy", "sortDir", "filterLengthMin", "filterLengthMax", "filterExpDate"],
6    "properties": {
7        "numDomains": {
8            "title": "Number of domains",
9            "description": "Total number of domains to be fetched.",
10            "type": "integer",
11            "editor": "number",
12            "default": 50,
13            "minimum": 25
14        },
15        "sortBy": {
16            "sectionCaption": "Sort settings",
17            "sectionDescription": "Domains to be fetched in specific order.",
18            "title": "Sort by",
19            "description": "Select one of the options.",
20            "type": "string",
21            "enum": ["name", "link", "enddate", "tld", "godaddy_price", "auction_type", "godaddy_bid", "ai_price_pred_price", "rdap_registration", "wayback_first_year", "wayback_count", "search_volume_volume", "search_volume_cpc", "google_status_is_blocked", "google_status_is_faillisted", "google_status_is_adult", "fb_shares", "fb_react", "majestic_extbacklinks", "majestic_citationflow", "majestic_trustflow", "moz_domain_authority", "moz_page_authority", "ownranks_brandrank", "ownranks_tldsaturation", "ownranks_radiotest"],
22            "enumTitles": ["Name", "Link", "Expiration date", "TLD", "Current price", "Auction type", "Godaddy bid count", "Est. auction price", "WHOIS birth year", "Internet archive birth year", "Internet archive count", "Search volume", "Cost per click", "Google blocked", "Google Faillisted", "Google adult", "Facebook shares", "Facebook react", "External backlinks", "Citation flow", "Trust flow", "MOZ domain authority", "MOZ page authority", "BrandRank score", "TLD Saturation", "RadioTest"],
23            "default": "name"
24        },
25        "sortDir": {
26            "title": "Sort direction",
27            "description": "Select one of the options.",
28            "type": "string",
29            "enum": ["asc", "desc"],
30            "enumTitles": ["ASC", "DESC"],
31            "default": "asc"
32        },
33        "filterTldCom": {
34            "sectionCaption": "Filter settings",
35            "sectionDescription": "Filter fetched domains by specific properties.",
36            "groupCaption": "Filter by TLD",
37            "groupDescription": "Specific TLDs to be fetched.",
38            "title": "com",
39            "description": "Domains ending with .com.",
40            "type": "boolean",
41            "default": true
42        },
43        "filterTldNet": {
44            "title": "net",
45            "description": "Domains ending with .net.",
46            "type": "boolean",
47            "default": true
48        },
49        "filterTldOrg": {
50            "title": "org",
51            "description": "Domains ending with .org.",
52            "type": "boolean",
53            "default": true
54        },
55        "filterTldIo": {
56            "title": "org",
57            "description": "Domains ending with .io.",
58            "type": "boolean",
59            "default": true
60        },
61        "filterTldUk": {
62            "title": "uk",
63            "description": "Domains ending with .uk.",
64            "type": "boolean",
65            "default": true
66        },
67        "filterTldN": {
68            "title": "nTLD",
69            "description": "Domains ending with .agency, .bio, etc.",
70            "type": "boolean",
71            "default": true
72        },
73        "filterTldOther": {
74            "title": "other TLDs",
75            "description": "Domains ending with all other extensions.",
76            "type": "boolean",
77            "default": true
78        },
79        "filterLengthMin": {
80            "title": "Length min",
81            "description": "Domains with minimum length of specified number.",
82            "type": "integer",
83            "default": 1,
84            "minimum": 1,
85            "maximum": 255,
86            "nullable": true
87        },
88        "filterLengthMax": {
89            "title": "Length max",
90            "description": "Domains with maximum length of specified number.",
91            "type": "integer",
92            "default": 255,
93            "minimum": 1,
94            "maximum": 255,
95            "nullable": true
96        },
97        "filterCharsHyp": {
98            "groupCaption": "Filter by used characters",
99            "groupDescription": "Domains excluding certain characters.",
100            "title": "No hyphens",
101            "description": "Domains excluding hyphens in their name.",
102            "type": "boolean",
103            "default": false
104        },
105        "filterCharsNum": {
106            "title": "No numbers",
107            "description": "Domains excluding numbers in their name.",
108            "type": "boolean",
109            "default": false
110        },
111        "filterCharsLet": {
112            "title": "No letters",
113            "description": "Domains excluding letters in their name.",
114            "type": "boolean",
115            "default": false
116        },
117        "filterGglNA": {
118            "groupCaption": "Filter by Google status",
119            "groupDescription": "Domains listed in certain google lists.",
120            "title": "Not adult",
121            "description": "Domains not marked as adult content.",
122            "type": "boolean",
123            "default": true
124        },
125        "filterGglNB": {
126            "title": "Not blocked",
127            "description": "Domains not marked as blocked.",
128            "type": "boolean",
129            "default": true
130        },
131        "filterGglA": {
132            "title": "Adult",
133            "description": "Domains marked as adult content.",
134            "type": "boolean",
135            "default": false
136        },
137        "filterGglB": {
138            "title": "Blocked",
139            "description": "Domains marked as blocked.",
140            "type": "boolean",
141            "default": false
142        },
143        "filterAucTypAuc": {
144            "groupCaption": "Filter by auction type",
145            "groupDescription": "Domains in certain auction processes.",
146            "title": "Godaddy auctions",
147            "description": "Domains with listed Godaddy auction.",
148            "type": "boolean",
149            "default": true
150        },
151        "filterAucTypClo": {
152            "title": "Godaddy closeouts",
153            "description": "Domains in the middle of closeout process.",
154            "type": "boolean",
155            "default": true
156        },
157        "filterAucTypDel": {
158            "title": "Pending delete",
159            "description": "Domains awaiting pending deletion.",
160            "type": "boolean",
161            "default": true
162        },
163        "filterExpDate": {
164            "title": "Expiration date",
165            "description": "Domains with selected date of expiration. Allowed date range is from today to 4 days after today. For example, if today is 2024-06-24, the allowed range is 2024-06-24 to 2024-06-28.",
166            "type": "string",
167            "editor": "datepicker",
168            "default": "2024-01-01",
169            "prefill": "2024-01-01"
170        }
171    }
172}

.vscode/launch.json

1{
2    "version": "0.2.0",
3    "configurations": [
4        {
5            "type": "node",
6            "request": "launch",
7            "name": "Launch via apify",
8            "runtimeExecutable": "apify",
9            "runtimeArgs": [
10              "run",
11              "--purge"
12            ],
13            "skipFiles": [
14              "<node_internals>/**"
15            ],
16          }
17    ]
18}

src/builders/domainBuilder.ts

1import { DomainParser } from '../parsers/domainParser.js';
2import { Domain } from '../types/domain.js';
3
4export class DomainBuilder {
5    domainParser: DomainParser;
6    domain: Domain = {} as Domain;
7
8    constructor(domainParser: DomainParser) {
9        this.domainParser = domainParser;
10    }
11
12    addName(): DomainBuilder {
13        this.domain.name = this.domainParser.name;
14        return this;
15    }
16
17    addLink(): DomainBuilder {
18        this.domain.link = this.domainParser.link;
19        return this;
20    }
21
22    addEnddate(): DomainBuilder {
23        this.domain.enddate = this.domainParser.enddate;
24        return this;
25    }
26
27    addTld(): DomainBuilder {
28        this.domain.tld = this.domainParser.tld;
29        return this;
30    }
31
32    addGodaddyPrice(): DomainBuilder {
33        this.domain.godaddyPrice = this.domainParser.godaddyPrice;
34        return this;
35    }
36
37    addGodaddyBid(): DomainBuilder {
38        this.domain.godaddyBid = this.domainParser.godaddyBid;
39        return this;
40    }
41
42    addAuctionType(): DomainBuilder {
43        this.domain.auctionType = this.domainParser.auctionType;
44        return this;
45    }
46
47    addAiPricePredPrice(): DomainBuilder {
48        this.domain.aiPricePredPrice = this.domainParser.aiPricePredPrice;
49        return this;
50    }
51
52    addRdapRegistration(): DomainBuilder {
53        this.domain.rdapRegistration = this.domainParser.rdapRegistration;
54        return this;
55    }
56
57    addWaybackFirstYear(): DomainBuilder {
58        this.domain.waybackFirstYear = this.domainParser.waybackFirstYear;
59        return this;
60    }
61
62    addWaybackCount(): DomainBuilder {
63        this.domain.waybackCount = this.domainParser.waybackCount;
64        return this;
65    }
66
67    addSearchVolumeVolume(): DomainBuilder {
68        this.domain.searchVolumeVolume = this.domainParser.searchVolumeVolume;
69        return this;
70    }
71
72    addSearchVolumeCpc(): DomainBuilder {
73        this.domain.searchVolumeCpc = this.domainParser.searchVolumeCpc;
74        return this;
75    }
76
77    addGoogleStatusIsBlocked(): DomainBuilder {
78        this.domain.googleStatusIsBlocked = this.domainParser.googleStatusIsBlocked;
79        return this;
80    }
81
82    addGoogleStatusIsFaillisted(): DomainBuilder {
83        this.domain.googleStatusIsFaillisted = this.domainParser.googleStatusIsFaillisted;
84        return this;
85    }
86
87    addGoogleStatusIsAdult(): DomainBuilder {
88        this.domain.googleStatusIsAdult = this.domainParser.googleStatusIsAdult;
89        return this;
90    }
91
92    addFbShares(): DomainBuilder {
93        this.domain.fbShares = this.domainParser.fbShares;
94        return this;
95    }
96
97    addFbReact(): DomainBuilder {
98        this.domain.fbReact = this.domainParser.fbReact;
99        return this;
100    }
101
102    addMajesticExtbacklinks(): DomainBuilder {
103        this.domain.majesticExtbacklinks = this.domainParser.majesticExtbacklinks;
104        return this;
105    }
106
107    addMajesticCitationflow(): DomainBuilder {
108        this.domain.majesticCitationflow = this.domainParser.majesticCitationflow;
109        return this;
110    }
111
112    addMajesticTrustflow(): DomainBuilder {
113        this.domain.majesticTrustflow = this.domainParser.majesticTrustflow;
114        return this;
115    }
116
117    addMozDomainAuthority(): DomainBuilder {
118        this.domain.mozDomainAuthority = this.domainParser.mozDomainAuthority;
119        return this;
120    }
121
122    addMozPageAuthority(): DomainBuilder {
123        this.domain.mozPageAuthority = this.domainParser.mozPageAuthority;
124        return this;
125    }
126
127    addOwnranksBrandrank(): DomainBuilder {
128        this.domain.ownranksBrandrank = this.domainParser.ownranksBrandrank;
129        return this;
130    }
131
132    addOwnranksTldsaturation(): DomainBuilder {
133        this.domain.ownranksTldsaturation = this.domainParser.ownranksTldsaturation;
134        return this;
135    }
136
137    addOwnranksRadiotest(): DomainBuilder {
138        this.domain.ownranksRadiotest = this.domainParser.ownranksRadiotest;
139        return this;
140    }
141
142    build(): Domain {
143        return this.domain;
144    }
145}

src/builders/requestBuilder.ts

1import { log, RequestOptions } from 'crawlee';
2import { config } from '../config.js';
3import { Input } from '../types/input.js';
4
5export class RequestBuilder {
6    private filters: string;
7    private sort: string;
8    private length: string;
9    private page: number;
10    private id: number;
11    private input: Input;
12
13    private includedColumns: string[] = [
14        'link',
15        'enddate',
16        'tld',
17        'godaddy_price',
18        'godaddy_bid',
19        'auction_type',
20        'ai_price_pred_price',
21        'rdap_registration',
22        'wayback_first_year',
23        'wayback_count',
24        'search_volume_volume',
25        'search_volume_cpc',
26        'google_status_is_blocked',
27        'google_status_is_faillisted',
28        'google_status_is_adult',
29        'fb_shares',
30        'fb_react',
31        'moz_domain_authority',
32        'moz_page_authority',
33        'majestic_citationflow',
34        'majestic_trustflow',
35        'majestic_extbacklinks',
36        'ownranks_brandrank',
37        'ownranks_tldsaturation',
38        'ownranks_radiotest',
39    ];
40
41    constructor(input: Input, page?: number) {
42        this.filters = '';
43        this.sort = '';
44        this.length = '';
45        this.page = page ?? 0;
46        this.id = 0;
47        this.input = input;
48    }
49
50    addTldFilters(): RequestBuilder {
51        if (this.input.filterTldCom) this.filters += 'com,';
52        if (this.input.filterTldNet) this.filters += 'net,';
53        if (this.input.filterTldOrg) this.filters += 'org,';
54        if (this.input.filterTldIo) this.filters += 'io,';
55        if (this.input.filterTldUk) this.filters += 'uk,';
56        if (this.input.filterTldN) this.filters += 'n,';
57        if (this.input.filterTldOther) this.filters += 'other,';
58
59        return this;
60    }
61
62    addLengthFilters(): RequestBuilder {
63        if (this.input.filterLengthMin > this.input.filterLengthMax) throw new Error('Min length cannot be greater than max length');
64        this.length += `min[domain_length]=${this.input.filterLengthMin}&max[domain_length]=${this.input.filterLengthMax}`;
65
66        return this;
67    }
68
69    addCharasterFilters(): RequestBuilder {
70        if (this.input.filterCharsHyp) this.filters += 'no-dashes,';
71        if (this.input.filterCharsNum) this.filters += 'no-numbers,';
72        if (this.input.filterCharsLet) this.filters += 'no-letters,';
73
74        return this;
75    }
76
77    addGoogleFilters(): RequestBuilder {
78        if (this.input.filterGglNA) this.filters += 'not-adult,';
79        if (this.input.filterGglNB) this.filters += 'not-faillisted-or-blocked,';
80        if (this.input.filterGglA) this.filters += 'Adult,';
81        if (this.input.filterGglB) this.filters += 'faillisted-or-blocked,';
82
83        return this;
84    }
85
86    addAuctionFilters(): RequestBuilder {
87        if (this.input.filterAucTypAuc) this.filters += 'godaddy-expiring,';
88        if (this.input.filterAucTypClo) this.filters += 'godaddy-closeouts,';
89        if (this.input.filterAucTypDel) this.filters += 'pending-delete,';
90
91        return this;
92    }
93
94    addExpDateFilter(): RequestBuilder {
95        const dateObj = new Date(this.input.filterExpDate);
96        const todayObj = new Date();
97        todayObj.setHours(0, 0, 0, 0);
98
99        const diffTime = dateObj.getTime() - todayObj.getTime();
100
101        let diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
102
103        if (diffTime < 0) {
104            log.warning('Selected date is in the past, setting it to today.');
105            diffDays = 0;
106        }
107
108        switch (diffDays) {
109            case 0:
110                this.filters += 'today,';
111                break;
112            case 1:
113                this.filters += 'tomorrow,';
114                break;
115            case 2:
116                this.filters += 'today2,';
117                break;
118            case 3:
119                this.filters += 'today3,';
120                break;
121            case 4:
122                this.filters += 'today4,';
123                break;
124            default:
125                log.warning('Date is outside of allowed range of 4 days from today. Setting it to today.');
126                this.filters += 'today,';
127                break;
128        }
129
130        return this;
131    }
132
133    addSort(): RequestBuilder {
134        this.sort = `${this.input.sortDir === 'asc' ? '' : '-'}${this.input.sortBy}`;
135
136        return this;
137    }
138
139    build(): RequestOptions {
140        const columns = this.includedColumns.map((col) => `columns[]=${col}`).join('&');
141        const payload = `filters=${this.filters}&sort=${this.sort}&page=${this.page}&${columns}&id=${this.id}`;
142
143        return {
144            url: config.fetchUrl,
145            method: 'POST',
146            uniqueKey: `page-${this.page}`,
147            headers: {
148                'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
149                origin: config.originUrl,
150            },
151            payload,
152        };
153    }
154}

src/helpers/helper.ts

1import { RequestOptions } from 'crawlee';
2import { RequestBuilder } from '../builders/requestBuilder.js';
3import { config } from '../config.js';
4import { Input } from '../types/input.js';
5
6export async function generateRequests(input: Input): Promise<RequestOptions[]> {
7    const requests: RequestOptions[] = [];
8
9    const pages = Math.ceil(input.numDomains / config.domainsPerPage);
10
11    for (let i = 0; i < pages; i++) {
12        const requestBuilder = new RequestBuilder(input, i);
13
14        const request = requestBuilder
15            .addTldFilters()
16            .addLengthFilters()
17            .addCharasterFilters()
18            .addGoogleFilters()
19            .addAuctionFilters()
20            .addExpDateFilter()
21            .addSort()
22            .build();
23
24        requests.push(request);
25    }
26
27    return requests;
28}
29
30export function convertToNumber(str: string) {
31    if (str.startsWith('$')) {
32        str = str.slice(1);
33    }
34
35    let number = parseFloat(str);
36
37    if (str.toLowerCase().endsWith('k')) {
38        number *= 1000;
39    }
40
41    if (str.toLowerCase().endsWith('m')) {
42        number *= 1000000;
43    }
44
45    return number;
46}

src/parsers/domainParser.ts

1import { Cheerio, Element } from 'cheerio';
2import { config } from '../config.js';
3import { convertToNumber } from '../helpers/helper.js';
4
5export class DomainParser {
6    element: Cheerio<Element>;
7
8    constructor(element: Cheerio<Element>) {
9        this.element = element;
10    }
11
12    get name(): string {
13        return this.element.find('td:nth-child(1)').text();
14    }
15
16    get link(): string | undefined {
17        const ref = this.element.find('td:nth-child(2) > a').attr('href');
18        return ref ? `${config.originUrl}${ref}` : undefined;
19    }
20
21    get enddate(): Date {
22        return new Date(this.element.find('td:nth-child(3)').text());
23    }
24
25    get tld(): string {
26        return this.element.find('td:nth-child(4)').text();
27    }
28
29    get godaddyPrice(): number | undefined {
30        const price = this.element.find('td:nth-child(5)').text();
31        return price !== '' ? convertToNumber(price) : undefined;
32    }
33
34    get godaddyBid(): number | undefined {
35        const bid = this.element.find('td:nth-child(6)').text();
36        return bid !== '' ? convertToNumber(bid) : undefined;
37    }
38
39    get auctionType(): string | undefined {
40        const type = this.element.find('td:nth-child(7)').text();
41        return type !== '' ? type : undefined;
42    }
43
44    get aiPricePredPrice(): number | undefined {
45        const price = this.element.find('td:nth-child(8)').text();
46        return price !== '' ? convertToNumber(price) : undefined;
47    }
48
49    get rdapRegistration(): number | undefined {
50        const year = this.element.find('td:nth-child(9)').text();
51        return year !== '' ? convertToNumber(year) : undefined;
52    }
53
54    get waybackFirstYear(): number | undefined {
55        const year = this.element.find('td:nth-child(10)').text();
56        return year !== '' ? convertToNumber(year) : undefined;
57    }
58
59    get waybackCount(): number | undefined {
60        const count = this.element.find('td:nth-child(11)').text();
61        return count !== '' ? convertToNumber(count) : undefined;
62    }
63
64    get searchVolumeVolume(): number | undefined {
65        const volume = this.element.find('td:nth-child(12)').text();
66        return volume !== '' ? convertToNumber(volume) : undefined;
67    }
68
69    get searchVolumeCpc(): number | undefined {
70        const cpc = this.element.find('td:nth-child(13)').text();
71        return cpc !== '' ? convertToNumber(cpc) : undefined;
72    }
73
74    get googleStatusIsBlocked(): boolean {
75        const status = this.element.find('td:nth-child(14)').text();
76        return status === '1';
77    }
78
79    get googleStatusIsFaillisted(): boolean {
80        const status = this.element.find('td:nth-child(15)').text();
81        return status === '1';
82    }
83
84    get googleStatusIsAdult(): boolean {
85        const status = this.element.find('td:nth-child(16)').text();
86        return status === '1';
87    }
88
89    get fbShares(): number | undefined {
90        const shares = this.element.find('td:nth-child(17)').text();
91        return shares !== '' ? convertToNumber(shares) : undefined;
92    }
93
94    get fbReact(): number | undefined {
95        const react = this.element.find('td:nth-child(18)').text();
96        return react !== '' ? convertToNumber(react) : undefined;
97    }
98
99    get mozDomainAuthority(): number | undefined {
100        const authority = this.element.find('td:nth-child(19)').text();
101        return authority !== '' ? convertToNumber(authority) : undefined;
102    }
103
104    get mozPageAuthority(): number | undefined {
105        const authority = this.element.find('td:nth-child(20)').text();
106        return authority !== '' ? convertToNumber(authority) : undefined;
107    }
108
109    get majesticCitationflow(): number | undefined {
110        const flow = this.element.find('td:nth-child(21)').text();
111        return flow !== '' ? convertToNumber(flow) : undefined;
112    }
113
114    get majesticTrustflow(): number | undefined {
115        const flow = this.element.find('td:nth-child(22)').text();
116        return flow !== '' ? convertToNumber(flow) : undefined;
117    }
118
119    get majesticExtbacklinks(): number | undefined {
120        const links = this.element.find('td:nth-child(23)').text();
121        return links !== '' ? convertToNumber(links) : undefined;
122    }
123
124    get ownranksBrandrank(): number | undefined {
125        const rank = this.element.find('td:nth-child(24)').text();
126        return rank !== '' ? convertToNumber(rank) : undefined;
127    }
128
129    get ownranksTldsaturation(): number | undefined {
130        const saturation = this.element.find('td:nth-child(25)').text();
131        return saturation !== '' ? convertToNumber(saturation) : undefined;
132    }
133
134    get ownranksRadiotest(): number | undefined {
135        const test = this.element.find('td:nth-child(26)').text();
136        return test !== '' ? convertToNumber(test) : undefined;
137    }
138}

src/parsers/pageParser.ts

1import { Cheerio, CheerioAPI, Element } from 'cheerio';
2import { DomainParser } from '../parsers/domainParser.js';
3import { DomainBuilder } from '../builders/domainBuilder.js';
4import { Domain } from '../types/domain.js';
5
6export class PageParser {
7    $: CheerioAPI;
8
9    constructor(api: CheerioAPI) {
10        this.$ = api;
11    }
12
13    get domains(): Domain[] {
14        const domains: Domain[] = [];
15
16        this.$('#result_win > table > tbody > tr').each((_i, el) => {
17            const domainElement: Cheerio<Element> = this.$(el);
18            const domainParser = new DomainParser(domainElement);
19            const domainBuilder = new DomainBuilder(domainParser);
20
21            const domain: Domain = domainBuilder
22                .addName()
23                .addLink()
24                .addEnddate()
25                .addTld()
26                .addGodaddyPrice()
27                .addGodaddyBid()
28                .addAuctionType()
29                .addAiPricePredPrice()
30                .addRdapRegistration()
31                .addWaybackFirstYear()
32                .addWaybackCount()
33                .addSearchVolumeVolume()
34                .addSearchVolumeCpc()
35                .addGoogleStatusIsBlocked()
36                .addGoogleStatusIsFaillisted()
37                .addGoogleStatusIsAdult()
38                .addFbShares()
39                .addFbReact()
40                .addMozDomainAuthority()
41                .addMozPageAuthority()
42                .addMajesticCitationflow()
43                .addMajesticTrustflow()
44                .addMajesticExtbacklinks()
45                .addOwnranksBrandrank()
46                .addOwnranksTldsaturation()
47                .addOwnranksRadiotest()
48                .build();
49
50            domains.push(domain);
51        });
52
53        return domains;
54    }
55}

src/types/config.ts

1export type Config = {
2    domainsPerPage: number;
3    originUrl: string;
4    fetchUrl: string;
5}

src/types/domain.ts

1export type Domain = {
2    name: string;
3    link: string | undefined;
4    enddate: Date;
5    tld: string;
6    godaddyPrice: number | undefined;
7    godaddyBid: number | undefined;
8    auctionType: string | undefined;
9    aiPricePredPrice: number | undefined;
10    rdapRegistration: number | undefined;
11    waybackFirstYear: number | undefined;
12    waybackCount: number | undefined;
13    searchVolumeVolume: number | undefined;
14    searchVolumeCpc: number | undefined;
15    googleStatusIsBlocked: boolean | undefined;
16    googleStatusIsFaillisted: boolean | undefined;
17    googleStatusIsAdult: boolean | undefined;
18    fbShares: number | undefined;
19    fbReact: number | undefined;
20    majesticExtbacklinks: number | undefined;
21    majesticCitationflow: number | undefined;
22    majesticTrustflow: number | undefined;
23    mozDomainAuthority: number | undefined;
24    mozPageAuthority: number | undefined;
25    ownranksBrandrank: number | undefined;
26    ownranksTldsaturation: number | undefined;
27    ownranksRadiotest: number | undefined;
28}

src/types/input.ts

1export type Input = {
2    numDomains: number;
3    sortBy: SortByOption;
4    sortDir: SortDirection;
5    filterTldCom: boolean;
6    filterTldNet: boolean;
7    filterTldOrg: boolean;
8    filterTldIo: boolean;
9    filterTldUk: boolean;
10    filterTldN: boolean;
11    filterTldOther: boolean;
12    filterLengthMin: number;
13    filterLengthMax: number;
14    filterCharsHyp: boolean;
15    filterCharsNum: boolean;
16    filterCharsLet: boolean;
17    filterGglNA: boolean;
18    filterGglNB: boolean;
19    filterGglA: boolean;
20    filterGglB: boolean;
21    filterAucTypAuc: boolean;
22    filterAucTypClo: boolean;
23    filterAucTypDel: boolean;
24    filterExpDate: string;
25}
26
27export type SortByOption =
28    | 'name'
29    | 'link'
30    | 'enddate'
31    | 'tld'
32    | 'godaddy_price'
33    | 'auction_type'
34    | 'godaddy_bid'
35    | 'ai_price_pred_price'
36    | 'rdap_registration'
37    | 'wayback_first_year'
38    | 'wayback_count'
39    | 'search_volume_volume'
40    | 'search_volume_cpc'
41    | 'google_status_is_blocked'
42    | 'google_status_is_faillisted'
43    | 'google_status_is_adult'
44    | 'fb_shares'
45    | 'fb_react'
46    | 'majestic_extbacklinks'
47    | 'majestic_citationflow'
48    | 'majestic_trustflow'
49    | 'moz_domain_authority'
50    | 'moz_page_authority'
51    | 'ownranks_brandrank'
52    | 'ownranks_tldsaturation'
53    | 'ownranks_radiotest';
54
55export type SortDirection = 'asc' | 'desc';

src/config.ts

1import { Config } from './types/config.js';
2
3export const config: Config = {
4    domainsPerPage: 25,
5    originUrl: 'https://expireddomains.com',
6    fetchUrl: 'https://expireddomains.com/site/search',
7};

src/main.ts

1import { Actor } from 'apify';
2import { CheerioCrawler, Dataset } from 'crawlee';
3import { load } from 'cheerio';
4import { PageParser } from './parsers/pageParser.js';
5import { generateRequests } from './helpers/helper.js';
6import { Input } from './types/input.js';
7import { Domain } from './types/domain.js';
8
9await Actor.main(async () => {
10    const input = await Actor.getInput<Input>();
11
12    if (!input) throw new Error('Input is missing');
13
14    const proxyConfiguration = await Actor.createProxyConfiguration();
15
16    const crawler = new CheerioCrawler({
17        proxyConfiguration,
18        requestHandler: async ({ body }) => {
19            const bodyJson = JSON.parse(body.toString());
20            const $ = load(bodyJson.result);
21
22            const pageParser = new PageParser($);
23
24            for (const domain of pageParser.domains) {
25                await Dataset.pushData<Domain>(domain);
26            }
27        },
28    });
29
30    const requests = await generateRequests(input);
31
32    await crawler.run(requests);
33});

test/domainParser.test.ts

1import { DomainParser } from '../src/parsers/domainParser.js'
2import { Cheerio, load } from 'cheerio'
3
4const sampleHtml = `
5<div>
6    <table>
7        <tbody>
8            <tr>
9                <td>example.com</td>
10                <td><a href="/link">Link</a></td>
11                <td>2024-06-01</td>
12                <td>com</td>
13                <td>$10</td>
14                <td>$5</td>
15                <td>Auction</td>
16                <td>$20</td>
17                <td>1999</td>
18                <td>2000</td>
19                <td>100</td>
20                <td>5000</td>
21                <td>$2</td>
22                <td>1</td>
23                <td>1</td>
24                <td>1</td>
25                <td>100</td>
26                <td>200</td>
27                <td>50</td>
28                <td>60</td>
29                <td>70</td>
30                <td>80</td>
31                <td>1000</td>
32                <td>90</td>
33                <td>95</td>
34                <td>85</td>
35            </tr>
36        </tbody>
37    </table>
38</div>
39`
40
41describe('DomainParser', () => {
42    let parser: DomainParser
43
44    beforeAll(() => {
45        const $ = load(sampleHtml)
46        const element: Cheerio<any> = $('tr')
47        parser = new DomainParser(element)
48    })
49
50    test('should parse name correctly', () => {
51        expect(parser.name).toBe('example.com')
52    })
53
54    test('should parse link correctly', () => {
55        expect(parser.link).toBe('https://expireddomains.com/link')
56    })
57
58    test('should parse enddate correctly', () => {
59        expect(parser.enddate).toEqual(new Date('2024-06-01'))
60    })
61
62    test('should parse tld correctly', () => {
63        expect(parser.tld).toBe('com')
64    })
65
66    test('should parse godaddyPrice correctly', () => {
67        expect(parser.godaddyPrice).toBe(10)
68    })
69
70    test('should parse godaddyBid correctly', () => {
71        expect(parser.godaddyBid).toBe(5)
72    })
73
74    test('should parse auctionType correctly', () => {
75        expect(parser.auctionType).toBe('Auction')
76    })
77
78    test('should parse aiPricePredPrice correctly', () => {
79        expect(parser.aiPricePredPrice).toBe(20)
80    })
81
82    test('should parse rdapRegistration correctly', () => {
83        expect(parser.rdapRegistration).toBe(1999)
84    })
85
86    test('should parse waybackFirstYear correctly', () => {
87        expect(parser.waybackFirstYear).toBe(2000)
88    })
89
90    test('should parse waybackCount correctly', () => {
91        expect(parser.waybackCount).toBe(100)
92    })
93
94    test('should parse searchVolumeVolume correctly', () => {
95        expect(parser.searchVolumeVolume).toBe(5000)
96    })
97
98    test('should parse searchVolumeCpc correctly', () => {
99        expect(parser.searchVolumeCpc).toBe(2)
100    })
101
102    test('should parse googleStatusIsBlocked correctly', () => {
103        expect(parser.googleStatusIsBlocked).toBe(true)
104    })
105
106    test('should parse googleStatusIsFaillisted correctly', () => {
107        expect(parser.googleStatusIsFaillisted).toBe(true)
108    })
109
110    test('should parse googleStatusIsAdult correctly', () => {
111        expect(parser.googleStatusIsAdult).toBe(true)
112    })
113
114    test('should parse fbShares correctly', () => {
115        expect(parser.fbShares).toBe(100)
116    })
117
118    test('should parse fbReact correctly', () => {
119        expect(parser.fbReact).toBe(200)
120    })
121
122    test('should parse mozDomainAuthority correctly', () => {
123        expect(parser.mozDomainAuthority).toBe(50)
124    })
125
126    test('should parse mozPageAuthority correctly', () => {
127        expect(parser.mozPageAuthority).toBe(60)
128    })
129
130    test('should parse majesticCitationflow correctly', () => {
131        expect(parser.majesticCitationflow).toBe(70)
132    })
133
134    test('should parse majesticTrustflow correctly', () => {
135        expect(parser.majesticTrustflow).toBe(80)
136    })
137
138    test('should parse majesticExtbacklinks correctly', () => {
139        expect(parser.majesticExtbacklinks).toBe(1000)
140    })
141
142    test('should parse ownranksBrandrank correctly', () => {
143        expect(parser.ownranksBrandrank).toBe(90)
144    })
145
146    test('should parse ownranksTldsaturation correctly', () => {
147        expect(parser.ownranksTldsaturation).toBe(95)
148    })
149
150    test('should parse ownranksRadiotest correctly', () => {
151        expect(parser.ownranksRadiotest).toBe(85)
152    })
153})

.dockerignore

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

.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    "rules": {
21        "import/no-extraneous-dependencies": [
22            "error", {
23                "devDependencies": true
24            }
25        ]
26    }
27
28}

.gitignore

1# This file tells Git which files shouldn't be added to source control
2
3.DS_Store
4.idea
5dist
6node_modules
7apify_storage
8storage
9
10# Added by Apify CLI
11.venv

jest.config.js

1/** @type {import('ts-jest').JestConfigWithTsJest} */
2export default {
3  preset: 'ts-jest',
4  testEnvironment: 'node',
5  moduleFileExtensions: ['ts', 'js'],
6  globals: {
7    'ts-jest': {
8      tsconfig: 'tsconfig.json',
9    },
10  },
11  moduleNameMapper: {
12    "^(\\.\\.?\\/.+)\\.js$": "$1",
13  }
14};

package.json

1{
2	"name": "expired-domains-scraper",
3	"version": "0.0.1",
4	"type": "module",
5	"description": "The Expired Domains Scraper automates finding valuable expired domains from expireddomains.com, offering filters and sorting by SEO metrics and auction details for efficient domain acquisition.",
6	"engines": {
7		"node": ">=18.0.0"
8	},
9	"dependencies": {
10		"apify": "^3.1.10",
11		"cheerio": "^1.0.0-rc.12",
12		"crawlee": "^3.5.4"
13	},
14	"devDependencies": {
15		"@apify/eslint-config-ts": "^0.3.0",
16		"@apify/tsconfig": "^0.1.0",
17		"@types/jest": "^29.5.12",
18		"@typescript-eslint/eslint-plugin": "^6.7.2",
19		"@typescript-eslint/parser": "^6.7.2",
20		"eslint": "^8.50.0",
21		"jest": "^29.7.0",
22		"ts-jest": "^29.1.5",
23		"tsx": "^4.6.2",
24		"typescript": "^5.3.3"
25	},
26	"scripts": {
27		"start": "npm run start:dev",
28		"start:prod": "node dist/main.js",
29		"start:dev": "tsx src/main.ts",
30		"build": "tsc",
31		"lint": "eslint ./src --ext .ts",
32		"lint:fix": "eslint ./src --ext .ts --fix",
33		"test": "jest"
34	},
35	"author": "Martin Fanta",
36	"license": "MIT"
37}

tsconfig.json

1{
2    "extends": "@apify/tsconfig",
3    "compilerOptions": {
4        "esModuleInterop": true,
5        "module": "NodeNext",
6        "moduleResolution": "NodeNext",
7        "target": "ES2022",
8        "outDir": "dist",
9        "noUnusedLocals": false,
10        "skipLibCheck": true,
11        "lib": ["DOM"],
12        "paths": {
13            "src/*": [
14                "./src/*"
15            ],
16        }
17    },
18    "include": [
19        "./src/**/*"
20    ]
21}
Developer
Maintained by Community

Actor Metrics

  • 9 monthly users

  • 3 stars

  • >99% runs succeeded

  • Created in Jun 2024

  • Modified 6 months ago