@nitpicker/crawler 0.13.0 → 0.14.0

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.
Files changed (71) hide show
  1. package/lib/archive/archive-accessor.d.ts +56 -0
  2. package/lib/archive/archive-accessor.js +72 -0
  3. package/lib/archive/archive.d.ts +11 -0
  4. package/lib/archive/archive.js +13 -0
  5. package/lib/archive/create-adjunct-tables.d.ts +7 -0
  6. package/lib/archive/create-adjunct-tables.js +150 -0
  7. package/lib/archive/create-entity-tables.d.ts +14 -0
  8. package/lib/archive/create-entity-tables.js +32 -1
  9. package/lib/archive/database.d.ts +55 -1
  10. package/lib/archive/database.js +81 -0
  11. package/lib/archive/db-ops/analysis/replace-page-templates.d.ts +20 -0
  12. package/lib/archive/db-ops/analysis/replace-page-templates.js +52 -0
  13. package/lib/archive/db-ops/config/info-column-allowlist.js +1 -0
  14. package/lib/archive/db-ops/lifecycle/init.d.ts +16 -10
  15. package/lib/archive/db-ops/lifecycle/init.js +20 -10
  16. package/lib/archive/db-ops/meta/get-audios-of-page.d.ts +11 -0
  17. package/lib/archive/db-ops/meta/get-audios-of-page.js +15 -0
  18. package/lib/archive/db-ops/meta/get-buttons-of-page.d.ts +11 -0
  19. package/lib/archive/db-ops/meta/get-buttons-of-page.js +15 -0
  20. package/lib/archive/db-ops/meta/get-canvases-of-page.d.ts +11 -0
  21. package/lib/archive/db-ops/meta/get-canvases-of-page.js +15 -0
  22. package/lib/archive/db-ops/meta/get-headings-of-page.d.ts +11 -0
  23. package/lib/archive/db-ops/meta/get-headings-of-page.js +15 -0
  24. package/lib/archive/db-ops/meta/get-iframes-of-page.d.ts +11 -0
  25. package/lib/archive/db-ops/meta/get-iframes-of-page.js +15 -0
  26. package/lib/archive/db-ops/meta/get-main-content-images-of-page.d.ts +11 -0
  27. package/lib/archive/db-ops/meta/get-main-content-images-of-page.js +15 -0
  28. package/lib/archive/db-ops/meta/get-main-content-tables-of-page.d.ts +11 -0
  29. package/lib/archive/db-ops/meta/get-main-content-tables-of-page.js +15 -0
  30. package/lib/archive/db-ops/meta/get-videos-of-page.d.ts +11 -0
  31. package/lib/archive/db-ops/meta/get-videos-of-page.js +15 -0
  32. package/lib/archive/db-ops/pages/read/build-page-query.js +1 -1
  33. package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +19 -10
  34. package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +5 -3
  35. package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +13 -3
  36. package/lib/archive/db-ops/pages/write/insert-audios.d.ts +14 -0
  37. package/lib/archive/db-ops/pages/write/insert-audios.js +25 -0
  38. package/lib/archive/db-ops/pages/write/insert-buttons.d.ts +14 -0
  39. package/lib/archive/db-ops/pages/write/insert-buttons.js +29 -0
  40. package/lib/archive/db-ops/pages/write/insert-canvases.d.ts +14 -0
  41. package/lib/archive/db-ops/pages/write/insert-canvases.js +26 -0
  42. package/lib/archive/db-ops/pages/write/insert-headings.d.ts +15 -0
  43. package/lib/archive/db-ops/pages/write/insert-headings.js +27 -0
  44. package/lib/archive/db-ops/pages/write/insert-iframes.d.ts +14 -0
  45. package/lib/archive/db-ops/pages/write/insert-iframes.js +28 -0
  46. package/lib/archive/db-ops/pages/write/insert-main-content-images.d.ts +20 -0
  47. package/lib/archive/db-ops/pages/write/insert-main-content-images.js +32 -0
  48. package/lib/archive/db-ops/pages/write/insert-main-content-tables.d.ts +14 -0
  49. package/lib/archive/db-ops/pages/write/insert-main-content-tables.js +29 -0
  50. package/lib/archive/db-ops/pages/write/insert-page.js +15 -2
  51. package/lib/archive/db-ops/pages/write/insert-videos.d.ts +14 -0
  52. package/lib/archive/db-ops/pages/write/insert-videos.js +28 -0
  53. package/lib/archive/db-ops/pages/write/update-page.js +21 -0
  54. package/lib/archive/init-schema.js +2 -1
  55. package/lib/archive/meta/compute-main-contents-denormalized.d.ts +22 -0
  56. package/lib/archive/meta/compute-main-contents-denormalized.js +63 -0
  57. package/lib/archive/meta/types.d.ts +263 -0
  58. package/lib/archive/migrate-info-main-content-selector.d.ts +11 -0
  59. package/lib/archive/migrate-info-main-content-selector.js +24 -0
  60. package/lib/archive/migrate-main-contents-columns.d.ts +21 -0
  61. package/lib/archive/migrate-main-contents-columns.js +50 -0
  62. package/lib/archive/page.d.ts +144 -2
  63. package/lib/archive/page.js +195 -0
  64. package/lib/archive/types.d.ts +39 -0
  65. package/lib/crawler/crawler.js +4 -2
  66. package/lib/crawler/fetch-destination.js +2 -0
  67. package/lib/crawler/link-to-page-data.js +2 -0
  68. package/lib/crawler/resource-to-page-data.js +2 -0
  69. package/lib/crawler/types.d.ts +5 -0
  70. package/lib/crawler-orchestrator.js +2 -0
  71. package/package.json +3 -3
