@crawlee/basic 4.0.0-beta.122 → 4.0.0-beta.123

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
- import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, ConcurrencySystemOptions, CrawleeLogger, CrawlingContext, DatasetExportOptions, EnqueueLinksOptions, FinalStatistics, GetUserDataFromRequest, IConcurrencySystem, IProxyConfiguration, IRequestLoader, IRequestManager, IStatistics, Request, RequestsLike, RouterHandler, RouterRoutes, SkippedRequestCallback, Source, StatisticState, StorageIdentifier, StorageWritePolicy, TaskLoopPredicates, TypedRequestsLike } from '@crawlee/core';
2
- import { ConcurrencySystem, Configuration, ContextPipeline, Dataset, EventManager, RequestQueue } from '@crawlee/core';
1
+ import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, ConcurrencySystemOptions, CrawleeLogger, CrawlingContext, DatasetExportOptions, EnqueueUrlsOptions, FinalStatistics, GetUserDataFromRequest, IConcurrencySystem, IProxyConfiguration, IRequestLoader, IRequestManager, IStatistics, RequestsLike, RouterHandler, RouterRoutes, SkippedRequestCallback, Source, StatisticState, StorageIdentifier, StorageWritePolicy, TaskLoopPredicates, TypedRequestsLike } from '@crawlee/core';
2
+ import { ConcurrencySystem, Configuration, ContextPipeline, Request, Dataset, EventManager, RequestQueue } from '@crawlee/core';
3
3
  import { BaseHttpClient } from '@crawlee/http-client';
4
- import type { Awaitable, BatchAddRequestsResult, Dictionary, ISession, ISessionPool, ProxyInfo, SetStatusMessageOptions, StorageBackend } from '@crawlee/types';
4
+ import type { Awaitable, Dictionary, ISession, ISessionPool, ProxyInfo, SetStatusMessageOptions, StorageBackend } from '@crawlee/types';
5
5
  import { RobotsTxtFile } from '@crawlee/utils';
6
- import type { ReadonlyDeep, SetRequired } from 'type-fest';
6
+ import type { ReadonlyDeep } from 'type-fest';
7
7
  import { z } from 'zod';
8
8
  import { TimeoutError } from '@apify/timeout';
