GitHub Actions Bot commited on
Commit
4d4616f
·
1 Parent(s): ec4bf22

🚀 Auto-deploy: 8968db766ef462f994117cc8e723669bb87429fb

Browse files
Files changed (5) hide show
  1. package.json +1 -1
  2. src/anti-detection.ts +121 -0
  3. src/config.ts +17 -6
  4. src/otto-crawler.ts +16 -7
  5. src/proxy-config.ts +20 -40
package.json CHANGED
@@ -7,7 +7,7 @@
7
  "start": "tsx quick-start.ts",
8
  "serve": "tsx watch server.ts",
9
  "start:prod": "tsx server.ts",
10
- "crawl:otto": "tsx src/otto-crawler.ts",
11
  "crawl:amazon": "tsx src/amazon-crawler.ts",
12
  "crawl:batch": "tsx src/batch-crawler.ts",
13
  "quick": "tsx quick-start.ts",
 
7
  "start": "tsx quick-start.ts",
8
  "serve": "tsx watch server.ts",
9
  "start:prod": "tsx server.ts",
10
+ "crawl:otto": "tsx src/-crawler.ts",
11
  "crawl:amazon": "tsx src/amazon-crawler.ts",
12
  "crawl:batch": "tsx src/batch-crawler.ts",
13
  "quick": "tsx quick-start.ts",
src/anti-detection.ts ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Page } from 'playwright';
2
+
3
+ /**
4
+ * 注入全面的反自动化检测脚本
5
+ */
6
+ export async function injectAntiDetectionScripts(page: Page): Promise<void> {
7
+ await page.addInitScript(() => {
8
+ // 覆盖navigator.webdriver属性
9
+ Object.defineProperty(navigator, 'webdriver', {
10
+ get: () => undefined,
11
+ });
12
+
13
+ // 移除Chrome特有属性
14
+ (window as any).chrome = {
15
+ runtime: {},
16
+ };
17
+
18
+ // 覆盖plugins和languages
19
+ Object.defineProperty(navigator, 'plugins', {
20
+ get: () => [1, 2, 3, 4, 5] as any,
21
+ });
22
+
23
+ Object.defineProperty(navigator, 'languages', {
24
+ get: () => ['en-US', 'en'],
25
+ });
26
+
27
+ // 隐藏WebDriver属性的其他方式
28
+ if ('_Selenium_IDE_Recorder' in window) {
29
+ delete window['_Selenium_IDE_Recorder'];
30
+ }
31
+
32
+ // 覆盖userAgent中的Headless标识
33
+ const originalUserAgent = navigator.userAgent;
34
+ Object.defineProperty(navigator, 'userAgent', {
35
+ value: originalUserAgent.replace('HeadlessChrome', 'Chrome'),
36
+ });
37
+
38
+ // 隐藏automation相关属性
39
+ Object.defineProperty(navigator, 'permissions', {
40
+ value: {
41
+ ...navigator.permissions,
42
+ query: new Proxy(navigator.permissions.query, {
43
+ apply(target, thisArg, args) {
44
+ return target.apply(thisArg, args).catch(() => ({
45
+ state: 'granted',
46
+ onchange: null,
47
+ addEventListener: () => {},
48
+ removeEventListener: () => {},
49
+ dispatchEvent: () => true,
50
+ }));
51
+ }
52
+ })
53
+ }
54
+ });
55
+ });
56
+ }
57
+
58
+ /**
59
+ * 设置真实的浏览器视窗尺寸
60
+ */
61
+ export async function setRealisticViewport(page: Page): Promise<void> {
62
+ // 设置常见的桌面分辨率
63
+ const viewports = [
64
+ { width: 1920, height: 1080 },
65
+ { width: 1366, height: 768 },
66
+ { width: 1536, height: 864 },
67
+ { width: 1440, height: 900 }
68
+ ];
69
+ const randomViewport = viewports[Math.floor(Math.random() * viewports.length)];
70
+ await page.setViewportSize(randomViewport);
71
+ }
72
+
73
+ /**
74
+ * 模拟真实用户行为
75
+ */
76
+ export async function simulateHumanBehavior(page: Page): Promise<void> {
77
+ try {
78
+ // 设置真实视窗
79
+ await setRealisticViewport(page);
80
+
81
+ // 随机鼠标移动
82
+ const dimensions = await page.evaluate(() => ({
83
+ width: document.documentElement.clientWidth,
84
+ height: document.documentElement.clientHeight
85
+ }));
86
+
87
+ if (dimensions.width > 0 && dimensions.height > 0) {
88
+ // 多次随机移动模拟真实浏览
89
+ for (let i = 0; i < 2 + Math.floor(Math.random() * 3); i++) {
90
+ const x = Math.floor(Math.random() * dimensions.width * 0.8) + 50;
91
+ const y = Math.floor(Math.random() * dimensions.height * 0.8) + 50;
92
+ await page.mouse.move(x, y);
93
+ await page.waitForTimeout(200 + Math.random() * 300);
94
+ }
95
+ }
96
+
97
+ // 随机滚动
98
+ const scrollHeight = await page.evaluate(() => document.body.scrollHeight);
99
+ if (scrollHeight > 0) {
100
+ const scrollPositions = [0.1, 0.3, 0.5, 0.7];
101
+ const randomPos = scrollPositions[Math.floor(Math.random() * scrollPositions.length)];
102
+ const randomScroll = Math.min(scrollHeight * randomPos, 1200);
103
+ await page.evaluate((height) => window.scrollTo(0, height), randomScroll);
104
+ }
105
+
106
+ // 随机等待时间
107
+ const randomWait = Math.floor(Math.random() * 1000) + 800;
108
+ await page.waitForTimeout(randomWait);
109
+
110
+ } catch (error) {
111
+ // 忽略模拟行为的错误,不影响主要功能
112
+ }
113
+ }
114
+
115
+ /**
116
+ * 添加随机延迟
117
+ */
118
+ export async function addRandomDelay(page: Page, minMs: number = 1500, maxMs: number = 4000): Promise<void> {
119
+ const delay = Math.floor(Math.random() * (maxMs - minMs)) + minMs;
120
+ await page.waitForTimeout(delay);
121
+ }
src/config.ts CHANGED
@@ -83,11 +83,11 @@ const PRESETS: Record<string, Partial<CrawlerConfig>> = {
83
  }
