1+ import { createHash } from 'node:crypto' ;
12import { readFile } from 'node:fs/promises' ;
23
34import { ImpitHttpClient } from '@crawlee/impit-client' ;
@@ -18,6 +19,7 @@ import {
1819} from 'crawlee' ;
1920
2021import { chargeFetch , chargeSearch } from './charging.js' ;
22+ import type { CrawlerKind } from './const.js' ;
2123import { ContentCrawlerTypes , GOOGLE_STANDARD_RESULTS_PER_PAGE } from './const.js' ;
2224import { deduplicateResults , scrapeOrganicResults } from './google-search/google-extractors-urls.js' ;
2325import {
@@ -27,10 +29,19 @@ import {
2729 TEXT_DOCUMENT_CONTENT_TYPES ,
2830} from './request-handler.js' ;
2931import { addEmptyResultToResponse , addResultToResponse , sendResponseError , sendResponseIfFinished } from './responses.js' ;
30- import type { ContentCrawlerOptions , ContentCrawlerUserData , Output , SearchCrawlerUserData } from './types.js' ;
32+ import type {
33+ ContentCrawlerOptions ,
34+ ContentCrawlerUserData ,
35+ Output ,
36+ ProxyOptions ,
37+ SearchCrawlerOptions ,
38+ SearchCrawlerUserData ,
39+ } from './types.js' ;
3140import { addTimeMeasureEvent , createRequest , createSearchRequest } from './utils.js' ;
3241
33- const crawlers = new Map < string , CheerioCrawler | PlaywrightCrawler > ( ) ;
42+ type Crawler = CheerioCrawler | PlaywrightCrawler ;
43+
44+ const crawlers = new Map < string , Crawler > ( ) ;
3445const client = new MemoryStorage ( { persistStorage : false } ) ;
3546
3647const contentCrawlerHttpClient = new ImpitHttpClient ( {
@@ -56,26 +67,58 @@ async function getGhosteryBlocker(): Promise<PlaywrightBlocker | undefined> {
5667 }
5768}
5869
59- export function getCrawlerKey ( crawlerOptions : CheerioCrawlerOptions | PlaywrightCrawlerOptions ) {
60- return JSON . stringify ( crawlerOptions ) ;
70+ /**
71+ * Identifies a crawler in the `crawlers` cache, and names its request queue. Keyed off the caller's
72+ * proxy options rather than the built `ProxyConfiguration`, whose child logger snapshots the log level
73+ * and whose URL rotation state mutates - both of which kept changing the key. Hashed because the key
74+ * is logged and used as a queue name, while the options can hold credentials.
75+ */
76+ export function getCrawlerKey ( kind : CrawlerKind , proxyOptions : ProxyOptions ) : string {
77+ const hash = createHash ( 'sha1' ) . update ( JSON . stringify ( proxyOptions ) ) . digest ( 'hex' ) ;
78+ return `${ kind } -${ hash . slice ( 0 , 12 ) } ` ;
79+ }
80+
81+ /** How many crawlers are alive. Exported so tests can assert that requests do not fork new ones. */
82+ export function getCrawlerCount ( ) {
83+ return crawlers . size ;
84+ }
85+
86+ async function getOrCreateCrawler ( key : string , kind : CrawlerKind , startCrawler : boolean , build : ( ) => Promise < Crawler > ) {
87+ const cached = crawlers . get ( key ) ;
88+ if ( cached ) {
89+ return cached ;
90+ }
91+
92+ log . info ( `Creating new ${ kind } crawler with key ${ key } ` ) ;
93+ const crawler = await build ( ) ;
94+ if ( startCrawler ) {
95+ crawler . run ( ) . then (
96+ ( ) => log . warning ( `Crawler ${ kind } has finished` ) ,
97+ // eslint-disable-next-line @typescript-eslint/no-empty-function
98+ ( ) => { } ,
99+ ) ;
100+ log . info ( `Crawler ${ kind } has started 💪🏼` ) ;
101+ }
102+ crawlers . set ( key , crawler ) ;
103+ log . info ( `Number of crawlers ${ crawlers . size } ` ) ;
104+ return crawler ;
61105}
62106
63107/**
64- * Adds a content crawl request to selected content crawler.
65- * Get existing crawler based on crawlerOptions and scraperSettings, if not present -> create new
108+ * Adds a content crawl request to the content crawler identified by `contentCrawlerKey`.
66109 */
67110export const addContentCrawlRequest = async (
68111 request : RequestOptions < ContentCrawlerUserData > ,
69112 responseId : string ,
70113 contentCrawlerKey : string ,
71114) => {
72115 const crawler = crawlers . get ( contentCrawlerKey ) ;
73- const name = crawler instanceof PlaywrightCrawler ? 'playwright' : 'cheerio' ;
74-
75116 if ( ! crawler ) {
76- log . error ( `Content crawler not found: key ${ contentCrawlerKey } ` ) ;
117+ log . error ( `Content crawler not found: ${ contentCrawlerKey } ` ) ;
77118 return ;
78119 }
120+
121+ const name = crawler instanceof PlaywrightCrawler ? 'playwright' : 'cheerio' ;
79122 try {
80123 await crawler . requestQueue ! . addRequest ( request ) ;
81124 // create an empty result in search request response
@@ -92,17 +135,13 @@ export const addContentCrawlRequest = async (
92135 * A crawler won't be created if it already exists.
93136 */
94137export async function createAndStartSearchCrawler (
95- searchCrawlerOptions : CheerioCrawlerOptions ,
138+ searchCrawlerOptions : SearchCrawlerOptions ,
96139 startCrawler = true ,
97140) {
98- const key = getCrawlerKey ( searchCrawlerOptions ) ;
99- if ( crawlers . has ( key ) ) {
100- return { key, crawler : crawlers . get ( key ) } ;
101- }
102-
103- log . info ( `Creating new cheerio crawler with key ${ key } ` ) ;
104- const crawler = new CheerioCrawler ( {
105- ...( searchCrawlerOptions as CheerioCrawlerOptions ) ,
141+ const { crawlerOptions, proxyOptions } = searchCrawlerOptions ;
142+ const key = getCrawlerKey ( 'search' , proxyOptions ) ;
143+ const crawler = await getOrCreateCrawler ( key , 'search' , startCrawler , async ( ) => new CheerioCrawler ( {
144+ ...crawlerOptions ,
106145 requestQueue : await RequestQueue . open ( key , { storageClient : client } ) ,
107146 requestHandler : async ( { request, $ : _$ , addRequests } : CheerioCrawlingContext < SearchCrawlerUserData > ) => {
108147 // NOTE: we need to cast this to fix `cheerio` type errors
@@ -150,7 +189,7 @@ export async function createAndStartSearchCrawler(
150189 collectedResults : deduplicated ,
151190 currentPage : nextPage ,
152191 } ,
153- searchCrawlerOptions . proxyConfiguration ,
192+ proxyOptions ,
154193 nextOffset ,
155194 ) ;
156195 await addRequests ( [ nextRequest ] ) ;
@@ -182,17 +221,8 @@ export async function createAndStartSearchCrawler(
182221 const errorResponse = { errorMessage : err . message } ;
183222 sendResponseError ( request . uniqueKey , JSON . stringify ( errorResponse ) ) ;
184223 } ,
185- } ) ;
186- if ( startCrawler ) {
187- crawler . run ( ) . then (
188- ( ) => log . warning ( 'Google-search-crawler has finished' ) ,
189- // eslint-disable-next-line @typescript-eslint/no-empty-function
190- ( ) => { } ,
191- ) ;
192- log . info ( 'Google-search-crawler has started 🫡' ) ;
193- }
194- crawlers . set ( key , crawler ) ;
195- log . info ( `Number of crawlers ${ crawlers . size } ` ) ;
224+ } ) ) ;
225+
196226 return { key, crawler } ;
197227}
198228
@@ -205,27 +235,15 @@ export async function createAndStartContentCrawler(
205235 contentCrawlerOptions : ContentCrawlerOptions ,
206236 startCrawler = true ,
207237) {
208- const { type : crawlerType , crawlerOptions } = contentCrawlerOptions ;
238+ const { type : crawlerType , crawlerOptions, proxyOptions } = contentCrawlerOptions ;
209239
210- const key = getCrawlerKey ( crawlerOptions ) ;
211- if ( crawlers . has ( key ) ) {
212- return { key, crawler : crawlers . get ( key ) } ;
213- }
240+ const key = getCrawlerKey ( crawlerType , proxyOptions ) ;
241+ const crawler = await getOrCreateCrawler ( key , crawlerType , startCrawler , async ( ) => (
242+ crawlerType === ContentCrawlerTypes . PLAYWRIGHT
243+ ? createPlaywrightContentCrawler ( crawlerOptions , key )
244+ : createCheerioContentCrawler ( crawlerOptions , key )
245+ ) ) ;
214246
215- const crawler = crawlerType === 'playwright'
216- ? await createPlaywrightContentCrawler ( crawlerOptions , key )
217- : await createCheerioContentCrawler ( crawlerOptions , key ) ;
218-
219- if ( startCrawler ) {
220- crawler . run ( ) . then (
221- ( ) => log . warning ( `Crawler ${ crawlerType } has finished` ) ,
222- // eslint-disable-next-line @typescript-eslint/no-empty-function
223- ( ) => { } ,
224- ) ;
225- log . info ( `Crawler ${ crawlerType } has started 💪🏼` ) ;
226- }
227- crawlers . set ( key , crawler ) ;
228- log . info ( `Number of crawlers ${ crawlers . size } ` ) ;
229247 return { key, crawler } ;
230248}
231249
@@ -261,7 +279,6 @@ async function createPlaywrightContentCrawler(
261279 crawlerOptions : PlaywrightCrawlerOptions ,
262280 key : string ,
263281) : Promise < PlaywrightCrawler > {
264- log . info ( `Creating new playwright crawler with key ${ key } ` ) ;
265282 const blocker = await getGhosteryBlocker ( ) ;
266283 return new PlaywrightCrawler ( {
267284 ...crawlerOptions ,
@@ -284,7 +301,6 @@ async function createCheerioContentCrawler(
284301 crawlerOptions : CheerioCrawlerOptions ,
285302 key : string ,
286303) : Promise < CheerioCrawler > {
287- log . info ( `Creating new cheerio crawler with key ${ key } ` ) ;
288304 return new CheerioCrawler ( {
289305 ...crawlerOptions ,
290306 keepAlive : crawlerOptions . keepAlive ,
@@ -305,20 +321,19 @@ async function createCheerioContentCrawler(
305321}
306322
307323/**
308- * Adds a search request to the Google search crawler.
324+ * Adds a search request to the search crawler identified by `searchCrawlerKey` .
309325 * Create a response for the request and set the desired number of results (maxResults).
310326 */
311327export const addSearchRequest = async (
312328 request : RequestOptions < ContentCrawlerUserData > ,
313- searchCrawlerOptions : CheerioCrawlerOptions ,
329+ searchCrawlerKey : string ,
314330) => {
315- const key = getCrawlerKey ( searchCrawlerOptions ) ;
316- const crawler = crawlers . get ( key ) ;
317-
331+ const crawler = crawlers . get ( searchCrawlerKey ) ;
318332 if ( ! crawler ) {
319- log . error ( `Cheerio crawler not found: key ${ key } ` ) ;
333+ log . error ( `Search crawler not found: ${ searchCrawlerKey } ` ) ;
320334 return ;
321335 }
336+
322337 addTimeMeasureEvent ( request . userData ! , 'before-cheerio-queue-add' ) ;
323338 await crawler . requestQueue ! . addRequest ( request ) ;
324339 log . info ( `Added request to cheerio-google-search-crawler: ${ request . url } ` ) ;
0 commit comments