@@ -60,6 +60,23 @@ const FLAT_META_COLUMNS = [
60
60
  'tag_count',
61
61
  'jsonld_count',
62
62
  'tags_providers_csv',
63
+ 'main_content_node_name',
64
+ 'main_content_id',
65
+ 'main_content_role',
66
+ 'main_content_selector',
67
+ 'main_content_class_list',
68
+ 'main_content_word_count',
69
+ 'main_content_body_word_count',
70
+ 'main_content_heading_count',
71
+ 'main_content_image_count',
72
+ 'main_content_table_count',
73
+ 'main_content_button_count',
74
+ 'main_content_iframe_count',
75
+ 'main_content_video_count',
76
+ 'main_content_audio_count',
77
+ 'main_content_canvas_count',
78
+ 'scroll_height_desktop',
79
+ 'scroll_height_mobile',
63
80
  ];
64
81
  /**
65
82
  * Represents a crawled page stored in the archive.
@@ -171,6 +188,104 @@ export default class Page {
171
188
  get lastCrawledAt() {
172
189
  return this.#raw.lastCrawledAt;
173
190
  }
191
+ /**
192
+ * Detected main-content element's `nodeName` (e.g. `'MAIN'`), or null when
193
+ * no main region was found or the page was not fully rendered.
194
+ */
195
+ get mainContentNodeName() {
196
+ return this.#raw.main_content_node_name;
197
+ }
198
+ /**
199
+ * Detected main-content element's `id`, or null.
200
+ */
201
+ get mainContentId() {
202
+ return this.#raw.main_content_id;
203
+ }
204
+ /**
205
+ * Detected main-content element's `role` attribute, or null.
206
+ */
207
+ get mainContentRole() {
208
+ return this.#raw.main_content_role;
209
+ }
210
+ /**
211
+ * Diagnostic tag+id+class selector for the detected main-content element, or null.
212
+ */
213
+ get mainContentSelector() {
214
+ return this.#raw.main_content_selector;
215
+ }
216
+ /**
217
+ * Detected main-content element's CSS classes, or null when no main region
218
+ * was found. Parsed from the JSON-encoded `main_content_class_list` column.
219
+ */
220
+ get mainContentClassList() {
221
+ if (this.#raw.main_content_class_list === null) {
222
+ return null;
223
+ }
224
+ return JSON.parse(this.#raw.main_content_class_list);
225
+ }
226
+ /**
227
+ * Character count of the main region's text content (denormalised
228
+ * aggregate written at scrape time), or null.
229
+ */
230
+ get mainContentWordCount() {
231
+ return this.#raw.main_content_word_count;
232
+ }
233
+ /**
234
+ * Character count of `document.body`'s text content (denormalised
235
+ * aggregate written at scrape time), or null.
236
+ */
237
+ get mainContentBodyWordCount() {
238
+ return this.#raw.main_content_body_word_count;
239
+ }
240
+ /**
241
+ * Number of headings within the main region (denormalised aggregate), or null.
242
+ */
243
+ get mainContentHeadingCount() {
244
+ return this.#raw.main_content_heading_count;
245
+ }
246
+ /**
247
+ * Number of images within the main region (denormalised aggregate), or null.
248
+ */
249
+ get mainContentImageCount() {
250
+ return this.#raw.main_content_image_count;
251
+ }
252
+ /**
253
+ * Number of tables within the main region (denormalised aggregate), or null.
254
+ */
255
+ get mainContentTableCount() {
256
+ return this.#raw.main_content_table_count;
257
+ }
258
+ /**
259
+ * Number of button-like elements within the main region (denormalised
260
+ * aggregate), or null.
261
+ */
262
+ get mainContentButtonCount() {
263
+ return this.#raw.main_content_button_count;
264
+ }
265
+ /**
266
+ * Number of iframes within the main region (denormalised aggregate), or null.
267
+ */
268
+ get mainContentIframeCount() {
269
+ return this.#raw.main_content_iframe_count;
270
+ }
271
+ /**
272
+ * Number of videos within the main region (denormalised aggregate), or null.
273
+ */
274
+ get mainContentVideoCount() {
275
+ return this.#raw.main_content_video_count;
276
+ }
277
+ /**
278
+ * Number of audios within the main region (denormalised aggregate), or null.
279
+ */
280
+ get mainContentAudioCount() {
281
+ return this.#raw.main_content_audio_count;
282
+ }
283
+ /**
284
+ * Number of canvases within the main region (denormalised aggregate), or null.
285
+ */
286
+ get mainContentCanvasCount() {
287
+ return this.#raw.main_content_canvas_count;
288
+ }
174
289
  /**
175
290
  * Iterable view over every flat meta column (~47 fields). Returns a frozen
176
291
  * record so consumers can pick fields by name without re-enumerating
@@ -280,6 +395,20 @@ export default class Page {
280
395
  get robots_raw() {
281
396
  return this.#raw.robots_raw;
282
397
  }
398
+ /**
399
+ * `document.body.scrollHeight` at the desktop-compact preset (denormalised
400
+ * aggregate written at scrape time), or null.
401
+ */
402
+ get scrollHeightDesktop() {
403
+ return this.#raw.scroll_height_desktop;
404
+ }
405
+ /**
406
+ * `document.body.scrollHeight` at the mobile-small preset (denormalised
407
+ * aggregate written at scrape time), or null.
408
+ */
409
+ get scrollHeightMobile() {
410
+ return this.#raw.scroll_height_mobile;
411
+ }
283
412
  /**
284
413
  * The reason this page was skipped during crawling, or null if it was not skipped.
285
414
  */
@@ -376,6 +505,39 @@ export default class Page {
376
505
  }
377
506
  return this.#archive.getAnchorsOnPage(this.#raw.id);
378
507
  }