9
9
  export interface BasicCrawlingContext<UserData extends Dictionary = Dictionary> extends CrawlingContext<UserData> {
@@ -733,6 +733,11 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
733
733
  * the batches via `waitBetweenBatchesMillis`. If you want to wait for all batches to be added to the queue, you can use
734
734
  * the `waitForAllRequestsToBeAdded` promise you get in the response object.
735
735
  *
736
+ * Optionally, the requests can be filtered using `include`/`exclude` glob or regexp patterns and an
737
+ * enqueue `strategy` (both AND-ed together, same as {@link CrawlingContext.enqueueLinks|`enqueueLinks`}),
738
+ * relative to `baseUrl`. Unlike `enqueueLinks`, there is no implicit "current page" to anchor the strategy
739
+ * to, so `strategy` defaults to {@link EnqueueStrategy.All|`all`} here.
740
+ *
736
741
  * This is an alias for calling `addRequestsBatched()` on the implicit `RequestQueue` for this crawler instance.
737
742
  *
738
743
  * @param requests The requests to add
@@ -827,14 +832,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
827
832
  private delayRequest;
828
833
  /** Handles a single request - runs the request handler with retries, error handling, and lifecycle management. */
829
834
  private handleRequest;
830
- /**
831
- * Wrapper around the crawling context's `enqueueLinks` method:
832
- * - Injects `crawlDepth` to each request being added based on the crawling context request.
833
- * - Provides defaults for the `enqueueLinks` options based on the crawler configuration.
834
- * - These options can be overridden by the user.
835
- * @internal
836
- */
837
- protected enqueueLinksWithCrawlDepth(options: SetRequired<EnqueueLinksOptions, 'urls'>, request: Request<Dictionary>, requestManager: IRequestManager): Promise<BatchAddRequestsResult>;
838
835
  /**
839
836
  * Generator function that yields requests injected with the given crawl depth.
840
837
  * @internal
@@ -893,7 +890,7 @@ export interface CreateContextOptions {
893
890
  session: ISession;
894
891
  proxyInfo?: ProxyInfo;
895
892
  }
896
- export interface CrawlerAddRequestsOptions extends AddRequestsBatchedOptions {
893
+ export interface CrawlerAddRequestsOptions extends AddRequestsBatchedOptions, EnqueueUrlsOptions {
897
894
  }
898
895
  export interface CrawlerAddRequestsResult extends AddRequestsBatchedResult {
899
896
  }
@@ -1,6 +1,6 @@
1
1
  import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import { dirname } from 'node:path';
3
- import { AutoscaledPool, bindMethodsToServiceLocator, BLOCKED_STATUS_CODES, ConcurrencySystem, Configuration, ContextPipeline, ContextPipelineCleanupError, ContextPipelineInitializationError, ContextPipelineInterruptedError, createStorageTransaction, CriticalError, currentStorageTransaction, Dataset, enqueueLinks, EnqueueStrategy, EventManager, EventType, getObjectType, KeyValueStore, log, LogLevel, mergeCookies, MissingSessionError, NavigationSkippedError, NonRetryableError, OwnedOrInjected, parseArgument, purgeDefaultStorages, RequestHandlerError, parseRetryAfterHeader, RequestThrottledError, RequestManagerTandem, RequestQueue, RequestState, RetryRequestError, supportsDomainThrottling, Router, schemas, ServiceLocator, serviceLocator, Session, SessionError, SessionPool, Statistics, validateUserData, validators, withDirectStorageAccess, } from '@crawlee/core';
3
+ import { applyRequestTransform, AutoscaledPool, bindMethodsToServiceLocator, BLOCKED_STATUS_CODES, buildEnqueueStrategyPatterns, ConcurrencySystem, Configuration, constructUrlPatternObjects, ContextPipeline, ContextPipelineCleanupError, ContextPipelineInitializationError, ContextPipelineInterruptedError, createRequestOptions, createStorageTransaction, Request, CriticalError, currentStorageTransaction, Dataset, EnqueueStrategy, EventManager, EventType, filterRequestOptionsByPatterns, getObjectType, KeyValueStore, log, LogLevel, mergeCookies, MissingSessionError, NavigationSkippedError, NonRetryableError, OwnedOrInjected, parseArgument, purgeDefaultStorages, RequestHandlerError, parseRetryAfterHeader, RequestThrottledError, RequestManagerTandem, RequestQueue, RequestState, RetryRequestError, supportsDomainThrottling, Router, schemas, ServiceLocator, serviceLocator, Session, SessionError, SessionPool, Statistics, validateUserData, validators, withDirectStorageAccess, } from '@crawlee/core';
4
4
  import { BaseHttpClient, FetchHttpClient } from '@crawlee/http-client';
5
5
  import { isAsyncIterable, isIterable, ROTATE_PROXY_ERRORS } from '@crawlee/utils/internal';
6
6
  import { RobotsTxtFile } from '@crawlee/utils';
@@ -43,6 +43,33 @@ const SAFE_MIGRATION_WAIT_MILLIS = 20000;
43
43
  const deferredCleanupKey = Symbol('deferredCleanup');
44
44
  // The request timeout plumbing (the window helper, the context symbols, and the race) lives in its own module.
45
45
  export { navigationDeadlineKey, remainingNavigationWindowMillis } from './request-timeout.js';
46
+ const urlPatternSchema = z.union([
47
+ z.string(),
48
+ z.instanceof(RegExp),
49
+ schemas.objectWithKeys(['glob']),
50
+ schemas.objectWithKeys(['regexp']),
51
+ ]);
52
+ // `looseObject` (rather than `strictObject`) lets subclasses forward their own extraction-only options
53
+ // (e.g. `selector`) straight through without having to strip them out first.
54
+ const addRequestsOptionsSchema = z.looseObject({
55
+ forefront: z.boolean().optional(),
56
+ cache: z.boolean().optional(),
57
+ waitForAllRequestsToBeAdded: z.boolean().optional(),
58
+ batchSize: schemas.anyNumber.optional(),
59
+ waitBetweenBatchesMillis: schemas.anyNumber.optional(),
60
+ maxNewRequests: schemas.anyNumber.optional(),
61
+ limit: schemas.anyNumber.optional(),
62
+ baseUrl: z.string().optional(),
63
+ userData: schemas.anyObject.optional(),
64
+ label: z.string().optional(),
65
+ sessionId: z.string().optional(),
66
+ skipNavigation: z.boolean().optional(),
67
+ include: schemas.arrayOf(urlPatternSchema, 'URL patterns').min(1).optional(),
68
+ exclude: schemas.arrayOf(urlPatternSchema, 'URL patterns').optional(),
69
+ transformRequestFunction: schemas.anyFunction.optional(),
70
+ strategy: z.enum(EnqueueStrategy).optional(),
71
+ onSkippedRequest: schemas.anyFunction.optional(),
72
+ });
46
73
  export class BasicCrawler {
47
74
  static CRAWLEE_STATE_KEY = 'CRAWLEE_STATE';
48
75
  /**
@@ -598,17 +625,13 @@ export class BasicCrawler {
598
625
  return { session, proxyInfo: session?.proxyInfo };
599
626
  }
600
627
  async createContextHelpers({ request, session }) {
601
- const enqueueLinksWrapper = async (options) => {
602
- const requestManager = await this.getRequestManager();
603
- return await this.enqueueLinksWithCrawlDepth(options, request, requestManager);
604
- };
605
628
  const addRequests = async (requests, options = {}) => {
606
629
  const newCrawlDepth = request.crawlDepth + 1;
607
630
  const requestsGenerator = this.addCrawlDepthRequestGenerator(requests, newCrawlDepth);
608
- await this.addRequests(requestsGenerator, options);
631
+ return await this.addRequests(requestsGenerator, options);
609
632
  };
610
633
  const sendRequest = createSendRequest(this.httpClient, request, session);
611
- return { enqueueLinks: enqueueLinksWrapper, addRequests, sendRequest };
634
+ return { addRequests, sendRequest };
612
635
  }
613
636
  buildFinalContextPipeline() {
614
637
  const subclassPipeline = (this.#contextPipelineOptions.contextPipelineBuilder?.() ??
@@ -1005,6 +1028,11 @@ export class BasicCrawler {
1005
1028
  * the batches via `waitBetweenBatchesMillis`. If you want to wait for all batches to be added to the queue, you can use
1006
1029
  * the `waitForAllRequestsToBeAdded` promise you get in the response object.
1007
1030
  *
1031
+ * Optionally, the requests can be filtered using `include`/`exclude` glob or regexp patterns and an
1032
+ * enqueue `strategy` (both AND-ed together, same as {@link CrawlingContext.enqueueLinks|`enqueueLinks`}),
1033
+ * relative to `baseUrl`. Unlike `enqueueLinks`, there is no implicit "current page" to anchor the strategy
1034
+ * to, so `strategy` defaults to {@link EnqueueStrategy.All|`all`} here.
1035
+ *
1008
1036
  * This is an alias for calling `addRequestsBatched()` on the implicit `RequestQueue` for this crawler instance.
1009
1037
  *
1010
1038
  * @param requests The requests to add
@@ -1012,55 +1040,95 @@ export class BasicCrawler {
1012
1040
  */
1013
1041
  async addRequests(requests, options = {}) {
1014
1042
  await this.getRequestManager();
1015
- const requestLimit = await this.calculateEnqueuedRequestLimit();
1016
- const skippedBecauseOfRobots = new Set();
1017
- const skippedBecauseOfMaxCrawlDepth = new Set();
1018
- const isAllowedBasedOnRobotsTxtFile = this.isAllowedBasedOnRobotsTxtFile.bind(this);
1019
- const maxCrawlDepth = this.maxCrawlDepth;
1020
- const validateRequestUserData = this.validateRequestUserData.bind(this);
1021
1043
  if (!isIterable(requests) && !isAsyncIterable(requests)) {
1022
1044
  throw new Error(`Expected an iterable or async iterable, got ${getObjectType(requests)}`);
1023
1045
  }
1046
+ parseArgument(options, addRequestsOptionsSchema, 'EnqueueUrlsOptions');
1047
+ // `label`/`userData` apply to every request this call produces, so a single upfront validation
1048
+ // against the label's schema covers them all and fails the whole call fast, rather than failing
1049
+ // lazily once the generator below is drained. Skipped when neither is set - each item still gets
1050
+ // its own per-item validation below, and validating an absent label/userData here would spuriously
1051
+ // check them against a registered default-route schema.
1052
+ if (options.label !== undefined || options.userData !== undefined) {
1053
+ await this.validateRequestUserData({ label: options.label, userData: options.userData });
1054
+ }
1055
+ const requestLimit = await this.calculateEnqueuedRequestLimit(options.limit);
1056
+ const strategy = options.strategy ?? EnqueueStrategy.All;
1057
+ const urlExcludePatternObjects = options.exclude?.length
1058
+ ? constructUrlPatternObjects(options.exclude)
1059
+ : [];
1060
+ const urlPatternObjects = options.include?.length
1061
+ ? constructUrlPatternObjects(options.include)
1062
+ : [];
1063
+ // The strategy always applies, even when `include` patterns are provided - the two are AND-ed together
1064
+ // (a URL must match an `include` pattern *and* satisfy the strategy). This mirrors crawlee-python.
1065
+ const enqueueStrategyPatterns = options.baseUrl
1066
+ ? buildEnqueueStrategyPatterns(options.baseUrl, strategy)
1067
+ : [];
1068
+ const isAllowedBasedOnRobotsTxtFile = this.isAllowedBasedOnRobotsTxtFile.bind(this);
1069
+ const maxCrawlDepth = this.maxCrawlDepth;
1070
+ const validateRequestUserData = this.validateRequestUserData.bind(this);
1071
+ const allSkipped = [];
1024
1072
  async function* filteredRequests() {
1025
1073
  for await (const request of requests) {
1026
- const url = typeof request === 'string' ? request : request.url;
1027
- if (maxCrawlDepth !== undefined && request.crawlDepth > maxCrawlDepth) {
1028
- skippedBecauseOfMaxCrawlDepth.add(url);
1074
+ const [requestOptions] = createRequestOptions([typeof request === 'string' ? request : request], { ...options, strategy });
1075
+ if (!requestOptions) {
1076
+ continue; // invalid URL, silently dropped (matches `createRequestOptions`'s own filtering)
1077
+ }
1078
+ if (maxCrawlDepth !== undefined && requestOptions.crawlDepth > maxCrawlDepth) {
1079
+ allSkipped.push({ url: requestOptions.url, reason: 'depth' });
1029
1080
  continue;
1030
1081
  }
1031
- if (await isAllowedBasedOnRobotsTxtFile(url)) {
1032
- await validateRequestUserData(request);
1033
- yield request;
1082
+ if (!(await isAllowedBasedOnRobotsTxtFile(requestOptions.url))) {
1083
+ allSkipped.push({ url: requestOptions.url, reason: 'robotsTxt' });
1084
+ continue;
1034
1085
  }
1035
- else {
1036
- skippedBecauseOfRobots.add(url);
1086
+ const onSkippedFilterUrl = (url) => allSkipped.push({ url, reason: 'filters' });
1087
+ // Filter by user patterns first (with exclude)...
1088
+ let filtered = filterRequestOptionsByPatterns([requestOptions], urlPatternObjects.length > 0 ? urlPatternObjects : undefined, urlExcludePatternObjects, strategy, onSkippedFilterUrl);
1089
+ // ...then filter by the enqueue strategy (making this an AND check)
1090
+ filtered = filterRequestOptionsByPatterns(filtered, enqueueStrategyPatterns.length > 0 ? enqueueStrategyPatterns : undefined, [], strategy, onSkippedFilterUrl);
1091
+ if (filtered.length === 0) {
1092
+ continue;
1037
1093
  }
1094
+ let [finalOptions] = filtered;
1095
+ if (options.transformRequestFunction) {
1096
+ const transformed = applyRequestTransform([finalOptions], options.transformRequestFunction, (r) => allSkipped.push({ url: r.url, reason: r.skippedReason ?? 'transform' }));
1097
+ if (transformed.length === 0) {
1098
+ continue;
1099
+ }
1100
+ [finalOptions] = transformed;
1101
+ }
1102
+ await validateRequestUserData(finalOptions);
1103
+ yield new Request(finalOptions);
1038
1104
  }
1039
1105
  }
1040
1106
  const result = await this.requestManager.addRequestsBatched(filteredRequests(), {
1041
- ...options,
1107
+ forefront: options.forefront,
1108
+ waitForAllRequestsToBeAdded: options.waitForAllRequestsToBeAdded,
1109
+ batchSize: options.batchSize,
1110
+ waitBetweenBatchesMillis: options.waitBetweenBatchesMillis,
1042
1111
  maxNewRequests: requestLimit,
1043
1112
  });
1044
- // Report requests skipped due to the maxNewRequests budget (i.e. maxRequestsPerCrawl limit)
1045
- const skippedBecauseOfLimit = result.requestsOverLimit ?? [];
1046
- if (skippedBecauseOfRobots.size > 0) {
1047
- this.log.warning(`Some requests were skipped because they were disallowed based on the robots.txt file`, {
1048
- skipped: [...skippedBecauseOfRobots],
1049
- });
1050
- }
1051
- if (skippedBecauseOfRobots.size > 0 ||
1052
- skippedBecauseOfLimit.length > 0 ||
1053
- skippedBecauseOfMaxCrawlDepth.size > 0) {
1054
- await Promise.all([...skippedBecauseOfRobots]
1055
- .map((url) => {
1056
- return this.handleSkippedRequest({ url, reason: 'robotsTxt' });
1057
- })
1058
- .concat(skippedBecauseOfLimit.map((request) => {
1059
- const url = typeof request === 'string' ? request : request.url;
1060
- return this.handleSkippedRequest({ url, reason: 'limit' });
1061
- }), [...skippedBecauseOfMaxCrawlDepth].map((url) => {
1062
- return this.handleSkippedRequest({ url, reason: 'depth' });
1063
- })));
1113
+ // Report requests skipped due to the maxNewRequests budget (i.e. maxRequestsPerCrawl limit, or an
1114
+ // explicit `limit` option)
1115
+ for (const request of result.requestsOverLimit ?? []) {
1116
+ allSkipped.push({ url: typeof request === 'string' ? request : request.url, reason: 'limit' });
1117
+ }
1118
+ if (allSkipped.length > 0) {
1119
+ const skippedRobotsUrls = allSkipped.filter((s) => s.reason === 'robotsTxt').map((s) => s.url);
1120
+ if (skippedRobotsUrls.length > 0) {
1121
+ this.log.warning(`Some requests were skipped because they were disallowed based on the robots.txt file`, { skipped: skippedRobotsUrls });
1122
+ }
1123
+ // Only log the limit message when an explicit `limit` was passed (not the internal
1124
+ // `maxRequestsPerCrawl`-derived one), and only once per call.
1125
+ if (options.limit !== undefined && allSkipped.some((s) => s.reason === 'limit')) {
1126
+ this.log.info(`Skipping requests in this call due to the enqueueLinks limit of ${options.limit}.`);
1127
+ }
1128
+ await Promise.all(allSkipped.map(async ({ url, reason }) => {
1129
+ await this.handleSkippedRequest({ url, reason });
1130
+ await options.onSkippedRequest?.({ url, reason });
1131
+ }));
1064
1132
  }
1065
1133
  return result;
1066
1134
  }
@@ -1459,52 +1527,6 @@ export class BasicCrawler {
1459
1527
  }
1460
1528
  }
1461
1529
  }
1462
- /**
1463
- * Wrapper around the crawling context's `enqueueLinks` method:
1464
- * - Injects `crawlDepth` to each request being added based on the crawling context request.
1465
- * - Provides defaults for the `enqueueLinks` options based on the crawler configuration.
1466
- * - These options can be overridden by the user.
1467
- * @internal
1468
- */
1469
- async enqueueLinksWithCrawlDepth(options, request, requestManager) {
1470
- const transformRequestFunctionWrapper = (requestOptions) => {
1471
- requestOptions.crawlDepth = request.crawlDepth + 1;
1472
- if (this.maxCrawlDepth !== undefined && requestOptions.crawlDepth > this.maxCrawlDepth) {
1473
- // Setting `skippedReason` before returning `false` ensures that `reportSkippedRequests`
1474
- // reports `'depth'` as the reason (via `request.skippedReason ?? reason` fallback),
1475
- // rather than the generic `'transform'` reason.
1476
- requestOptions.skippedReason = 'depth';
1477
- return false;
1478
- }
1479
- // After injecting the crawlDepth, we call the user-provided transform function, if there is one.
1480
- return options.transformRequestFunction?.(requestOptions) ?? requestOptions;
1481
- };
1482
- // Create a request-scoped callback that logs enqueueLimit once per request handler call
1483
- // Only log if an explicit limit was passed to enqueueLinks (not the internal maxRequestsPerCrawl-derived limit)
1484
- let loggedEnqueueLimitForThisRequest = false;
1485
- const onSkippedRequest = async (skippedOptions) => {
1486
- if (skippedOptions.reason === 'enqueueLimit') {
1487
- if (!loggedEnqueueLimitForThisRequest && options.limit !== undefined) {
1488
- this.log.info(`Skipping URLs in the handler for ${request.url} due to the enqueueLinks limit of ${options.limit}.`);
1489
- loggedEnqueueLimitForThisRequest = true;
1490
- }
1491
- }
1492
- await this.handleSkippedRequest(skippedOptions);
1493
- };
1494
- // `enqueueLinks` applies `options.label`/`options.userData` to every newly enqueued request, so a single
1495
- // validation against the label's schema covers them all (a no-op unless the router declares a schema).
1496
- await this.validateRequestUserData({ label: options.label, userData: options.userData });
1497
- return await enqueueLinks({
1498
- requestManager,
1499
- robotsTxtFile: await this.getRobotsTxtFileForUrl(request.url),
1500
- respectRobotsTxtFile: this.#respectRobotsTxtFile,
1501
- onSkippedRequest,
1502
- limit: await this.calculateEnqueuedRequestLimit(options.limit),
1503
- // Allow user options to override defaults set above ⤴
1504
- ...options,
1505
- transformRequestFunction: transformRequestFunctionWrapper,
1506
- });
1507
- }
1508
1530
  /**
1509
1531
  * Generator function that yields requests injected with the given crawl depth.
1510
1532
  * @internal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/basic",
3
- "version": "4.0.0-beta.122",
3
+ "version": "4.0.0-beta.123",
4
4
  "description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -42,10 +42,10 @@
42
42
  "@apify/datastructures": "^2.0.0",
43
43
  "@apify/timeout": "^0.4.4",
44
44
  "@apify/utilities": "^2.15.5",
45
- "@crawlee/core": "4.0.0-beta.122",
46
- "@crawlee/http-client": "4.0.0-beta.122",
47
- "@crawlee/types": "4.0.0-beta.122",
48
- "@crawlee/utils": "4.0.0-beta.122",
45
+ "@crawlee/core": "4.0.0-beta.123",
46
+ "@crawlee/http-client": "4.0.0-beta.123",
47
+ "@crawlee/types": "4.0.0-beta.123",
48
+ "@crawlee/utils": "4.0.0-beta.123",
49
49
  "csv-stringify": "^6.5.2",
50
50
  "tldts": "^7.0.6",
51
51
  "tslib": "^2.8.1",
@@ -53,7 +53,7 @@
53
53
  "zod": "^4.4.3"
54
54
  },
55
55
  "optionalDependencies": {
56
- "@crawlee/impit-client": "^4.0.0-beta.122"
56
+ "@crawlee/impit-client": "^4.0.0-beta.123"
57
57
  },
58
58
  "lerna": {
59
59
  "command": {
@@ -62,5 +62,5 @@
62
62
  }
63
63
  }
64
64
  },
65
- "gitHead": "2c3e87fefdb9e1fca4c144f03167d8524cfdc2e5"
65
+ "gitHead": "f77648095c6a3f5ed8815c7620ea765db430ae44"
66
66
  }