84
  },
85
  stealth: {
86
- maxConcurrency: 3,
87
- requestHandlerTimeoutSecs: 180,
88
- maxRequestRetries: 1,
89
- pageLoadWaitMs: 500,
90
- useProxy: true, // 启用代理
91
  launchOptions: {
92
  headless: 'new',
93
  args: [
@@ -105,7 +105,18 @@ const PRESETS: Record<string, Partial<CrawlerConfig>> = {
105
  '--mute-audio',
106
  '--no-first-run',
107
  '--no-pings',
108
- '--disable-features=IsolateOrigins,site-per-process'
 
 
 
 
 
 
 
 
 
 
 
109
  ]
110
  }
111
  }
 
83
  }
84
  },
85
  stealth: {
86
+ maxConcurrency: 2, // 进一步降低并发数
87
+ requestHandlerTimeoutSecs: 240, // 延长超时时间
88
+ maxRequestRetries: 2, // 增加重试次数
89
+ pageLoadWaitMs: 800, // 增加页面等待时间
90
+ useProxy: false, // 启用代理
91
  launchOptions: {
92
  headless: 'new',
93
  args: [
 
105
  '--mute-audio',
106
  '--no-first-run',
107
  '--no-pings',
108
+ '--disable-features=IsolateOrigins,site-per-process',
109
+ // 新增反检测参数
110
+ '--disable-blink-features=AutomationControlled',
111
+ '--disable-automation',
112
+ '--disable-web-security',
113
+ '--allow-running-insecure-content',
114
+ '--disable-features=VizDisplayCompositor',
115
+ '--disable-features=ImprovedCookieControls,SameSiteByDefaultCookies,CookiesWithoutSameSiteMustBeSecure',
116
+ '--disable-features=WebRtcHideLocalIpsWithMdns',
117
+ '--disable-features=WebContentsForceDark',
118
+ '--disable-features=AutofillEnableAccountWalletStorage',
119
+ '--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
120
  ]
121
  }
122
  }
src/otto-crawler.ts CHANGED
@@ -3,6 +3,8 @@ import { Page } from 'playwright';
3
  import { getCrawlerConfig, CrawlerConfig } from './config.js';
4
  import { ProductData } from './productData.js';
5
  import { createProxyConfiguration } from './proxy-config.js';
 
 
6
 
7
  // 默认配置
8
  let currentConfig: CrawlerConfig = getCrawlerConfig();