508
+ /**
509
+ * Retrieves the audios within this page's detected main content region
510
+ * from `page_main_content_audios`. Lazy — runs a single SELECT per call.
511
+ * @returns Ordered audio rows.
512
+ */
513
+ async getAudios() {
514
+ return this.#archive.getAudiosOfPage(this.#raw.id);
515
+ }
516
+ /**
517
+ * Retrieves the button-like elements within this page's detected main
518
+ * content region from `page_main_content_buttons`. Lazy — runs a single
519
+ * SELECT per call.
520
+ * @returns Ordered button rows.
521
+ */
522
+ async getButtons() {
523
+ return this.#archive.getButtonsOfPage(this.#raw.id);
524
+ }
525
+ /**
526
+ * Retrieves the canvases within this page's detected main content region
527
+ * from `page_main_content_canvases`. Lazy — runs a single SELECT per call.
528
+ * @returns Ordered canvas rows.
529
+ */
530
+ async getCanvases() {
531
+ return this.#archive.getCanvasesOfPage(this.#raw.id);
532
+ }
533
+ /**
534
+ * Retrieves the headings within this page's detected main content region
535
+ * from `page_main_content_headings`. Lazy — runs a single SELECT per call.
536
+ * @returns Ordered heading rows.
537
+ */
538
+ async getHeadings() {
539
+ return this.#archive.getHeadingsOfPage(this.#raw.id);
540
+ }
379
541
  /**
380
542
  * Thin wrapper that forwards this page's id to the accessor's
381
543
  * BLOB-read path. Lets callers hold a `Page` reference and ask for its
@@ -391,6 +553,14 @@ export default class Page {
391
553
  async getHtml() {
392
554
  return this.#archive.getHtmlOfPage(this.#raw.id);
393
555
  }
556
+ /**
557
+ * Retrieves the iframes within this page's detected main content region
558
+ * from `page_main_content_iframes`. Lazy — runs a single SELECT per call.
559
+ * @returns Ordered iframe rows.
560
+ */
561
+ async getIframes() {
562
+ return this.#archive.getIframesOfPage(this.#raw.id);
563
+ }
394
564
  /**
395
565
  * Retrieves the JSON-LD entries for this page from `page_jsonld`.
396
566
  * Lazy — runs a single SELECT per call. Returns entries in insertion
@@ -400,6 +570,23 @@ export default class Page {
400
570
  async getJsonLd() {
401
571
  return this.#archive.getJsonLdOfPage(this.#raw.id);
402
572
  }
573
+ /**
574
+ * Retrieves the images within this page's detected main content region
575
+ * from `page_main_content_images`. Lazy — runs a single SELECT per call.
576
+ * Distinct from the whole-page image scan (`image_items`).
577
+ * @returns Ordered image rows.
578
+ */
579
+ async getMainContentImages() {
580
+ return this.#archive.getMainContentImagesOfPage(this.#raw.id);
581
+ }
582
+ /**
583
+ * Retrieves the tables within this page's detected main content region
584
+ * from `page_main_content_tables`. Lazy — runs a single SELECT per call.
585
+ * @returns Ordered table rows.
586
+ */
587
+ async getMainContentTables() {
588
+ return this.#archive.getMainContentTablesOfPage(this.#raw.id);
589
+ }
403
590
  /**
404
591
  * Retrieves the referrers (incoming links) pointing to this page.
405
592
  * Uses pre-loaded data if available, otherwise queries the database.
@@ -447,6 +634,14 @@ export default class Page {
447
634
  async getTags() {
448
635
  return this.#archive.getTagsOfPage(this.#raw.id);
449
636
  }
637
+ /**
638
+ * Retrieves the videos within this page's detected main content region
639
+ * from `page_main_content_videos`. Lazy — runs a single SELECT per call.
640
+ * @returns Ordered video rows.
641
+ */
642
+ async getVideos() {
643
+ return this.#archive.getVideosOfPage(this.#raw.id);
644
+ }
450
645
  /**
451
646
  * Checks whether this page is an internal HTML page (not external and has `text/html` content type).
452
647
  * @returns `true` if this is an internal HTML page, `false` otherwise.
@@ -67,6 +67,11 @@ export interface Config extends Required<Pick<ParseURLOptions, 'disableQueries'>
67
67
  userAgent: string;
68
68
  /** Whether robots.txt restrictions were ignored during crawling. */
