基于Puppeteer的墨西哥房产网站爬虫运行缓慢求助排查
问题:墨西哥房产网站爬虫速度过慢排查
我用Node.js的Puppeteer开发了针对墨西哥房产网站vivanuncios.com.mx的爬虫,但本地运行时速度极慢。尝试并发处理后速度更差,检查浏览器网络面板也没找到能获取所需信息的API接口。附上完整代码,求有经验的开发者帮忙排查问题:
const mysql = require('mysql2'); const puppeteer = require('puppeteer-extra'); const StealthPlugin = require('puppeteer-extra-plugin-stealth'); puppeteer.use(StealthPlugin()); function delay(time) { return new Promise(function(resolve) { setTimeout(resolve, time) }); } async function createInmueble(page, info){ return await page.evaluate((info) => { let inmueble = {}; inmueble.titulo = info.title; inmueble.precios = info?.priceOperationTypes?.[0]?.prices?.[0]?.amount.toString() || document.querySelector(".price-items").firstElementChild.textContent.trim().split(" ")[1]; inmueble.moneda = info?.priceOperationTypes?.[0]?.prices?.[0]?.isoCode || document.querySelector(".price-items").firstElementChild.textContent.trim().split(" ")[0]; inmueble.inmobiliaria = info?.publisher?.name || null; inmueble.tipo = info?.realEstateType?.name || null; inmueble.estado = info?.postingLocation?.location?.parent?.parent?.name || null; inmueble.municipio = info?.postingLocation?.location?.parent?.name || null; inmueble.zona = info?.postingLocation?.location?.name || null; inmueble.latitud = info?.postingLocation?.postingGeolocation?.geolocation?.latitude.toString() || null inmueble.longitud = info?.postingLocation?.postingGeolocation?.geolocation?.longitude.toString() || null inmueble.imagenes = info?.pictures?.map(picture => picture.url1200x1200) || null; inmueble.metros_totales = info?.mainFeatures?.CFT100?.value || "0"; inmueble.metros_construidos = info?.mainFeatures?.CFT101?.value || "0"; inmueble.banios = info?.mainFeatures?.CFT3?.value || "0"; inmueble.estacionamientos = info?.mainFeatures?.CFT7?.value || "0"; inmueble.recamaras = info?.mainFeatures?.CFT2?.value || "0"; inmueble.antiguedad = info?.mainFeatures?.CFT5?.value || "0"; inmueble.medio_banio = info?.mainFeatures?.CFT4?.value || "0"; inmueble.direcciones = info?.postingLocation?.address?.name || null; inmueble.new_latitud = inmueble?.latitud || null; inmueble.new_longitud = inmueble?.longitud || null; inmueble.url = info?.seoAttributes?.canonical || null; inmueble.telefono = info?.whatsApp; inmueble.nombre_asesor = null; inmueble.email = null; return inmueble; }, info); } async function getInfoInmueble(page){ console.log(page.url()); const info = await page.evaluate(() => { return POSTING; }); return info; } async function scopeEnlaces(page, enlaces){ let info; let inmuebles = []; for(const enlace of enlaces){ try{ await page.goto(enlace); info = await getInfoInmueble(page); const inmueble = await createInmueble(page, info); inmuebles.push(inmueble); } catch(err){ console.error("error", enlace); } } return inmuebles; } async function getEnlaces(page){ const enlaces = await page.evaluate(() => { try{ let div_padre = document.querySelector(".postings-container"); const div_hijo = Array.from(div_padre.children); const enlaces = div_hijo.map(div => { const link_base = "https://www.vivanuncios.com.mx"; const div_enlace = div.firstElementChild; const enlace = div_enlace.getAttribute("data-to-posting"); return link_base+enlace; }).filter(enlace => enlace !== null); return enlaces; } catch(err){ console.log("Error en page:", err); return []; } }); return enlaces; } async function getInmuebles(page, url) { await page.goto(url); const enlaces = await getEnlaces(page); return await scopeEnlaces(page, enlaces); } async function postInmueble(inmueble){ let query = `INSERT INTO inmueblesvivanuncio (titulo, precios, moneda, inmobiliaria, tipo, estado, municipio, zona, latitud, longitud, imagenes, metros_totales, metros_construidos, banios, estacionamientos, recamaras, antiguedad, medio_banio, direcciones, new_latitud, new_longitud, url, telefono, nombre_asesor, email) VALUES ('${inmueble.titulo}', '${inmueble.precios}', '${inmueble.moneda}', '${inmueble.inmobiliaria}', '${inmueble.tipo}', '${inmueble.estado}', '${inmueble.municipio}', '${inmueble.zona}', '${inmueble.latitud}', '${inmueble.longitud}', '${JSON.stringify(inmueble.imagenes)}', '${inmueble.metros_totales}', '${inmueble.metros_construidos}', '${inmueble.banios}', '${inmueble.estacionamientos}', '${inmueble.recamaras}', '${inmueble.antiguedad}', '${inmueble.medio_banio}', '${inmueble.direcciones}', '${inmueble.new_latitud}', '${inmueble.new_longitud}', '${inmueble.url}', '${inmueble.telefono}', '${inmueble.nombre_asesor}', '${inmueble.email}')`; connection.query(query, (error, results, fields) => { if (error){ console.log("error", error); } else{ console.log('Fila insertada con éxito'); } }); } async function scopeInmuebles(all_inmuebles){ await all_inmuebles.forEach(async inmueble => { try{ await postInmueble(inmueble); } catch(err){ console.error("surgio un error"); } }); } async function getBotonSiguiente(page){ const boton = await page.evaluate(() => { const boton = document.querySelector(".sc-n5babu-2.kNwjYM"); if(boton){ return true; } return false; }); return boton } async function scanPage(page){ const BASE_URL = 'https://www.vivanuncios.com.mx/s-casas-en-venta'; let estado = "chihuahua"; let i = 16; let all_inmuebles = []; let boton; do{ let url = `${BASE_URL}/${estado}/page-${i}`; console.log("Pagina nueva: ", url, "\n"); const inmuebles = await getInmuebles(page, url); all_inmuebles = all_inmuebles.concat(inmuebles); await page.goto(url); if(i%5 == 0){ await scopeInmuebles(all_inmuebles); all_inmuebles = []; } boton = await getBotonSiguiente(page); if (boton) { i++; } else { console.log("No hay boton"); await scopeInmuebles(all_inmuebles); } } while(boton && i <= 1000); return all_inmuebles; } async function scrapeo(){ const browser = await puppeteer.launch({headless: false, executablePath: "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"}); const page = await browser.newPage(); const inmuebles = await scanPage(page); await browser.close(); return inmuebles; } async function main(){ const all_inmuebles = await scrapeo(); connection.end(); } const connection = mysql.createConnection({ host: '127.0.0.1', user: 'root', password: 'CocacolA&_2016', database: 'InmobiliariaPrueba' }); connection.connect(); main();
核心问题与优化方案
1. 单页面串行访问是速度瓶颈
当前用单个页面逐个打开房源链接,每个链接的加载、渲染都要耗时,这是最慢的模式。改成多页面并发处理,用浏览器创建多个页面同时处理不同房源,但要控制并发数(建议4-6个),避免触发反爬:
// 改写scopeEnlaces为并发模式,限制并发数 async function scopeEnlaces(browser, enlaces, concurrency = 4) { let inmuebles = []; for (let i = 0; i < enlaces.length; i += concurrency) { const batch = enlaces.slice(i, i + concurrency); const promises = batch.map(async (enlace) => { const page = await browser.newPage(); try { await page.goto(enlace); const info = await getInfoInmueble(page); const inmueble = createInmueble(null, info); // 直接用info生成,无需再调用page.evaluate return inmueble; } catch (err) { console.error("error", enlace); return null; } finally { await page.close(); } }); const results = await Promise.all(promises); inmuebles.push(...results.filter(Boolean)); } return inmuebles; }
2. 禁用不必要的资源加载
Puppeteer默认加载所有资源(图片、CSS、视频等),极大拖慢速度。在页面初始化时设置拦截:
// 在创建页面后添加拦截逻辑 await page.setRequestInterception(true); page.on('request', (req) => { const blockedTypes = ['image', 'stylesheet', 'font', 'media', 'script']; // 只保留必要的HTML和关键JS if (blockedTypes.includes(req.resourceType()) && !req.url().includes('POSTING')) { req.abort(); } else { req.continue(); } });
3. 数据库插入逻辑错误
scopeInmuebles中用forEach+async/await无效,forEach不会等待异步操作完成,导致大量并发数据库请求。改用批量插入或for...of控制并发:
// 批量插入示例 async function postInmueblesBatch(inmuebles) { const values = inmuebles.map(inmueble => `('${inmueble.titulo}', '${inmueble.precios}', ...)`).join(','); const query = `INSERT INTO inmueblesvivanuncio (...) VALUES ${values}`; await connection.promise().query(query); }
4. 重复的页面跳转
scanPage中调用getInmuebles已经执行了page.goto(url),之后又重复跳转,直接去掉第二次page.goto(url)即可。
5. 启用高效无头模式
当前用headless: false可视化浏览器,速度远低于无头模式。改成新无头模式并添加优化参数:
const browser = await puppeteer.launch({ headless: 'new', executablePath: "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe", args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-extensions' ] });
6. 去掉冗余的DOM查询
已经从页面拿到POSTING全局变量,createInmueble中的DOM查询完全多余,直接从info提取数据即可,无需再调用page.evaluate。
内容的提问来源于stack exchange,提问作者Eduardo Cardoza
相关产品推荐
相关产品推荐