@@ -93,13 +95,16 @@ function createOttoCrawler(options: { preset?: string } & Partial<CrawlerConfig>
93
  requestHandlerTimeoutSecs: config.requestHandlerTimeoutSecs,
94
  maxRequestRetries: config.maxRequestRetries,
95
  navigationTimeoutSecs: 120, // 导航超时120秒
96
- // 使用从ip.txt加载的代理配置
97
  proxyConfiguration: proxyConfiguration,
98
  launchContext: {
99
  launchOptions
100
  },
101
  preNavigationHooks: [
102
  async ({ page }) => {
 
 
 
103
  // 拦截并 abort 非必要的资源类型
104
  await page.route('**/*', (route, request) => {
105
  const resourceType = request.resourceType();
@@ -116,17 +121,21 @@ function createOttoCrawler(options: { preset?: string } & Partial<CrawlerConfig>
116
  const startTime = Date.now();
117
  log.info(`🔄 开始处理: ${request.url}`);
118
 
 
 
 
119
  // 1. 等待 DOM 加载完成
120
  const domStartTime = Date.now();
121
  await page.waitForLoadState('domcontentloaded');
122
  const domLoadTime = Date.now() - domStartTime;
123
  log.info(`⏱️ DOM 加载耗时: ${domLoadTime}ms`);
124
 
125
- // 2. 智能等待:等待页面标题或价格等关键元素出现
 
 
 
126
  const waitStartTime = Date.now();
127
  const waitForSelectors = [
128
- // ...currentConfig.selectors.title || [],
129
- // ...currentConfig.selectors.price || []
130
  'span.js_pdp_cr-rating--review-count'
131
  ];
132
 
@@ -151,7 +160,7 @@ function createOttoCrawler(options: { preset?: string } & Partial<CrawlerConfig>
151
  log.warning('⚠️ 未检测到关键元素,但将继续尝试提取数据');
152
  }
153
 
154
- // 3. 短暂等待让动态内容渲染(可选,根据实际情况调整)
155
  await page.waitForTimeout(500);
156
 
157
  const productData: ProductData = {
@@ -252,10 +261,10 @@ export {
252
  // 如果直接运行此文件,执行示例
253
  if (import.meta.url === `file://${process.argv[1]}`) {
254
  const exampleUrls = [
255
- 'https://www.otto.de/p/samsung-galaxy-s23-sm-s911b-dual-sim-256-gb-phantom-black-1487828507/'
256
  ];
257
 
258
- runOttoCrawler(exampleUrls, { preset: 'stable' })
259
  .then(results => {
260
  console.log('\n📦 抓取结果:');
261
  console.log(JSON.stringify(results, null, 2));
 
3
  import { getCrawlerConfig, CrawlerConfig } from './config.js';
4
  import { ProductData } from './productData.js';
5
  import { createProxyConfiguration } from './proxy-config.js';
6
+ // 导入反检测模块
7
+ import { injectAntiDetectionScripts, simulateHumanBehavior, addRandomDelay } from './anti-detection.js';
8
 
9
  // 默认配置
10
  let currentConfig: CrawlerConfig = getCrawlerConfig();
 
95
  requestHandlerTimeoutSecs: config.requestHandlerTimeoutSecs,
96
  maxRequestRetries: config.maxRequestRetries,
97
  navigationTimeoutSecs: 120, // 导航超时120秒
98
+ // 使用 Decodo 代理配置
99
  proxyConfiguration: proxyConfiguration,
100
  launchContext: {
101
  launchOptions
102
  },
103
  preNavigationHooks: [
104
  async ({ page }) => {
105
+ // 注入反检测脚本
106
+ await injectAntiDetectionScripts(page);
107
+
108
  // 拦截并 abort 非必要的资源类型
109
  await page.route('**/*', (route, request) => {
110
  const resourceType = request.resourceType();
 
121
  const startTime = Date.now();
122
  log.info(`🔄 开始处理: ${request.url}`);
123
 
124
+ // 添加随机延迟,模拟真实用户行为
125
+ await addRandomDelay(page, 1000, 3000);
126
+
127
  // 1. 等待 DOM 加载完成
128
  const domStartTime = Date.now();
129
  await page.waitForLoadState('domcontentloaded');
130
  const domLoadTime = Date.now() - domStartTime;
131
  log.info(`⏱️ DOM 加载耗时: ${domLoadTime}ms`);
132
 
133
+ // 2. 模拟真实用户行为
134
+ await simulateHumanBehavior(page);
135
+
136
+ // 3. 智能等待:等待页面标题或价格等关键元素出现
137
  const waitStartTime = Date.now();
138
  const waitForSelectors = [
 
 
139
  'span.js_pdp_cr-rating--review-count'
140
  ];
141
 
 
160
  log.warning('⚠️ 未检测到关键元素,但将继续尝试提取数据');
161
  }
162
 
163
+ // 4. 短暂等待让动态内容渲染(可选,根据实际情况调整)
164
  await page.waitForTimeout(500);
165
 
166
  const productData: ProductData = {
 
261
  // 如果直接运行此文件,执行示例
262
  if (import.meta.url === `file://${process.argv[1]}`) {
263
  const exampleUrls = [
264
+ 'https://www.otto.de/p/flieks-etagenbett-kinderbett-hausbett-90x200cm-mit-schoenem-fenster-dach-und-schraegleiter-S03FA0WG/'
265
  ];
266
 
267
+ runOttoCrawler(exampleUrls, { preset: 'stealth' })
268
  .then(results => {
269
  console.log('\n📦 抓取结果:');
270
  console.log(JSON.stringify(results, null, 2));
src/proxy-config.ts CHANGED
@@ -1,47 +1,27 @@
1
  import { ProxyConfiguration } from 'crawlee';
2
- import * as fs from 'fs';
3
- import * as path from 'path';
4
 
5
- // 读取IP代理列表
6
- export function loadProxyUrls(): string[] {
7
- try {
8
- const ipFilePath = path.join(process.cwd(), 'storage', 'ip.txt');
9
- if (!fs.existsSync(ipFilePath)) {
10
- console.warn('⚠️ IP代理文件不存在:', ipFilePath);
11
- return [];
12
- }
13
-
14
- const content = fs.readFileSync(ipFilePath, 'utf-8');
15
- const lines = content.split('\n').map(line => line.trim()).filter(line => line.length > 0);
16
-
17
- // 转换为完整的代理URL格式 (假设是HTTP代理,无认证)
18
- const proxyUrls = lines.map(ipPort => {
19
- // 检查是否已经是完整的URL格式
20
- if (ipPort.startsWith('http://') || ipPort.startsWith('https://')) {
21
- return ipPort;
22
- }
23
- // 否则假设是IP:PORT格式,转换为http://IP:PORT
24
- return `http://${ipPort}`;
25
- });
26
-
27
- console.log(`✅ 成功加载 ${proxyUrls.length} 个代理IP`);
28
- return proxyUrls;
29
- } catch (error) {
30
- console.error('❌ 加载代理IP失败:', error);
31
- return [];
32
- }
33
  }
34
 
35
- // 创建代理配置
36
  export function createProxyConfiguration(): ProxyConfiguration | undefined {
37
- const proxyUrls = loadProxyUrls();
38
-
39
- if (proxyUrls.length === 0) {
40
- console.warn('⚠️ 未找到可用的代理IP,将不使用代理');
41
- return undefined;
42
- }
43
-
44
  return new ProxyConfiguration({
45
- proxyUrls: proxyUrls
46
  });
47
- }
 
 
 
 
 
 
1
  import { ProxyConfiguration } from 'crawlee';
 
 
2
 
3
+ // Dataify 动态住宅代理配置
4
+ const PROXY_HOST = 'eu.dataify.top';
5
+ const PROXY_PORT = '6600';
6
+ const PROXY_USERNAME = 'gqFOVXhf9yd2-r-de';
7
+ const PROXY_PASSWORD = 'm9E0BDFM';
8
+
9
+ // 创建代理URL
10
+ function getProxyUrl(): string {
11
+ return `https://${PROXY_USERNAME}:${PROXY_PASSWORD}@${PROXY_HOST}:${PROXY_PORT}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  }
13
 
14
+ // 创建代理配置(Dataify 动态代理,每次请求自动轮换IP)
15
  export function createProxyConfiguration(): ProxyConfiguration | undefined {
16
+ const proxyUrl = getProxyUrl();
17
+ console.log(`✅ 使用 Dataify 动态代理: ${PROXY_HOST}:${PROXY_PORT}`);
18
+
 
 
 
 
19
  return new ProxyConfiguration({
20
+ proxyUrls: [proxyUrl],
21
  });
22
+ }
23
+
24
+ // 保留向后兼容的函数
25
+ export function loadProxyUrls(): string[] {
26
+ return [getProxyUrl()];
27
+ }