69
69
  ignoreRobots: boolean;
70
+ /**
71
+ * CSS selector overriding beholder's automatic main-content-region
72
+ * detection, or `null`/omitted to use the automatic heuristic.
73
+ */
74
+ mainContentSelector?: string | null;
70
75
  }
71
76
  /**
72
77
  * Provenance of a page or resource row — which crawler channel originally
@@ -292,6 +297,40 @@ export interface DB_Page {
292
297
  jsonld_count: number | null;
293
298
  /** Sorted unique provider names, comma-separated (empty string when none). */
294
299
  tags_providers_csv: string | null;
300
+ /** Detected main-content element's `nodeName` (e.g. `'MAIN'`), or null. */
301
+ main_content_node_name: string | null;
302
+ /** Detected main-content element's `id`, or null. */
303
+ main_content_id: string | null;
304
+ /** Detected main-content element's `role` attribute, or null. */
305
+ main_content_role: string | null;
306
+ /** Diagnostic tag+id+class selector for the detected element, or null. */
307
+ main_content_selector: string | null;
308
+ /** JSON-encoded array of the detected element's CSS classes, or null. */
309
+ main_content_class_list: string | null;
310
+ /** Character count of the main region's text content, or null. */
311
+ main_content_word_count: number | null;
312
+ /** Character count of `document.body`'s text content, or null. */
313
+ main_content_body_word_count: number | null;
314
+ /** Number of headings within the main region, or null. */
315
+ main_content_heading_count: number | null;
316
+ /** Number of images within the main region, or null. */
317
+ main_content_image_count: number | null;
318
+ /** Number of tables within the main region, or null. */
319
+ main_content_table_count: number | null;
320
+ /** Number of button-like elements within the main region, or null. */
321
+ main_content_button_count: number | null;
322
+ /** Number of iframes within the main region, or null. */
323
+ main_content_iframe_count: number | null;
324
+ /** Number of videos within the main region, or null. */
325
+ main_content_video_count: number | null;
326
+ /** Number of audios within the main region, or null. */
327
+ main_content_audio_count: number | null;
328
+ /** Number of canvases within the main region, or null. */
329
+ main_content_canvas_count: number | null;
330
+ /** `document.body.scrollHeight` at the desktop-compact preset, or null. */
331
+ scroll_height_desktop: number | null;
332
+ /** `document.body.scrollHeight` at the mobile-small preset, or null. */
333
+ scroll_height_mobile: number | null;
295
334
  /** JSON-serialised nested Meta sub-objects not flattened above. */
