@monostate/node-scraper 1.1.1 → 1.3.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.
- package/README.md +76 -1
- package/bin/lightpanda +0 -0
- package/index.js +256 -27
- package/package.json +10 -4
- package/scripts/install-lightpanda.js +183 -0
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -19,6 +19,17 @@ yarn add @monostate/node-scraper
|
|
|
19
19
|
pnpm add @monostate/node-scraper
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
**🎉 New in v1.3.0**: PDF parsing support added! Automatically extracts text, metadata, and page count from PDF documents.
|
|
23
|
+
|
|
24
|
+
**✨ Also in v1.2.0**: Lightpanda binary is now automatically downloaded and configured during installation! No manual setup required.
|
|
25
|
+
|
|
26
|
+
### Zero-Configuration Setup
|
|
27
|
+
|
|
28
|
+
The package now automatically:
|
|
29
|
+
- 📦 Downloads the correct Lightpanda binary for your platform (macOS, Linux, Windows/WSL)
|
|
30
|
+
- 🔧 Configures binary paths and permissions
|
|
31
|
+
- ✅ Validates installation health on first use
|
|
32
|
+
|
|
22
33
|
### Basic Usage
|
|
23
34
|
|
|
24
35
|
```javascript
|
|
@@ -36,6 +47,10 @@ console.log(screenshot.screenshot); // Base64 encoded image
|
|
|
36
47
|
// Quick screenshot (optimized for speed)
|
|
37
48
|
const quick = await quickShot('https://example.com');
|
|
38
49
|
console.log(quick.screenshot); // Fast screenshot capture
|
|
50
|
+
|
|
51
|
+
// PDF parsing (automatic detection)
|
|
52
|
+
const pdfResult = await smartScrape('https://example.com/document.pdf');
|
|
53
|
+
console.log(pdfResult.content); // Extracted text, metadata, page count
|
|
39
54
|
```
|
|
40
55
|
|
|
41
56
|
### Advanced Usage
|
|
@@ -57,12 +72,13 @@ await scraper.cleanup(); // Clean up resources
|
|
|
57
72
|
|
|
58
73
|
## 🔧 How It Works
|
|
59
74
|
|
|
60
|
-
BNCA uses a sophisticated
|
|
75
|
+
BNCA uses a sophisticated multi-tier system with intelligent detection:
|
|
61
76
|
|
|
62
77
|
### 1. 🔄 Direct Fetch (Fastest)
|
|
63
78
|
- Pure HTTP requests with intelligent HTML parsing
|
|
64
79
|
- **Performance**: Sub-second responses
|
|
65
80
|
- **Success rate**: 75% of websites
|
|
81
|
+
- **PDF Detection**: Automatically detects PDFs by URL, content-type, or magic bytes
|
|
66
82
|
|
|
67
83
|
### 2. 🐼 Lightpanda Browser (Fast)
|
|
68
84
|
- Lightweight browser engine (2-3x faster than Chromium)
|
|
@@ -74,6 +90,12 @@ BNCA uses a sophisticated 3-tier fallback system:
|
|
|
74
90
|
- **Performance**: Complete JavaScript execution
|
|
75
91
|
- **Fallback triggers**: Complex interactions needed
|
|
76
92
|
|
|
93
|
+
### 📄 PDF Parser (Specialized)
|
|
94
|
+
- Automatic PDF detection and parsing
|
|
95
|
+
- **Features**: Text extraction, metadata, page count
|
|
96
|
+
- **Smart Detection**: Works even when PDFs are served with wrong content-types
|
|
97
|
+
- **Performance**: Typically 100-500ms for most PDFs
|
|
98
|
+
|
|
77
99
|
### 📸 Screenshot Methods
|
|
78
100
|
- **Chrome CLI**: Direct Chrome screenshot capture
|
|
79
101
|
- **Quickshot**: Optimized with retry logic and smart timeouts
|
|
@@ -177,6 +199,34 @@ Clean up resources (close browser instances).
|
|
|
177
199
|
await scraper.cleanup();
|
|
178
200
|
```
|
|
179
201
|
|
|
202
|
+
### 📄 PDF Support
|
|
203
|
+
|
|
204
|
+
BNCA automatically detects and parses PDF documents:
|
|
205
|
+
|
|
206
|
+
```javascript
|
|
207
|
+
const pdfResult = await smartScrape('https://example.com/document.pdf');
|
|
208
|
+
|
|
209
|
+
// Parsed content includes:
|
|
210
|
+
const content = JSON.parse(pdfResult.content);
|
|
211
|
+
console.log(content.title); // PDF title
|
|
212
|
+
console.log(content.author); // Author name
|
|
213
|
+
console.log(content.pages); // Number of pages
|
|
214
|
+
console.log(content.text); // Full extracted text
|
|
215
|
+
console.log(content.creationDate); // Creation date
|
|
216
|
+
console.log(content.metadata); // Additional metadata
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**PDF Detection Methods:**
|
|
220
|
+
- URL ending with `.pdf`
|
|
221
|
+
- Content-Type header `application/pdf`
|
|
222
|
+
- Binary content starting with `%PDF` (magic bytes)
|
|
223
|
+
- Works with PDFs served as `application/octet-stream` (e.g., GitHub raw files)
|
|
224
|
+
|
|
225
|
+
**Limitations:**
|
|
226
|
+
- Maximum file size: 20MB
|
|
227
|
+
- Text extraction only (no image OCR)
|
|
228
|
+
- Requires `pdf-parse` dependency (automatically installed)
|
|
229
|
+
|
|
180
230
|
## 📱 Next.js Integration
|
|
181
231
|
|
|
182
232
|
### API Route Example
|
|
@@ -343,6 +393,31 @@ const scraper: BNCASmartScraper = new BNCASmartScraper({
|
|
|
343
393
|
const result: ScrapingResult = await scraper.scrape('https://example.com');
|
|
344
394
|
```
|
|
345
395
|
|
|
396
|
+
## 📋 Changelog
|
|
397
|
+
|
|
398
|
+
### v1.3.0 (Latest)
|
|
399
|
+
- 📄 **PDF Support**: Full PDF parsing with text extraction, metadata, and page count
|
|
400
|
+
- 🔍 **Smart PDF Detection**: Detects PDFs by URL patterns, content-type, or magic bytes
|
|
401
|
+
- 🚀 **Robust Parsing**: Handles PDFs served with incorrect content-types (e.g., GitHub raw files)
|
|
402
|
+
- ⚡ **Fast Performance**: PDF parsing typically completes in 100-500ms
|
|
403
|
+
- 📊 **Comprehensive Extraction**: Title, author, creation date, page count, and full text
|
|
404
|
+
|
|
405
|
+
### v1.2.0
|
|
406
|
+
- 🎉 **Auto-Installation**: Lightpanda binary is now automatically downloaded during `npm install`
|
|
407
|
+
- 🔧 **Cross-Platform Support**: Automatic detection and installation for macOS, Linux, and Windows/WSL
|
|
408
|
+
- ⚡ **Improved Performance**: Enhanced binary detection and ES6 module compatibility
|
|
409
|
+
- 🛠️ **Better Error Handling**: More robust installation scripts with retry logic
|
|
410
|
+
- 📦 **Zero Configuration**: No manual setup required - works out of the box
|
|
411
|
+
|
|
412
|
+
### v1.1.1
|
|
413
|
+
- Bug fixes and stability improvements
|
|
414
|
+
- Enhanced Puppeteer integration
|
|
415
|
+
|
|
416
|
+
### v1.1.0
|
|
417
|
+
- Added screenshot capabilities
|
|
418
|
+
- Improved fallback system
|
|
419
|
+
- Performance optimizations
|
|
420
|
+
|
|
346
421
|
## 🤝 Contributing
|
|
347
422
|
|
|
348
423
|
See the [main repository](https://github.com/your-org/bnca-prototype) for contribution guidelines.
|
package/bin/lightpanda
ADDED
|
Binary file
|
package/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import fetch from 'node-fetch';
|
|
2
|
-
import { spawn } from 'child_process';
|
|
2
|
+
import { spawn, execSync } from 'child_process';
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
|
+
import { existsSync, statSync } from 'fs';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
import { fileURLToPath } from 'url';
|
|
6
7
|
import { promises as fsPromises } from 'fs';
|
|
8
|
+
import pdfParse from 'pdf-parse/lib/pdf-parse.js';
|
|
7
9
|
|
|
8
10
|
let puppeteer = null;
|
|
9
11
|
try {
|
|
@@ -41,7 +43,8 @@ export class BNCASmartScraper {
|
|
|
41
43
|
this.stats = {
|
|
42
44
|
directFetch: { attempts: 0, successes: 0 },
|
|
43
45
|
lightpanda: { attempts: 0, successes: 0 },
|
|
44
|
-
puppeteer: { attempts: 0, successes: 0 }
|
|
46
|
+
puppeteer: { attempts: 0, successes: 0 },
|
|
47
|
+
pdf: { attempts: 0, successes: 0 }
|
|
45
48
|
};
|
|
46
49
|
}
|
|
47
50
|
|
|
@@ -59,6 +62,35 @@ export class BNCASmartScraper {
|
|
|
59
62
|
let lastError = null;
|
|
60
63
|
|
|
61
64
|
try {
|
|
65
|
+
// Check if URL is a PDF (by extension or content-type check)
|
|
66
|
+
const isPdfUrl = url.toLowerCase().endsWith('.pdf') ||
|
|
67
|
+
url.toLowerCase().includes('.pdf?') ||
|
|
68
|
+
url.toLowerCase().includes('/pdf/');
|
|
69
|
+
|
|
70
|
+
if (isPdfUrl) {
|
|
71
|
+
this.log(' 📄 PDF detected, using PDF parser...');
|
|
72
|
+
result = await this.tryPDFParse(url, config);
|
|
73
|
+
|
|
74
|
+
if (result.success) {
|
|
75
|
+
method = 'pdf';
|
|
76
|
+
this.log(' ✅ PDF parsing successful');
|
|
77
|
+
|
|
78
|
+
const totalTime = Date.now() - startTime;
|
|
79
|
+
return {
|
|
80
|
+
...result,
|
|
81
|
+
method,
|
|
82
|
+
performance: {
|
|
83
|
+
totalTime,
|
|
84
|
+
method
|
|
85
|
+
},
|
|
86
|
+
stats: this.getStats()
|
|
87
|
+
};
|
|
88
|
+
} else {
|
|
89
|
+
this.log(' ❌ PDF parsing failed');
|
|
90
|
+
lastError = result.error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
62
94
|
// Step 1: Try direct fetch first (fastest)
|
|
63
95
|
this.log(' 🔄 Attempting direct fetch...');
|
|
64
96
|
result = await this.tryDirectFetch(url, config);
|
|
@@ -66,6 +98,29 @@ export class BNCASmartScraper {
|
|
|
66
98
|
if (result.success && !result.needsBrowser) {
|
|
67
99
|
method = 'direct-fetch';
|
|
68
100
|
this.log(' ✅ Direct fetch successful');
|
|
101
|
+
} else if (result.isPdf) {
|
|
102
|
+
// Direct fetch detected a PDF, try PDF parser
|
|
103
|
+
this.log(' 📄 Direct fetch detected PDF content, using PDF parser...');
|
|
104
|
+
result = await this.tryPDFParse(url, config);
|
|
105
|
+
|
|
106
|
+
if (result.success) {
|
|
107
|
+
method = 'pdf';
|
|
108
|
+
this.log(' ✅ PDF parsing successful');
|
|
109
|
+
|
|
110
|
+
const totalTime = Date.now() - startTime;
|
|
111
|
+
return {
|
|
112
|
+
...result,
|
|
113
|
+
method,
|
|
114
|
+
performance: {
|
|
115
|
+
totalTime,
|
|
116
|
+
method
|
|
117
|
+
},
|
|
118
|
+
stats: this.getStats()
|
|
119
|
+
};
|
|
120
|
+
} else {
|
|
121
|
+
this.log(' ❌ PDF parsing failed');
|
|
122
|
+
lastError = result.error;
|
|
123
|
+
}
|
|
69
124
|
} else {
|
|
70
125
|
this.log(result.needsBrowser ? ' ⚠️ Browser rendering required' : ' ❌ Direct fetch failed');
|
|
71
126
|
lastError = result.error;
|
|
@@ -151,7 +206,32 @@ export class BNCASmartScraper {
|
|
|
151
206
|
};
|
|
152
207
|
}
|
|
153
208
|
|
|
154
|
-
|
|
209
|
+
// Check if the response is actually a PDF
|
|
210
|
+
const contentType = response.headers.get('content-type') || '';
|
|
211
|
+
if (contentType.includes('application/pdf')) {
|
|
212
|
+
return {
|
|
213
|
+
success: false,
|
|
214
|
+
error: 'Content is PDF, should use PDF parser',
|
|
215
|
+
isPdf: true
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Get response as array buffer to check magic bytes
|
|
220
|
+
const buffer = await response.arrayBuffer();
|
|
221
|
+
const firstBytes = new Uint8Array(buffer.slice(0, 5));
|
|
222
|
+
const signature = Array.from(firstBytes).map(b => String.fromCharCode(b)).join('');
|
|
223
|
+
|
|
224
|
+
// Check for PDF magic bytes
|
|
225
|
+
if (signature.startsWith('%PDF')) {
|
|
226
|
+
return {
|
|
227
|
+
success: false,
|
|
228
|
+
error: 'Content is PDF (detected by magic bytes), should use PDF parser',
|
|
229
|
+
isPdf: true
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Convert buffer back to text for HTML processing
|
|
234
|
+
const html = new TextDecoder().decode(buffer);
|
|
155
235
|
|
|
156
236
|
// Intelligent browser detection
|
|
157
237
|
const needsBrowser = this.detectBrowserRequirement(html, url);
|
|
@@ -201,7 +281,13 @@ export class BNCASmartScraper {
|
|
|
201
281
|
|
|
202
282
|
try {
|
|
203
283
|
// Check if binary exists
|
|
204
|
-
|
|
284
|
+
const stats = statSync(this.options.lightpandaPath);
|
|
285
|
+
if (!stats.isFile()) {
|
|
286
|
+
return {
|
|
287
|
+
success: false,
|
|
288
|
+
error: 'Lightpanda binary is not a file'
|
|
289
|
+
};
|
|
290
|
+
}
|
|
205
291
|
} catch {
|
|
206
292
|
return {
|
|
207
293
|
success: false,
|
|
@@ -210,9 +296,9 @@ export class BNCASmartScraper {
|
|
|
210
296
|
}
|
|
211
297
|
|
|
212
298
|
return new Promise((resolve) => {
|
|
213
|
-
const args = ['fetch', '--dump',
|
|
299
|
+
const args = ['fetch', '--dump', url];
|
|
214
300
|
const process = spawn(this.options.lightpandaPath, args, {
|
|
215
|
-
timeout: config.timeout + 1000 // Add buffer
|
|
301
|
+
timeout: config.timeout + 1000 // Add buffer for process timeout only
|
|
216
302
|
});
|
|
217
303
|
|
|
218
304
|
let output = '';
|
|
@@ -383,11 +469,114 @@ export class BNCASmartScraper {
|
|
|
383
469
|
}
|
|
384
470
|
}
|
|
385
471
|
|
|
472
|
+
/**
|
|
473
|
+
* PDF parsing method - handles PDF documents
|
|
474
|
+
*/
|
|
475
|
+
async tryPDFParse(url, config) {
|
|
476
|
+
this.stats.pdf.attempts++;
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
// Download PDF with timeout
|
|
480
|
+
const controller = new AbortController();
|
|
481
|
+
const timeoutId = setTimeout(() => controller.abort(), config.timeout);
|
|
482
|
+
|
|
483
|
+
const response = await fetch(url, {
|
|
484
|
+
headers: {
|
|
485
|
+
'User-Agent': config.userAgent,
|
|
486
|
+
'Accept': 'application/pdf,*/*'
|
|
487
|
+
},
|
|
488
|
+
signal: controller.signal
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
clearTimeout(timeoutId);
|
|
492
|
+
|
|
493
|
+
if (!response.ok) {
|
|
494
|
+
return {
|
|
495
|
+
success: false,
|
|
496
|
+
error: `HTTP ${response.status}: ${response.statusText}`
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Check content type (be lenient - accept various content types)
|
|
501
|
+
const contentType = response.headers.get('content-type') || '';
|
|
502
|
+
const acceptableTypes = ['pdf', 'octet-stream', 'binary', 'download'];
|
|
503
|
+
const isAcceptableType = acceptableTypes.some(type => contentType.includes(type));
|
|
504
|
+
|
|
505
|
+
if (!isAcceptableType && !url.toLowerCase().includes('.pdf')) {
|
|
506
|
+
return {
|
|
507
|
+
success: false,
|
|
508
|
+
error: `Not a PDF document: ${contentType}`
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Get PDF buffer
|
|
513
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
514
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
515
|
+
|
|
516
|
+
// Check size limit (20MB)
|
|
517
|
+
if (buffer.length > 20 * 1024 * 1024) {
|
|
518
|
+
return {
|
|
519
|
+
success: false,
|
|
520
|
+
error: 'PDF too large (max 20MB)'
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Parse PDF
|
|
525
|
+
const pdfData = await pdfParse(buffer);
|
|
526
|
+
|
|
527
|
+
// Extract structured content
|
|
528
|
+
const content = {
|
|
529
|
+
title: pdfData.info?.Title || 'Untitled PDF',
|
|
530
|
+
author: pdfData.info?.Author || '',
|
|
531
|
+
subject: pdfData.info?.Subject || '',
|
|
532
|
+
keywords: pdfData.info?.Keywords || '',
|
|
533
|
+
creator: pdfData.info?.Creator || '',
|
|
534
|
+
producer: pdfData.info?.Producer || '',
|
|
535
|
+
creationDate: pdfData.info?.CreationDate || '',
|
|
536
|
+
modificationDate: pdfData.info?.ModificationDate || '',
|
|
537
|
+
pages: pdfData.numpages || 0,
|
|
538
|
+
text: pdfData.text || '',
|
|
539
|
+
metadata: pdfData.metadata || null,
|
|
540
|
+
url: url
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
this.stats.pdf.successes++;
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
success: true,
|
|
547
|
+
content: JSON.stringify(content, null, 2),
|
|
548
|
+
size: buffer.length,
|
|
549
|
+
contentType: 'application/pdf',
|
|
550
|
+
pages: content.pages
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
} catch (error) {
|
|
554
|
+
return {
|
|
555
|
+
success: false,
|
|
556
|
+
error: `PDF parsing error: ${error.message}`
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
386
561
|
/**
|
|
387
562
|
* Intelligent detection of browser requirement
|
|
388
563
|
*/
|
|
389
564
|
detectBrowserRequirement(html, url) {
|
|
390
|
-
//
|
|
565
|
+
// Whitelist simple sites that should always use direct fetch
|
|
566
|
+
const simpleSites = [
|
|
567
|
+
'example.com',
|
|
568
|
+
'httpbin.org',
|
|
569
|
+
'wikipedia.org',
|
|
570
|
+
'github.io',
|
|
571
|
+
'netlify.app',
|
|
572
|
+
'vercel.app'
|
|
573
|
+
];
|
|
574
|
+
|
|
575
|
+
if (simpleSites.some(site => url.includes(site))) {
|
|
576
|
+
return false; // Always use direct fetch for these
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Check for common SPA patterns (be more specific)
|
|
391
580
|
const spaIndicators = [
|
|
392
581
|
/<div[^>]*id=['"]?root['"]?[^>]*>\s*<\/div>/i,
|
|
393
582
|
/<div[^>]*id=['"]?app['"]?[^>]*>\s*<\/div>/i,
|
|
@@ -411,7 +600,23 @@ export class BNCASmartScraper {
|
|
|
411
600
|
/attention required.*cloudflare/i
|
|
412
601
|
];
|
|
413
602
|
|
|
414
|
-
//
|
|
603
|
+
// Domain-based checks for known SPA sites
|
|
604
|
+
const domainIndicators = [
|
|
605
|
+
/instagram\.com/i,
|
|
606
|
+
/twitter\.com/i,
|
|
607
|
+
/facebook\.com/i,
|
|
608
|
+
/linkedin\.com/i,
|
|
609
|
+
/maps\.google/i,
|
|
610
|
+
/gmail\.com/i,
|
|
611
|
+
/youtube\.com/i
|
|
612
|
+
];
|
|
613
|
+
|
|
614
|
+
// Check if it's clearly a SPA or protected site
|
|
615
|
+
const hasSpaIndicators = spaIndicators.some(pattern => pattern.test(html));
|
|
616
|
+
const hasProtection = protectionIndicators.some(pattern => pattern.test(html));
|
|
617
|
+
const isKnownSpa = domainIndicators.some(pattern => pattern.test(url));
|
|
618
|
+
|
|
619
|
+
// Check for minimal content BUT only if we also have SPA indicators
|
|
415
620
|
const bodyContent = html.match(/<body[^>]*>([\s\S]*)<\/body>/i)?.[1] || '';
|
|
416
621
|
const textContent = bodyContent
|
|
417
622
|
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
@@ -420,22 +625,11 @@ export class BNCASmartScraper {
|
|
|
420
625
|
.replace(/\s+/g, ' ')
|
|
421
626
|
.trim();
|
|
422
627
|
|
|
423
|
-
const hasMinimalContent = textContent.length <
|
|
424
|
-
|
|
425
|
-
// Domain-based checks
|
|
426
|
-
const domainIndicators = [
|
|
427
|
-
/instagram\.com/i,
|
|
428
|
-
/twitter\.com/i,
|
|
429
|
-
/facebook\.com/i,
|
|
430
|
-
/linkedin\.com/i,
|
|
431
|
-
/maps\.google/i
|
|
432
|
-
];
|
|
628
|
+
const hasMinimalContent = textContent.length < 200; // More conservative threshold
|
|
629
|
+
const isLikelySpa = hasMinimalContent && hasSpaIndicators;
|
|
433
630
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
protectionIndicators.some(pattern => pattern.test(html)) ||
|
|
437
|
-
(hasMinimalContent && spaIndicators.some(pattern => pattern.test(html))) ||
|
|
438
|
-
domainIndicators.some(pattern => pattern.test(url));
|
|
631
|
+
// Only require browser if we have strong indicators
|
|
632
|
+
const needsBrowser = hasProtection || isKnownSpa || isLikelySpa;
|
|
439
633
|
|
|
440
634
|
return needsBrowser;
|
|
441
635
|
}
|
|
@@ -541,19 +735,40 @@ export class BNCASmartScraper {
|
|
|
541
735
|
* Find Lightpanda binary
|
|
542
736
|
*/
|
|
543
737
|
findLightpandaBinary() {
|
|
738
|
+
// First check the package's bin directory (installed by postinstall script)
|
|
739
|
+
const packageDir = path.dirname(new URL(import.meta.url).pathname);
|
|
740
|
+
const packageBinPath = path.join(packageDir, 'bin', 'lightpanda');
|
|
741
|
+
|
|
544
742
|
const possiblePaths = [
|
|
743
|
+
packageBinPath, // Package's bin directory (highest priority)
|
|
545
744
|
'./lightpanda',
|
|
546
745
|
'../lightpanda',
|
|
547
746
|
'./lightpanda/lightpanda',
|
|
548
747
|
'/usr/local/bin/lightpanda',
|
|
549
|
-
path.join(process.cwd(), 'lightpanda')
|
|
748
|
+
path.join(process.cwd(), 'lightpanda'),
|
|
749
|
+
path.join(process.cwd(), 'bin', 'lightpanda')
|
|
550
750
|
];
|
|
551
751
|
|
|
552
752
|
for (const binaryPath of possiblePaths) {
|
|
553
753
|
try {
|
|
554
|
-
// Synchronous check for binary
|
|
754
|
+
// Synchronous check for binary existence and executability
|
|
555
755
|
const fullPath = path.resolve(binaryPath);
|
|
556
|
-
|
|
756
|
+
if (existsSync(fullPath)) {
|
|
757
|
+
const stats = statSync(fullPath);
|
|
758
|
+
if (stats.isFile()) {
|
|
759
|
+
// Check if it's executable (on Unix-like systems including WSL)
|
|
760
|
+
if (process.platform !== 'win32' || this.isWSL()) {
|
|
761
|
+
const mode = stats.mode;
|
|
762
|
+
const isExecutable = Boolean(mode & parseInt('111', 8));
|
|
763
|
+
if (isExecutable) {
|
|
764
|
+
return fullPath;
|
|
765
|
+
}
|
|
766
|
+
} else {
|
|
767
|
+
// On native Windows (not WSL), Lightpanda is not supported
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
557
772
|
} catch {
|
|
558
773
|
continue;
|
|
559
774
|
}
|
|
@@ -562,6 +777,18 @@ export class BNCASmartScraper {
|
|
|
562
777
|
return null;
|
|
563
778
|
}
|
|
564
779
|
|
|
780
|
+
/**
|
|
781
|
+
* Check if running in WSL environment
|
|
782
|
+
*/
|
|
783
|
+
isWSL() {
|
|
784
|
+
try {
|
|
785
|
+
const uname = execSync('uname -r', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
786
|
+
return uname.toLowerCase().includes('microsoft') || uname.toLowerCase().includes('wsl');
|
|
787
|
+
} catch {
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
565
792
|
/**
|
|
566
793
|
* Get performance statistics
|
|
567
794
|
*/
|
|
@@ -574,7 +801,9 @@ export class BNCASmartScraper {
|
|
|
574
801
|
lightpanda: this.stats.lightpanda.attempts > 0 ?
|
|
575
802
|
(this.stats.lightpanda.successes / this.stats.lightpanda.attempts * 100).toFixed(1) + '%' : '0%',
|
|
576
803
|
puppeteer: this.stats.puppeteer.attempts > 0 ?
|
|
577
|
-
(this.stats.puppeteer.successes / this.stats.puppeteer.attempts * 100).toFixed(1) + '%' : '0%'
|
|
804
|
+
(this.stats.puppeteer.successes / this.stats.puppeteer.attempts * 100).toFixed(1) + '%' : '0%',
|
|
805
|
+
pdf: this.stats.pdf.attempts > 0 ?
|
|
806
|
+
(this.stats.pdf.successes / this.stats.pdf.attempts * 100).toFixed(1) + '%' : '0%'
|
|
578
807
|
}
|
|
579
808
|
};
|
|
580
809
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monostate/node-scraper",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Intelligent web scraping with multi-level fallback system - 11.35x faster than Firecrawl",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Intelligent web scraping with PDF support and multi-level fallback system - 11.35x faster than Firecrawl",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "index.d.ts",
|
|
@@ -15,8 +15,13 @@
|
|
|
15
15
|
"index.js",
|
|
16
16
|
"index.d.ts",
|
|
17
17
|
"README.md",
|
|
18
|
-
"package.json"
|
|
18
|
+
"package.json",
|
|
19
|
+
"scripts/",
|
|
20
|
+
"bin/"
|
|
19
21
|
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"postinstall": "node scripts/install-lightpanda.js"
|
|
24
|
+
},
|
|
20
25
|
"keywords": [
|
|
21
26
|
"web-scraping",
|
|
22
27
|
"crawling",
|
|
@@ -35,7 +40,8 @@
|
|
|
35
40
|
"author": "BNCA Team",
|
|
36
41
|
"license": "MIT",
|
|
37
42
|
"dependencies": {
|
|
38
|
-
"node-fetch": "^3.3.2"
|
|
43
|
+
"node-fetch": "^3.3.2",
|
|
44
|
+
"pdf-parse": "^1.1.1"
|
|
39
45
|
},
|
|
40
46
|
"peerDependencies": {
|
|
41
47
|
"puppeteer": ">=20.0.0"
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import https from 'https';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { createWriteStream } from 'fs';
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
|
|
9
|
+
const LIGHTPANDA_VERSION = 'nightly';
|
|
10
|
+
const BINARY_DIR = path.join(path.dirname(path.dirname(new URL(import.meta.url).pathname)), 'bin');
|
|
11
|
+
const BINARY_NAME = 'lightpanda';
|
|
12
|
+
const BINARY_PATH = path.join(BINARY_DIR, BINARY_NAME);
|
|
13
|
+
|
|
14
|
+
// Platform-specific download URLs (matching official Lightpanda instructions)
|
|
15
|
+
const DOWNLOAD_URLS = {
|
|
16
|
+
'darwin': `https://github.com/lightpanda-io/browser/releases/download/${LIGHTPANDA_VERSION}/lightpanda-aarch64-macos`,
|
|
17
|
+
'linux': `https://github.com/lightpanda-io/browser/releases/download/${LIGHTPANDA_VERSION}/lightpanda-x86_64-linux`,
|
|
18
|
+
'wsl': `https://github.com/lightpanda-io/browser/releases/download/${LIGHTPANDA_VERSION}/lightpanda-x86_64-linux` // WSL uses Linux binary
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function detectPlatform() {
|
|
22
|
+
const platform = process.platform;
|
|
23
|
+
|
|
24
|
+
if (platform === 'darwin') {
|
|
25
|
+
return 'darwin';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (platform === 'linux') {
|
|
29
|
+
return 'linux';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (platform === 'win32') {
|
|
33
|
+
// Check if we're running in WSL
|
|
34
|
+
try {
|
|
35
|
+
const uname = execSync('uname -r', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
36
|
+
if (uname.toLowerCase().includes('microsoft') || uname.toLowerCase().includes('wsl')) {
|
|
37
|
+
console.log('🐧 WSL detected - using Linux binary');
|
|
38
|
+
return 'wsl';
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// Not in WSL or uname not available
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log('⚠️ Windows detected. Lightpanda is recommended to run in WSL2.');
|
|
45
|
+
console.log(' Please install WSL2 and run this package from within WSL2.');
|
|
46
|
+
console.log(' See: https://docs.microsoft.com/en-us/windows/wsl/install');
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function downloadFile(url, destination) {
|
|
54
|
+
console.log(`📥 Downloading Lightpanda binary from: ${url}`);
|
|
55
|
+
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const request = https.get(url, (response) => {
|
|
58
|
+
// Handle redirects
|
|
59
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
60
|
+
return downloadFile(response.headers.location, destination).then(resolve).catch(reject);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (response.statusCode !== 200) {
|
|
64
|
+
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const fileStream = createWriteStream(destination);
|
|
69
|
+
const totalSize = parseInt(response.headers['content-length'] || '0');
|
|
70
|
+
let downloadedSize = 0;
|
|
71
|
+
|
|
72
|
+
response.on('data', (chunk) => {
|
|
73
|
+
downloadedSize += chunk.length;
|
|
74
|
+
if (totalSize > 0) {
|
|
75
|
+
const progress = (downloadedSize / totalSize * 100).toFixed(1);
|
|
76
|
+
process.stdout.write(`\r⏳ Progress: ${progress}%`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
response.on('end', () => {
|
|
81
|
+
process.stdout.write('\r✅ Download completed! \n');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
response.pipe(fileStream);
|
|
85
|
+
|
|
86
|
+
fileStream.on('finish', () => {
|
|
87
|
+
fileStream.close();
|
|
88
|
+
resolve();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
fileStream.on('error', reject);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
request.on('error', reject);
|
|
95
|
+
request.setTimeout(60000, () => {
|
|
96
|
+
request.destroy();
|
|
97
|
+
reject(new Error('Download timeout'));
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function makeExecutable(filePath) {
|
|
103
|
+
try {
|
|
104
|
+
await fs.promises.chmod(filePath, 0o755);
|
|
105
|
+
console.log(`🔧 Made ${filePath} executable`);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.warn(`⚠️ Warning: Could not make binary executable: ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function installLightpanda() {
|
|
112
|
+
try {
|
|
113
|
+
const platform = detectPlatform();
|
|
114
|
+
|
|
115
|
+
if (!platform) {
|
|
116
|
+
console.log(' Falling back to Puppeteer for browser-based scraping.');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const downloadUrl = DOWNLOAD_URLS[platform];
|
|
121
|
+
|
|
122
|
+
if (!downloadUrl) {
|
|
123
|
+
console.log(`⚠️ Lightpanda binary not available for platform: ${platform}`);
|
|
124
|
+
console.log(' Falling back to Puppeteer for browser-based scraping.');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Create bin directory if it doesn't exist
|
|
129
|
+
if (!fs.existsSync(BINARY_DIR)) {
|
|
130
|
+
await fs.promises.mkdir(BINARY_DIR, { recursive: true });
|
|
131
|
+
console.log(`📁 Created directory: ${BINARY_DIR}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check if binary already exists
|
|
135
|
+
if (fs.existsSync(BINARY_PATH)) {
|
|
136
|
+
console.log(`✅ Lightpanda binary already exists at: ${BINARY_PATH}`);
|
|
137
|
+
await makeExecutable(BINARY_PATH);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log(`🚀 Installing Lightpanda binary for ${platform}...`);
|
|
142
|
+
|
|
143
|
+
// Download the binary
|
|
144
|
+
await downloadFile(downloadUrl, BINARY_PATH);
|
|
145
|
+
|
|
146
|
+
// Make executable (all Unix-like systems including WSL)
|
|
147
|
+
await makeExecutable(BINARY_PATH);
|
|
148
|
+
|
|
149
|
+
// Verify the binary
|
|
150
|
+
if (fs.existsSync(BINARY_PATH)) {
|
|
151
|
+
const stats = await fs.promises.stat(BINARY_PATH);
|
|
152
|
+
console.log(`✅ Lightpanda binary installed successfully!`);
|
|
153
|
+
console.log(` Location: ${BINARY_PATH}`);
|
|
154
|
+
console.log(` Size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
|
|
155
|
+
|
|
156
|
+
// Additional WSL information
|
|
157
|
+
if (platform === 'wsl') {
|
|
158
|
+
console.log('');
|
|
159
|
+
console.log('📝 WSL Setup Notes:');
|
|
160
|
+
console.log(' - Lightpanda binary installed for WSL environment');
|
|
161
|
+
console.log(' - Ensure your Node.js application runs within WSL2');
|
|
162
|
+
console.log(' - For best performance, keep files within WSL filesystem');
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
throw new Error('Binary download verification failed');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error(`❌ Failed to install Lightpanda binary: ${error.message}`);
|
|
170
|
+
console.log(' The package will fall back to Puppeteer for browser-based scraping.');
|
|
171
|
+
|
|
172
|
+
// Don't fail the installation, just log the issue
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Only run if this is the main module (not imported)
|
|
178
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
179
|
+
installLightpanda().catch((error) => {
|
|
180
|
+
console.error('Installation failed:', error);
|
|
181
|
+
process.exit(0); // Don't fail package installation
|
|
182
|
+
});
|
|
183
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 BNCA Team
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|