app-manager-edge-worker 1.0.4 → 1.0.6
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.
- package/package.json +45 -45
- package/src/banner.js +19 -5
- package/src/handlers/previewHandler.js +484 -346
- package/src/handlers/scrapeHandler.js +801 -433
- package/src/handlers/transformUtils.js +340 -0
|
@@ -1,433 +1,801 @@
|
|
|
1
|
-
import axios from "axios";
|
|
2
|
-
import * as cheerio from "cheerio";
|
|
3
|
-
import localBrowserManager from "../browser.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
strategy.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (attr === "
|
|
105
|
-
return el.first().
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
css =
|
|
111
|
-
css = css.replace(
|
|
112
|
-
css = css.replace(/\/([a-zA-Z0-9_-]+)/g, " > $1");
|
|
113
|
-
css = css.replace(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (attr === "
|
|
121
|
-
return el.first().
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
} else {
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import * as cheerio from "cheerio";
|
|
3
|
+
import localBrowserManager from "../browser.js";
|
|
4
|
+
import { extractSmartCheerioText, runTransformPipeline, castValueToType } from "./transformUtils.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extracts a single field value from a Puppeteer page given a strategy.
|
|
8
|
+
*/
|
|
9
|
+
async function extractBrowserField(page, strategy) {
|
|
10
|
+
if (!strategy || !strategy.value) return null;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const type = strategy.type || "css";
|
|
14
|
+
const attr = strategy.attribute || "text";
|
|
15
|
+
|
|
16
|
+
if (type === "css") {
|
|
17
|
+
const el = await page.$(strategy.value);
|
|
18
|
+
if (!el) return null;
|
|
19
|
+
|
|
20
|
+
if (attr === "text") {
|
|
21
|
+
const text = await page.evaluate((element) => element.textContent, el);
|
|
22
|
+
return text ? text.trim() : null;
|
|
23
|
+
} else if (attr === "html") {
|
|
24
|
+
return await page.evaluate((element) => element.innerHTML, el);
|
|
25
|
+
} else {
|
|
26
|
+
const val = await page.evaluate((element, a) => element.getAttribute(a), el, attr);
|
|
27
|
+
return val !== null && val !== undefined ? String(val).trim() : null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (type === "xpath") {
|
|
32
|
+
const val = await page.evaluate(
|
|
33
|
+
(xpathStr, attrName) => {
|
|
34
|
+
try {
|
|
35
|
+
const result = document.evaluate(
|
|
36
|
+
xpathStr,
|
|
37
|
+
document,
|
|
38
|
+
null,
|
|
39
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
40
|
+
null
|
|
41
|
+
);
|
|
42
|
+
const node = result.singleNodeValue;
|
|
43
|
+
if (!node) return null;
|
|
44
|
+
if (attrName === "text") {
|
|
45
|
+
return node.textContent ? node.textContent.trim() : null;
|
|
46
|
+
}
|
|
47
|
+
return node.getAttribute ? node.getAttribute(attrName) : null;
|
|
48
|
+
} catch (e) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
strategy.value,
|
|
53
|
+
attr
|
|
54
|
+
);
|
|
55
|
+
return val;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (type === "text-relative") {
|
|
59
|
+
return await page.evaluate(
|
|
60
|
+
(anchorText, relation) => {
|
|
61
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
|
|
62
|
+
let node;
|
|
63
|
+
while ((node = walker.nextNode())) {
|
|
64
|
+
if (node.textContent.includes(anchorText)) {
|
|
65
|
+
const parent = node.parentElement;
|
|
66
|
+
if (relation === "next-sibling") {
|
|
67
|
+
const sibling = parent.nextElementSibling;
|
|
68
|
+
return sibling ? (sibling.innerText || sibling.textContent)?.trim() : null;
|
|
69
|
+
}
|
|
70
|
+
if (relation === "parent") {
|
|
71
|
+
const grandParent = parent.parentElement;
|
|
72
|
+
return grandParent ? (grandParent.innerText || grandParent.textContent)?.trim() : null;
|
|
73
|
+
}
|
|
74
|
+
return (parent.innerText || parent.textContent)?.trim();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
},
|
|
79
|
+
strategy.anchor || strategy.value,
|
|
80
|
+
strategy.relation || "self"
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Extracts a single field from a Cheerio instance given a strategy.
|
|
92
|
+
*/
|
|
93
|
+
function extractCheerioField($, strategy) {
|
|
94
|
+
if (!strategy || !strategy.value) return null;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const type = strategy.type || "css";
|
|
98
|
+
const attr = strategy.attribute || "text";
|
|
99
|
+
|
|
100
|
+
if (type === "css") {
|
|
101
|
+
const el = $(strategy.value);
|
|
102
|
+
if (!el || el.length === 0) return null;
|
|
103
|
+
|
|
104
|
+
if (attr === "text") return extractSmartCheerioText($, el.first());
|
|
105
|
+
if (attr === "html") return el.first().html();
|
|
106
|
+
return el.first().attr(attr) || null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (type === "xpath") {
|
|
110
|
+
let css = String(strategy.value).trim();
|
|
111
|
+
css = css.replace(/\/\/\*\[@id=["']([^"']+)["']\]/g, "#$1");
|
|
112
|
+
css = css.replace(/\/([a-zA-Z0-9_-]+)\[([0-9]+)\]/g, " > $1:nth-of-type($2)");
|
|
113
|
+
css = css.replace(/\/([a-zA-Z0-9_-]+)/g, " > $1");
|
|
114
|
+
css = css.replace(/^\/+/g, "").replace(/^>\s*/, "").trim();
|
|
115
|
+
|
|
116
|
+
if (css) {
|
|
117
|
+
try {
|
|
118
|
+
const el = $(css);
|
|
119
|
+
if (el && el.length > 0) {
|
|
120
|
+
if (attr === "text") return extractSmartCheerioText($, el.first());
|
|
121
|
+
if (attr === "html") return el.first().html();
|
|
122
|
+
return el.first().attr(attr) || null;
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// Ignore parse error
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (type === "text-relative") {
|
|
131
|
+
const anchor = strategy.anchor || strategy.value;
|
|
132
|
+
const match = $(`:contains("${anchor}")`).last();
|
|
133
|
+
if (match && match.length > 0) return extractSmartCheerioText($, match);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Extracts a table collection using Cheerio.
|
|
144
|
+
*/
|
|
145
|
+
function extractTableCollectionCheerio($, collectionDef) {
|
|
146
|
+
if (!collectionDef) return [];
|
|
147
|
+
const selector = collectionDef.containerSelector || collectionDef.tableSelector || "table";
|
|
148
|
+
const tableEl = $(selector).first();
|
|
149
|
+
if (!tableEl || tableEl.length === 0) return [];
|
|
150
|
+
|
|
151
|
+
const hasHeaders = collectionDef.hasHeaders !== false;
|
|
152
|
+
const headerRowIndex = typeof collectionDef.headerRowIndex === "number" ? collectionDef.headerRowIndex : 0;
|
|
153
|
+
const maxItems = collectionDef.maxItems || 100;
|
|
154
|
+
const columns = collectionDef.columns || [];
|
|
155
|
+
|
|
156
|
+
const rows = [];
|
|
157
|
+
tableEl.find("tr").each((rIdx, rowEl) => {
|
|
158
|
+
const $row = $(rowEl);
|
|
159
|
+
const cells = [];
|
|
160
|
+
$row.find("th, td").each((_, cellEl) => {
|
|
161
|
+
cells.push(extractSmartCheerioText($, $(cellEl)));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (cells.length > 0) {
|
|
165
|
+
rows.push({ rowIndex: rIdx, cells, rowEl: $row });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (rows.length === 0) return [];
|
|
170
|
+
|
|
171
|
+
let activeColumns = [...columns];
|
|
172
|
+
if (activeColumns.length === 0) {
|
|
173
|
+
if (hasHeaders && rows[headerRowIndex]) {
|
|
174
|
+
activeColumns = rows[headerRowIndex].cells.map((headerText, colIdx) => {
|
|
175
|
+
const normalizedKey =
|
|
176
|
+
headerText
|
|
177
|
+
.toLowerCase()
|
|
178
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
179
|
+
.replace(/^_+|_+$/g, "") || `column_${colIdx + 1}`;
|
|
180
|
+
return {
|
|
181
|
+
key: normalizedKey,
|
|
182
|
+
label: headerText || `Column ${colIdx + 1}`,
|
|
183
|
+
columnIndex: colIdx,
|
|
184
|
+
type: "string",
|
|
185
|
+
excluded: false,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
} else {
|
|
189
|
+
activeColumns = rows[0].cells.map((_, colIdx) => ({
|
|
190
|
+
key: `column_${colIdx + 1}`,
|
|
191
|
+
label: `Column ${colIdx + 1}`,
|
|
192
|
+
columnIndex: colIdx,
|
|
193
|
+
type: "string",
|
|
194
|
+
excluded: false,
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const startRowIdx = hasHeaders ? headerRowIndex + 1 : 0;
|
|
200
|
+
const dataRows = rows.slice(startRowIdx, startRowIdx + maxItems);
|
|
201
|
+
const results = [];
|
|
202
|
+
|
|
203
|
+
for (const { cells, rowEl } of dataRows) {
|
|
204
|
+
const record = {};
|
|
205
|
+
for (let colIdx = 0; colIdx < activeColumns.length; colIdx++) {
|
|
206
|
+
const col = activeColumns[colIdx];
|
|
207
|
+
if (col.excluded) continue;
|
|
208
|
+
let rawVal = null;
|
|
209
|
+
const colSelector = col.relativeSelector || col.selector;
|
|
210
|
+
|
|
211
|
+
if (colSelector) {
|
|
212
|
+
const subEl = rowEl.find(colSelector).first();
|
|
213
|
+
if (subEl && subEl.length > 0) {
|
|
214
|
+
const attr = col.attribute || "text";
|
|
215
|
+
rawVal = attr === "text" ? extractSmartCheerioText($, subEl) : subEl.attr(attr) || null;
|
|
216
|
+
}
|
|
217
|
+
} else if (typeof col.columnIndex === "number" && col.columnIndex < cells.length) {
|
|
218
|
+
rawVal = cells[col.columnIndex];
|
|
219
|
+
} else if (colIdx < cells.length) {
|
|
220
|
+
rawVal = cells[colIdx];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if ((rawVal === null || rawVal === undefined || rawVal === "") && col.defaultValue !== undefined) {
|
|
224
|
+
rawVal = col.defaultValue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Apply column transforms
|
|
228
|
+
if (col.transforms && col.transforms.length > 0) {
|
|
229
|
+
rawVal = runTransformPipeline(rawVal, col.transforms);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Cast to target type
|
|
233
|
+
if (col.type) {
|
|
234
|
+
rawVal = castValueToType(rawVal, col.type);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
record[col.key] = rawVal;
|
|
238
|
+
}
|
|
239
|
+
results.push(record);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return results;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Extracts repeating items/cards using Cheerio.
|
|
247
|
+
*/
|
|
248
|
+
function extractRepeatingCollectionCheerio($, collectionDef) {
|
|
249
|
+
if (!collectionDef) return [];
|
|
250
|
+
|
|
251
|
+
let items;
|
|
252
|
+
if (collectionDef.rowSelector) {
|
|
253
|
+
if (collectionDef.containerSelector) {
|
|
254
|
+
items = $(collectionDef.containerSelector).find(collectionDef.rowSelector);
|
|
255
|
+
}
|
|
256
|
+
if (!items || items.length === 0) {
|
|
257
|
+
items = $(collectionDef.rowSelector);
|
|
258
|
+
}
|
|
259
|
+
} else if (collectionDef.containerSelector) {
|
|
260
|
+
items = $(collectionDef.containerSelector);
|
|
261
|
+
} else {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!items || items.length === 0) return [];
|
|
266
|
+
|
|
267
|
+
const maxItems = collectionDef.maxItems || 100;
|
|
268
|
+
const columns = collectionDef.columns || [];
|
|
269
|
+
const results = [];
|
|
270
|
+
|
|
271
|
+
items.slice(0, maxItems).each((_, itemEl) => {
|
|
272
|
+
const $item = $(itemEl);
|
|
273
|
+
const record = {};
|
|
274
|
+
|
|
275
|
+
for (const col of columns) {
|
|
276
|
+
if (col.excluded) continue;
|
|
277
|
+
let rawVal = null;
|
|
278
|
+
const colSelector = col.relativeSelector || col.selector;
|
|
279
|
+
|
|
280
|
+
if (colSelector) {
|
|
281
|
+
const target = $item.find(colSelector).first();
|
|
282
|
+
if (target && target.length > 0) {
|
|
283
|
+
const attr = col.attribute || "text";
|
|
284
|
+
rawVal = attr === "text" ? extractSmartCheerioText($, target) : target.attr(attr) || null;
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
const attr = col.attribute || "text";
|
|
288
|
+
rawVal = attr === "text" ? extractSmartCheerioText($, $item) : $item.attr(attr) || null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if ((rawVal === null || rawVal === undefined || rawVal === "") && col.defaultValue !== undefined) {
|
|
292
|
+
rawVal = col.defaultValue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Apply column transforms
|
|
296
|
+
if (col.transforms && col.transforms.length > 0) {
|
|
297
|
+
rawVal = runTransformPipeline(rawVal, col.transforms);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Cast to target type
|
|
301
|
+
if (col.type) {
|
|
302
|
+
rawVal = castValueToType(rawVal, col.type);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
record[col.key] = rawVal;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const hasValue = Object.values(record).some((v) => v !== null && v !== undefined && v !== "");
|
|
309
|
+
if (hasValue) {
|
|
310
|
+
results.push(record);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return results;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Handles SCRAPE_CHROMIUM task via local stealth Puppeteer.
|
|
319
|
+
*/
|
|
320
|
+
export async function handleChromiumScrape(payload) {
|
|
321
|
+
const startTime = Date.now();
|
|
322
|
+
const {
|
|
323
|
+
url,
|
|
324
|
+
fieldMappings = [],
|
|
325
|
+
collections = [],
|
|
326
|
+
navigation = {},
|
|
327
|
+
waitFor = {},
|
|
328
|
+
timeout = 30000,
|
|
329
|
+
waitForSelector = null,
|
|
330
|
+
waitForTimeout = 1000,
|
|
331
|
+
actions = [],
|
|
332
|
+
extractSelectors = [],
|
|
333
|
+
headers = {},
|
|
334
|
+
cookies = [],
|
|
335
|
+
viewport = { width: 1366, height: 900 },
|
|
336
|
+
includeHtml = false,
|
|
337
|
+
} = payload;
|
|
338
|
+
|
|
339
|
+
const navTimeout = navigation.timeoutMs || timeout || 30000;
|
|
340
|
+
const navWaitUntil = navigation.waitUntil || "domcontentloaded";
|
|
341
|
+
|
|
342
|
+
const browser = await localBrowserManager.getBrowser();
|
|
343
|
+
const page = await browser.newPage();
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
await page.setViewport({
|
|
347
|
+
width: viewport.width || 1366,
|
|
348
|
+
height: viewport.height || 900,
|
|
349
|
+
deviceScaleFactor: 1,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
if (Object.keys(headers).length > 0) {
|
|
353
|
+
await page.setExtraHTTPHeaders(headers);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (Array.isArray(cookies) && cookies.length > 0) {
|
|
357
|
+
await page.setCookie(...cookies);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Navigate
|
|
361
|
+
const response = await page.goto(url, {
|
|
362
|
+
waitUntil: navWaitUntil,
|
|
363
|
+
timeout: navTimeout,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const status = response ? response.status() : 200;
|
|
367
|
+
|
|
368
|
+
// Handle wait conditions
|
|
369
|
+
if (waitFor && waitFor.type) {
|
|
370
|
+
const waitTimeout = waitFor.timeoutMs || 10000;
|
|
371
|
+
if (waitFor.type === "SELECTOR_EXISTS" && waitFor.selector) {
|
|
372
|
+
await page.waitForSelector(waitFor.selector, { timeout: waitTimeout }).catch(() => {});
|
|
373
|
+
} else if (waitFor.type === "SELECTOR_VISIBLE" && waitFor.selector) {
|
|
374
|
+
await page.waitForSelector(waitFor.selector, { visible: true, timeout: waitTimeout }).catch(() => {});
|
|
375
|
+
} else if (waitFor.type === "TEXT_PRESENT" && waitFor.text) {
|
|
376
|
+
await page
|
|
377
|
+
.waitForFunction(
|
|
378
|
+
(t) => document.body && document.body.innerText.includes(t),
|
|
379
|
+
{ timeout: waitTimeout },
|
|
380
|
+
waitFor.text
|
|
381
|
+
)
|
|
382
|
+
.catch(() => {});
|
|
383
|
+
} else if (waitFor.type === "FIXED_DELAY" && waitFor.delayMs > 0) {
|
|
384
|
+
await new Promise((r) => setTimeout(r, Math.min(waitFor.delayMs, 10000)));
|
|
385
|
+
}
|
|
386
|
+
} else if (waitForSelector) {
|
|
387
|
+
await page.waitForSelector(waitForSelector, { timeout: 10000 }).catch(() => {});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Optional brief delay for dynamic content
|
|
391
|
+
if (waitForTimeout > 0) {
|
|
392
|
+
await new Promise((r) => setTimeout(r, Math.min(waitForTimeout, 10000)));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Execute sequential interactive actions if configured
|
|
396
|
+
if (Array.isArray(actions) && actions.length > 0) {
|
|
397
|
+
for (const action of actions) {
|
|
398
|
+
try {
|
|
399
|
+
if (action.type === "click" && action.selector) {
|
|
400
|
+
await page.click(action.selector);
|
|
401
|
+
} else if (action.type === "type" && action.selector && action.text) {
|
|
402
|
+
await page.type(action.selector, action.text);
|
|
403
|
+
} else if (action.type === "scroll") {
|
|
404
|
+
await page.evaluate((y) => window.scrollBy(0, y || 500), action.distance);
|
|
405
|
+
} else if (action.type === "wait") {
|
|
406
|
+
await new Promise((r) => setTimeout(r, action.duration || 1000));
|
|
407
|
+
}
|
|
408
|
+
} catch (actionErr) {
|
|
409
|
+
console.warn(`[ScrapeHandler] Action ${action.type} failed:`, actionErr.message);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const pageTitle = (await page.title()) || "";
|
|
415
|
+
const finalUrl = page.url();
|
|
416
|
+
|
|
417
|
+
const raw = {};
|
|
418
|
+
const strategySummary = {};
|
|
419
|
+
|
|
420
|
+
// 1. Extract mapped fields (Field Schema & Strategies)
|
|
421
|
+
if (Array.isArray(fieldMappings) && fieldMappings.length > 0) {
|
|
422
|
+
for (const mapping of fieldMappings) {
|
|
423
|
+
let extractedVal = null;
|
|
424
|
+
let successfulStrategy = null;
|
|
425
|
+
|
|
426
|
+
const strategies = mapping.strategies || [];
|
|
427
|
+
for (let i = 0; i < strategies.length; i++) {
|
|
428
|
+
const strat = strategies[i];
|
|
429
|
+
const val = await extractBrowserField(page, strat);
|
|
430
|
+
if (val !== null && val !== undefined && val !== "") {
|
|
431
|
+
extractedVal = val;
|
|
432
|
+
successfulStrategy = strat.type || `strategy_${i + 1}`;
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (mapping.transforms && mapping.transforms.length > 0) {
|
|
438
|
+
extractedVal = runTransformPipeline(extractedVal, mapping.transforms);
|
|
439
|
+
}
|
|
440
|
+
if (mapping.type) {
|
|
441
|
+
extractedVal = castValueToType(extractedVal, mapping.type);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
raw[mapping.field] = extractedVal;
|
|
445
|
+
strategySummary[mapping.field] = {
|
|
446
|
+
extracted: extractedVal !== null,
|
|
447
|
+
strategyUsed: successfulStrategy,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 2. Extract legacy extractSelectors if present
|
|
453
|
+
if (Array.isArray(extractSelectors) && extractSelectors.length > 0) {
|
|
454
|
+
for (const item of extractSelectors) {
|
|
455
|
+
const { field, selector, type = "text", attribute = null } = item;
|
|
456
|
+
if (!field || !selector) continue;
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
const values = await page.evaluate(
|
|
460
|
+
(sel, t, attr) => {
|
|
461
|
+
const elements = Array.from(document.querySelectorAll(sel));
|
|
462
|
+
return elements.map((el) => {
|
|
463
|
+
if (t === "attribute" && attr) {
|
|
464
|
+
return el.getAttribute(attr) || "";
|
|
465
|
+
}
|
|
466
|
+
if (t === "html") {
|
|
467
|
+
return el.innerHTML;
|
|
468
|
+
}
|
|
469
|
+
return el.innerText ? el.innerText.trim() : el.textContent?.trim() || "";
|
|
470
|
+
});
|
|
471
|
+
},
|
|
472
|
+
selector,
|
|
473
|
+
type,
|
|
474
|
+
attribute
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
const extractedVal = values.length <= 1 ? (values[0] ?? null) : values;
|
|
478
|
+
if (raw[field] === undefined) {
|
|
479
|
+
raw[field] = extractedVal;
|
|
480
|
+
}
|
|
481
|
+
} catch {
|
|
482
|
+
if (raw[field] === undefined) {
|
|
483
|
+
raw[field] = null;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// 5. Extract structured collections (Tables & Repeating items)
|
|
490
|
+
const extractedCollections = {};
|
|
491
|
+
if (Array.isArray(collections) && collections.length > 0) {
|
|
492
|
+
for (const coll of collections) {
|
|
493
|
+
if (!coll || !coll.name) continue;
|
|
494
|
+
try {
|
|
495
|
+
if (coll.type === "TABLE") {
|
|
496
|
+
extractedCollections[coll.name] = await page.evaluate((cDef) => {
|
|
497
|
+
const sel = cDef.containerSelector || "table";
|
|
498
|
+
const table = document.querySelector(sel);
|
|
499
|
+
if (!table) return [];
|
|
500
|
+
|
|
501
|
+
const hasHeaders = cDef.hasHeaders !== false;
|
|
502
|
+
const headerRowIndex = typeof cDef.headerRowIndex === "number" ? cDef.headerRowIndex : 0;
|
|
503
|
+
const maxItems = cDef.maxItems || 100;
|
|
504
|
+
const columns = cDef.columns || [];
|
|
505
|
+
|
|
506
|
+
const rowEls = Array.from(table.querySelectorAll("tr"));
|
|
507
|
+
const rowsData = rowEls
|
|
508
|
+
.map((r) => {
|
|
509
|
+
const cells = Array.from(r.querySelectorAll("th, td")).map(
|
|
510
|
+
(c) => c.innerText?.trim() || c.textContent?.trim() || ""
|
|
511
|
+
);
|
|
512
|
+
return { cells, el: r };
|
|
513
|
+
})
|
|
514
|
+
.filter((r) => r.cells.length > 0);
|
|
515
|
+
|
|
516
|
+
if (rowsData.length === 0) return [];
|
|
517
|
+
|
|
518
|
+
let activeColumns = [...columns];
|
|
519
|
+
if (activeColumns.length === 0) {
|
|
520
|
+
if (hasHeaders && rowsData[headerRowIndex]) {
|
|
521
|
+
activeColumns = rowsData[headerRowIndex].cells.map((h, idx) => {
|
|
522
|
+
const key =
|
|
523
|
+
h.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") ||
|
|
524
|
+
`col_${idx + 1}`;
|
|
525
|
+
return { key, label: h || `Column ${idx + 1}`, columnIndex: idx, excluded: false };
|
|
526
|
+
});
|
|
527
|
+
} else {
|
|
528
|
+
activeColumns = rowsData[0].cells.map((_, idx) => ({
|
|
529
|
+
key: `col_${idx + 1}`,
|
|
530
|
+
label: `Column ${idx + 1}`,
|
|
531
|
+
columnIndex: idx,
|
|
532
|
+
excluded: false,
|
|
533
|
+
}));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const startIdx = hasHeaders ? headerRowIndex + 1 : 0;
|
|
538
|
+
const sliceRows = rowsData.slice(startIdx, startIdx + maxItems);
|
|
539
|
+
const results = [];
|
|
540
|
+
|
|
541
|
+
for (const row of sliceRows) {
|
|
542
|
+
const rec = {};
|
|
543
|
+
for (let colIdx = 0; colIdx < activeColumns.length; colIdx++) {
|
|
544
|
+
const col = activeColumns[colIdx];
|
|
545
|
+
if (col.excluded) continue;
|
|
546
|
+
let val = null;
|
|
547
|
+
const colSel = col.relativeSelector || col.selector;
|
|
548
|
+
if (colSel) {
|
|
549
|
+
const subEl = row.el.querySelector(colSel);
|
|
550
|
+
if (subEl) {
|
|
551
|
+
const attr = col.attribute || "text";
|
|
552
|
+
val = attr === "text" ? (subEl.innerText?.trim() || subEl.textContent?.trim()) : subEl.getAttribute(attr);
|
|
553
|
+
}
|
|
554
|
+
} else if (typeof col.columnIndex === "number" && col.columnIndex < row.cells.length) {
|
|
555
|
+
val = row.cells[col.columnIndex];
|
|
556
|
+
} else if (colIdx < row.cells.length) {
|
|
557
|
+
val = row.cells[colIdx];
|
|
558
|
+
}
|
|
559
|
+
if ((val === null || val === undefined || val === "") && col.defaultValue !== undefined) {
|
|
560
|
+
val = col.defaultValue;
|
|
561
|
+
}
|
|
562
|
+
rec[col.key] = val;
|
|
563
|
+
}
|
|
564
|
+
results.push(rec);
|
|
565
|
+
}
|
|
566
|
+
return results;
|
|
567
|
+
}, coll);
|
|
568
|
+
} else {
|
|
569
|
+
extractedCollections[coll.name] = await page.evaluate((cDef) => {
|
|
570
|
+
let items = [];
|
|
571
|
+
if (cDef.rowSelector) {
|
|
572
|
+
if (cDef.containerSelector) {
|
|
573
|
+
const container = document.querySelector(cDef.containerSelector);
|
|
574
|
+
if (container) {
|
|
575
|
+
items = Array.from(container.querySelectorAll(cDef.rowSelector));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (items.length === 0) {
|
|
579
|
+
items = Array.from(document.querySelectorAll(cDef.rowSelector));
|
|
580
|
+
}
|
|
581
|
+
} else if (cDef.containerSelector) {
|
|
582
|
+
items = Array.from(document.querySelectorAll(cDef.containerSelector));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (items.length === 0) return [];
|
|
586
|
+
|
|
587
|
+
const maxItems = cDef.maxItems || 100;
|
|
588
|
+
const columns = cDef.columns || [];
|
|
589
|
+
const results = [];
|
|
590
|
+
|
|
591
|
+
for (const item of items.slice(0, maxItems)) {
|
|
592
|
+
const rec = {};
|
|
593
|
+
for (const col of columns) {
|
|
594
|
+
if (col.excluded) continue;
|
|
595
|
+
let val = null;
|
|
596
|
+
const colSel = col.relativeSelector || col.selector;
|
|
597
|
+
if (colSel) {
|
|
598
|
+
const target = item.querySelector(colSel);
|
|
599
|
+
if (target) {
|
|
600
|
+
const attr = col.attribute || "text";
|
|
601
|
+
val = attr === "text" ? (target.innerText?.trim() || target.textContent?.trim()) : target.getAttribute(attr);
|
|
602
|
+
}
|
|
603
|
+
} else {
|
|
604
|
+
const attr = col.attribute || "text";
|
|
605
|
+
val = attr === "text" ? (item.innerText?.trim() || item.textContent?.trim()) : item.getAttribute(attr);
|
|
606
|
+
}
|
|
607
|
+
if ((val === null || val === undefined || val === "") && col.defaultValue !== undefined) {
|
|
608
|
+
val = col.defaultValue;
|
|
609
|
+
}
|
|
610
|
+
rec[col.key] = val;
|
|
611
|
+
}
|
|
612
|
+
const hasVal = Object.values(rec).some((v) => v !== null && v !== undefined && v !== "");
|
|
613
|
+
if (hasVal) results.push(rec);
|
|
614
|
+
}
|
|
615
|
+
return results;
|
|
616
|
+
}, coll);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Apply column transforms and type casting
|
|
620
|
+
if (Array.isArray(extractedCollections[coll.name]) && coll.columns) {
|
|
621
|
+
extractedCollections[coll.name] = extractedCollections[coll.name].map((rec) => {
|
|
622
|
+
const transformed = { ...rec };
|
|
623
|
+
for (const col of coll.columns) {
|
|
624
|
+
if (col.excluded) continue;
|
|
625
|
+
let val = transformed[col.key];
|
|
626
|
+
if ((val === null || val === undefined || val === "") && col.defaultValue !== undefined) {
|
|
627
|
+
val = col.defaultValue;
|
|
628
|
+
}
|
|
629
|
+
if (col.transforms && col.transforms.length > 0) {
|
|
630
|
+
val = runTransformPipeline(val, col.transforms);
|
|
631
|
+
}
|
|
632
|
+
if (col.type) {
|
|
633
|
+
val = castValueToType(val, col.type);
|
|
634
|
+
}
|
|
635
|
+
transformed[col.key] = val;
|
|
636
|
+
}
|
|
637
|
+
return transformed;
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
} catch {
|
|
641
|
+
extractedCollections[coll.name] = [];
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const executionTimeMs = Date.now() - startTime;
|
|
647
|
+
const hasExtractedValues = Object.values(raw).some(
|
|
648
|
+
(v) => v !== null && v !== undefined && v !== ""
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
// Avoid transferring huge raw HTML unless requested or nothing was extracted
|
|
652
|
+
let html = "";
|
|
653
|
+
if (includeHtml || (!hasExtractedValues && fieldMappings.length > 0)) {
|
|
654
|
+
html = await page.content().catch(() => "");
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return {
|
|
658
|
+
status,
|
|
659
|
+
title: pageTitle,
|
|
660
|
+
url: finalUrl,
|
|
661
|
+
raw,
|
|
662
|
+
strategySummary,
|
|
663
|
+
extractedData: raw,
|
|
664
|
+
collections: extractedCollections,
|
|
665
|
+
html,
|
|
666
|
+
executionTimeMs,
|
|
667
|
+
};
|
|
668
|
+
} finally {
|
|
669
|
+
await page.close().catch(() => {});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Handles fast SCRAPE_HTTP task via Cheerio.
|
|
675
|
+
*/
|
|
676
|
+
export async function handleHttpScrape(payload) {
|
|
677
|
+
const startTime = Date.now();
|
|
678
|
+
const {
|
|
679
|
+
url,
|
|
680
|
+
fieldMappings = [],
|
|
681
|
+
collections = [],
|
|
682
|
+
timeout = 15000,
|
|
683
|
+
headers = {},
|
|
684
|
+
extractSelectors = [],
|
|
685
|
+
includeHtml = false,
|
|
686
|
+
} = payload;
|
|
687
|
+
|
|
688
|
+
const response = await axios.get(url, {
|
|
689
|
+
timeout,
|
|
690
|
+
headers: {
|
|
691
|
+
"User-Agent":
|
|
692
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
693
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
694
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
695
|
+
...headers,
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
let pageHtml = typeof response.data === "string" ? response.data : Buffer.isBuffer(response.data) ? response.data.toString("utf-8") : "";
|
|
700
|
+
const $ = cheerio.load(pageHtml);
|
|
701
|
+
const title = $("title").first().text().trim() || "";
|
|
702
|
+
|
|
703
|
+
const raw = {};
|
|
704
|
+
const strategySummary = {};
|
|
705
|
+
|
|
706
|
+
// 1. Extract fieldMappings
|
|
707
|
+
if (Array.isArray(fieldMappings) && fieldMappings.length > 0) {
|
|
708
|
+
for (const mapping of fieldMappings) {
|
|
709
|
+
let extractedVal = null;
|
|
710
|
+
let successfulStrategy = null;
|
|
711
|
+
|
|
712
|
+
const strategies = mapping.strategies || [];
|
|
713
|
+
for (let i = 0; i < strategies.length; i++) {
|
|
714
|
+
const strat = strategies[i];
|
|
715
|
+
const val = extractCheerioField($, strat);
|
|
716
|
+
if (val !== null && val !== undefined && val !== "") {
|
|
717
|
+
extractedVal = val;
|
|
718
|
+
successfulStrategy = strat.type || `strategy_${i + 1}`;
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (mapping.transforms && mapping.transforms.length > 0) {
|
|
724
|
+
extractedVal = runTransformPipeline(extractedVal, mapping.transforms);
|
|
725
|
+
}
|
|
726
|
+
if (mapping.type) {
|
|
727
|
+
extractedVal = castValueToType(extractedVal, mapping.type);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
raw[mapping.field] = extractedVal;
|
|
731
|
+
strategySummary[mapping.field] = {
|
|
732
|
+
extracted: extractedVal !== null,
|
|
733
|
+
strategyUsed: successfulStrategy,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// 2. Extract legacy extractSelectors
|
|
739
|
+
if (Array.isArray(extractSelectors) && extractSelectors.length > 0) {
|
|
740
|
+
for (const item of extractSelectors) {
|
|
741
|
+
const { field, selector, type = "text", attribute = null } = item;
|
|
742
|
+
if (!field || !selector) continue;
|
|
743
|
+
|
|
744
|
+
try {
|
|
745
|
+
const values = [];
|
|
746
|
+
$(selector).each((_, el) => {
|
|
747
|
+
if (type === "attribute" && attribute) {
|
|
748
|
+
values.push($(el).attr(attribute) || "");
|
|
749
|
+
} else if (type === "html") {
|
|
750
|
+
values.push($(el).html() || "");
|
|
751
|
+
} else {
|
|
752
|
+
values.push($(el).text().trim());
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
const extractedVal = values.length <= 1 ? (values[0] ?? null) : values;
|
|
757
|
+
if (raw[field] === undefined) {
|
|
758
|
+
raw[field] = extractedVal;
|
|
759
|
+
}
|
|
760
|
+
} catch {
|
|
761
|
+
if (raw[field] === undefined) {
|
|
762
|
+
raw[field] = null;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// 3. Extract structured collections (Tables & Repeating items)
|
|
769
|
+
const extractedCollections = {};
|
|
770
|
+
if (Array.isArray(collections) && collections.length > 0) {
|
|
771
|
+
for (const coll of collections) {
|
|
772
|
+
if (!coll || !coll.name) continue;
|
|
773
|
+
try {
|
|
774
|
+
if (coll.type === "TABLE") {
|
|
775
|
+
extractedCollections[coll.name] = extractTableCollectionCheerio($, coll);
|
|
776
|
+
} else {
|
|
777
|
+
extractedCollections[coll.name] = extractRepeatingCollectionCheerio($, coll);
|
|
778
|
+
}
|
|
779
|
+
} catch {
|
|
780
|
+
extractedCollections[coll.name] = [];
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const executionTimeMs = Date.now() - startTime;
|
|
786
|
+
const hasExtractedValues = Object.values(raw).some(
|
|
787
|
+
(v) => v !== null && v !== undefined && v !== ""
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
return {
|
|
791
|
+
status: response.status,
|
|
792
|
+
title,
|
|
793
|
+
url,
|
|
794
|
+
raw,
|
|
795
|
+
strategySummary,
|
|
796
|
+
extractedData: raw,
|
|
797
|
+
collections: extractedCollections,
|
|
798
|
+
html: includeHtml || (!hasExtractedValues && fieldMappings.length > 0) ? pageHtml : "",
|
|
799
|
+
executionTimeMs,
|
|
800
|
+
};
|
|
801
|
+
}
|