296
335
  meta_extras: string | null;
297
336
  /** JSON-serialized network logs captured during scraping, or null if not collected. */
@@ -149,6 +149,7 @@ class Crawler extends EventEmitter {
149
149
  verbose: options?.verbose ?? false,
150
150
  userAgent: options?.userAgent || `Nitpicker/${pkg.version}`,
151
151
  ignoreRobots: options?.ignoreRobots ?? false,
152
+ mainContentSelector: options?.mainContentSelector ?? null,
152
153
  lookupResource: options?.lookupResource ?? null,
153
154
  lookupPageSource: options?.lookupPageSource ?? null,
154
155
  inventoryMode: options?.inventoryMode ?? null,
@@ -1355,8 +1356,8 @@ class Crawler extends EventEmitter {
1355
1356
  // sub-resource requests issued from the same page —
1356
1357
  // including cross-origin requests to a different hostname
1357
1358
  // sharing the same IP / port (e.g. an embedded
1358
- // `<img src="http://127.0.0.1:8010/…">` loaded from a
1359
- // `localhost:8010` page) — get the cached `Authorization`
1359
+ // `<img src="http://127.0.0.1:PORT/…">` loaded from a
1360
+ // `localhost:PORT` page) — get the cached `Authorization`
1360
1361
  // header re-attached by the network stack. The
1361
1362
  // `Fetch.authRequired` event never fires for these
1362
1363
  // pre-emptive attachments, so neither `page.authenticate`
@@ -1399,6 +1400,7 @@ class Crawler extends EventEmitter {
1399
1400
  metadataOnly,
1400
1401
  retries: this.#options.retry,
1401
1402
  headCheckResult,
1403
+ mainContentSelector: this.#options.mainContentSelector,
1402
1404
  });
1403
1405
  // Image dom-path capture runs here — after the scrape completed but
1404
1406
  // while `page` is still alive — because beholder's image metadata
@@ -196,6 +196,8 @@ async function _fetchHead(url, isExternal, method, titleBytesLimit, userAgent, t
196
196
  imageList: [],
197
197
  anchorList: [],
198
198
  html: '',
199
+ mainContents: null,
200
+ scrollHeight: null,
199
201
  isSkipped: false,
200
202
  };
201
203
  };
@@ -48,6 +48,8 @@ export function linkToPageData(link) {
48
48
  anchorList: [],
49
49
  imageList: [],
50
50
  html: '',
51
+ mainContents: null,
52
+ scrollHeight: null,
51
53
  isSkipped: false,
52
54
  };
53
55
  }
@@ -54,6 +54,8 @@ export function resourceToPageData(params) {
54
54
  anchorList: [],
55
55
  imageList: [],
56
56
  html: '',
57
+ mainContents: null,
58
+ scrollHeight: null,
57
59
  isSkipped: false,
58
60
  };
59
61
  }
@@ -129,6 +129,11 @@ export interface CrawlerOptions extends Required<Pick<ParseURLOptions, 'disableQ
129
129
  userAgent: string;
130
130
  /** Whether to ignore robots.txt restrictions. */
131
131
  ignoreRobots: boolean;
132
+ /**
133
+ * CSS selector overriding beholder's automatic main-content-region
134
+ * detection, or `null`/undefined to use the automatic heuristic.
135
+ */
136
+ mainContentSelector?: string | null;
132
137
  /**
133
138
  * Lookup for previously captured sub-resources, or `null` to disable the
134
139
  * resource-reuse optimization. See {@link ResourceLookup}.
@@ -109,6 +109,7 @@ export class CrawlerOrchestrator extends EventEmitter {
109
109
  verbose: options?.verbose ?? false,
110
110
  userAgent: options?.userAgent || defaultUserAgent,
111
111
  ignoreRobots: options?.ignoreRobots ?? false,
112
+ mainContentSelector: options?.mainContentSelector ?? null,
112
113
  // Let the crawler reuse sub-resource data captured during page
113
114
  // rendering instead of issuing a redundant HEAD pre-flight.
114
115
  lookupResource: async (urls) => {
@@ -337,6 +338,7 @@ export class CrawlerOrchestrator extends EventEmitter {
337
338
  disableQueries,
338
339
  userAgent: options?.userAgent || defaultUserAgent,
339
340
  ignoreRobots: options?.ignoreRobots ?? false,
341
+ mainContentSelector: options?.mainContentSelector ?? null,
340
342
  });
341
343
  const orchestrator = new CrawlerOrchestrator(archive, {
342
344
  ...options,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitpicker/crawler",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Web crawler engine with headless browser rendering and archive storage",
5
5
  "author": "D-ZERO",
6
6
  "license": "Apache-2.0",
@@ -27,7 +27,7 @@
27
27
  "clean": "tsc --build --clean"
28
28
  },
29
29
  "dependencies": {
30
- "@d-zero/beholder": "3.1.5",
30
+ "@d-zero/beholder": "4.0.0",
31
31
  "@d-zero/dealer": "1.9.4",
32
32
  "@d-zero/fs": "0.2.6",
33
33
  "@d-zero/shared": "0.22.2",
@@ -48,5 +48,5 @@
48
48
  "@types/tar": "7.0.87",
49
49
  "@types/unzipper": "0.10.11"
50
50
  },
51
- "gitHead": "2cd462ea6b183f4462b7caba27b4db04aaba7c70"
51
+ "gitHead": "98222c2bbce55ab6d253787a251f040b51c7a9a0"
52
52
  }