{
  "version": "1.0.0",
  "exported_at": "2026-06-01T19:00:00.000Z",
  "project": {
    "name": "Lamudi Listing Scraper",
    "description": "Scrapes Lamudi México listing pages such as https://www.lamudi.com.mx/sinaloa/mazatlan/for-sale/ and exports Octoparse-equivalent fields: title, URL, address, description, price, bedrooms, bathrooms, built area, image, agent, member-since and WhatsApp/contact. Navigation is handled as a best-effort load-all step: the browser scrolls and clicks visible Cargar más / Ver más controls if present, then creates a visible virtual row list with one row per unique Lamudi /detalle/ URL to avoid duplicate rows caused by multiple anchors per property card. The Octoparse sample detail URLs currently return Lamudi 404 pages, and Lamudi may use anti-bot/CAPTCHA or layout changes.",
    "color": "bg-[#4589ff]",
    "template_id": "ai-generated"
  },
  "blocks": [
    {
      "block_id": "set-window-size-1",
      "block_type": "process",
      "title": "Set Window Size",
      "description": "Set browser window dimensions",
      "position_x": 120,
      "position_y": 260,
      "config": {
        "width": 1920,
        "height": 1080
      }
    },
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 480,
      "position_y": 260,
      "config": {
        "url": "https://www.lamudi.com.mx/sinaloa/mazatlan/for-sale/",
        "color": "bg-[#4589ff]"
      }
    },
    {
      "block_id": "wait-for-page-load-1",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for page to finish loading",
      "position_x": 840,
      "position_y": 260,
      "config": {
        "timeout": 30
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Allow initial Lamudi listing content to render",
      "position_x": 1200,
      "position_y": 260,
      "config": {
        "duration": 2
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Load listings and build visible unique scraper rows",
      "position_x": 1560,
      "position_y": 260,
      "config": {
        "jsCode": "(async () => {\n  const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));\n  const norm = (s) => (s || '').replace(/\\s+/g, ' ').trim();\n  const visible = (el) => {\n    if (!el) return false;\n    const r = el.getBoundingClientRect();\n    const s = getComputedStyle(el);\n    return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && s.display !== 'none';\n  };\n  const detailHref = (a) => {\n    try {\n      const href = a.href || a.getAttribute('href') || '';\n      if (!href || !href.includes('/detalle/')) return '';\n      const u = new URL(href, location.href);\n      u.hash = '';\n      return u.href;\n    } catch (_) {\n      return '';\n    }\n  };\n  const bestCardForAnchor = (a) => {\n    let best = a;\n    let bestScore = -1;\n    let el = a;\n    for (let i = 0; el && i < 10 && el !== document.body; i++, el = el.parentElement) {\n      const txt = norm(el.textContent);\n      const hasDetail = !!el.querySelector(\"a[href*='/detalle/']\");\n      if (!hasDetail) continue;\n      let score = 0;\n      if (/\\$\\s?[\\d.,]+\\s*(MXN|M\\.N\\.)?/i.test(txt)) score += 1000;\n      if (/m²/i.test(txt)) score += 200;\n      if (/rec[aá]maras?|habitaciones?|baños?/i.test(txt)) score += 200;\n      if (el.querySelector('img')) score += 100;\n      if (el.matches(\"[data-test*='listing'], [data-testid*='listing'], [class*='Listing'], [class*='listing'], [class*='property'], [class*='Property'], article, li\")) score += 300;\n      score += Math.min(txt.length, 1500) / 10;\n      if (score > bestScore && txt.length < 8000) {\n        best = el;\n        bestScore = score;\n      }\n    }\n    return best;\n  };\n  const extractFrom = (href, anchor, card) => {\n    const linksForHref = Array.from(card.querySelectorAll(\"a[href*='/detalle/']\")).filter(x => detailHref(x) === href);\n    const bestTextLink = linksForHref\n      .filter(x => norm(x.textContent || x.getAttribute('title')).length > 8)\n      .sort((a, b) => norm(b.textContent || b.getAttribute('title')).length - norm(a.textContent || a.getAttribute('title')).length)[0] || anchor;\n    const h = card.querySelector('h1,h2,h3,[data-test*=title],[data-testid*=title],[class*=title],[class*=Title]');\n    const hText = norm(h && h.textContent);\n    const linkText = norm(bestTextLink.textContent || bestTextLink.getAttribute('title'));\n    const title = /^\\d[\\d,.]*\\s+Inmuebles/i.test(hText) ? linkText : (hText || linkText);\n    const fullText = norm(card.textContent);\n    const loc = card.querySelector('[data-test*=location],[data-testid*=location],[class*=location],[class*=Location],[class*=address],[class*=Address]');\n    const desc = card.querySelector('[data-test*=description],[data-testid*=description],[class*=description],[class*=Description],[class*=summary],[class*=Summary],p');\n    const priceEl = card.querySelector('[data-test*=price],[data-testid*=price],[class*=price],[class*=Price]');\n    const priceText = norm((priceEl && priceEl.textContent) || fullText);\n    const price = (priceText.match(/\\$\\s?[\\d.,]+\\s*(?:MXN|M\\.N\\.)?/i) || [''])[0];\n    const rec = (fullText.match(/(?:Rec[aá]maras?|Habitaciones?)\\D{0,20}(\\d+)/i) || fullText.match(/(\\d+)\\s*(?:rec[aá]maras?|habitaciones?|hab\\.?)/i) || [null, ''])[1] || '';\n    const banos = (fullText.match(/(?:Baños?|número_de_baños)\\D{0,20}(\\d+(?:\\.5)?)/i) || fullText.match(/(\\d+(?:\\.5)?)\\s*(?:baños?|baths?)/i) || [null, ''])[1] || '';\n    const area = (fullText.match(/(?:Superficie construida|Construida|Construcción)\\D{0,30}(\\d[\\d.,]*\\s*m²)/i) || fullText.match(/(\\d[\\d.,]*\\s*m²)/i) || [null, ''])[1] || '';\n    const img = card.querySelector('img');\n    const srcset = img ? (img.getAttribute('srcset') || '') : '';\n    const imgUrl = img ? (img.currentSrc || img.src || img.getAttribute('data-src') || img.getAttribute('data-lazy-src') || img.getAttribute('src') || (srcset ? srcset.split(',')[0].trim().split(' ')[0] : '')) : '';\n    const agentEl = card.querySelector('[data-test*=agent],[data-testid*=agent],[class*=agent],[class*=Agent],[class*=agency],[class*=Agency],[class*=publisher],[class*=Publisher],[class*=broker],[class*=Broker]');\n    const agentRaw = norm(agentEl && agentEl.textContent);\n    const agent = agentRaw.replace(/^Contactar\\s+/i, '').replace(/\\s*Miembro\\s+desde\\s+[\\d,.]+.*$/i, '').trim();\n    const miembro = (fullText.match(/Miembro\\s+desde\\s+[\\d,.]+/i) || [''])[0];\n    const contactLink = card.querySelector(\"a[href*='wa.me'],a[href*='whatsapp'],a[href^='tel:'],a[href^='mailto:']\");\n    const phone = (fullText.match(/\\+?52\\s?\\d[\\d\\s-]{8,}/) || [''])[0].replace(/\\s+/g, '');\n    const email = (fullText.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i) || [''])[0];\n    const whatsapp = contactLink ? norm((contactLink.getAttribute('href') || contactLink.textContent || '').replace(/^tel:|^mailto:/, '')) : (phone || email);\n    return {\n      title: norm(title),\n      url: href,\n      address: norm(loc && loc.textContent),\n      description: norm(desc && desc.textContent),\n      price: norm(price),\n      bedrooms: norm(rec),\n      bathrooms: norm(banos),\n      area: norm(area),\n      img: norm(imgUrl),\n      agent: norm(agent),\n      member: norm(miembro),\n      whatsapp: norm(whatsapp)\n    };\n  };\n  const buildRows = () => {\n    let box = document.querySelector('#uscraper-lamudi-rows');\n    if (!box) {\n      box = document.createElement('div');\n      box.id = 'uscraper-lamudi-rows';\n      box.style.cssText = 'display:block !important; visibility:visible !important; position:relative; z-index:2147483647; background:#fff; color:#000; padding:4px; min-height:20px;';\n      document.body.insertBefore(box, document.body.firstChild);\n    }\n    box.innerHTML = '';\n    const seen = new Set();\n    let n = 0;\n    for (const a of Array.from(document.querySelectorAll(\"a[href*='/detalle/']\"))) {\n      const href = detailHref(a);\n      if (!href || seen.has(href)) continue;\n      seen.add(href);\n      const card = bestCardForAnchor(a);\n      const d = extractFrom(href, a, card);\n      if (!d.url) continue;\n      n += 1;\n      const row = document.createElement('div');\n      row.className = 'uscraper-lamudi-row';\n      row.textContent = d.title || d.url;\n      row.style.cssText = 'display:block !important; visibility:visible !important; min-height:1px; height:auto; overflow:visible;';\n      row.setAttribute('data-title', d.title);\n      row.setAttribute('data-url', d.url);\n      row.setAttribute('data-address', d.address);\n      row.setAttribute('data-description', d.description);\n      row.setAttribute('data-price', d.price);\n      row.setAttribute('data-bedrooms', d.bedrooms);\n      row.setAttribute('data-bathrooms', d.bathrooms);\n      row.setAttribute('data-area', d.area);\n      row.setAttribute('data-img', d.img);\n      row.setAttribute('data-agent', d.agent);\n      row.setAttribute('data-member', d.member);\n      row.setAttribute('data-whatsapp', d.whatsapp);\n      box.appendChild(row);\n    }\n    return n;\n  };\n  buildRows();\n  let last = document.querySelectorAll('#uscraper-lamudi-rows .uscraper-lamudi-row').length;\n  let stable = 0;\n  for (let i = 0; i < 20; i++) {\n    const controls = Array.from(document.querySelectorAll('button, a[href]')).filter(el => {\n      if (!visible(el)) return false;\n      const href = el.getAttribute('href') || '';\n      if (href.includes('/detalle/')) return false;\n      const label = `${el.textContent || ''} ${el.getAttribute('aria-label') || ''} ${el.getAttribute('title') || ''}`.toLowerCase();\n      return /cargar\\s*m[aá]s|ver\\s*m[aá]s|mostrar\\s*m[aá]s|load\\s*more|show\\s*more/.test(label);\n    });\n    if (controls[0]) {\n      try { controls[0].click(); } catch (_) {}\n      await delay(1000);\n    }\n    window.scrollTo(0, document.body.scrollHeight);\n    await delay(700);\n    const now = buildRows();\n    if (now > last) { last = now; stable = 0; } else { stable += 1; }\n    if (stable >= 4) break;\n  }\n  window.scrollTo(0, 0);\n  return document.querySelectorAll('#uscraper-lamudi-rows .uscraper-lamudi-row').length;\n})();",
        "waitForCompletion": true,
        "timeout": 90
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until generated unique scraper rows exist",
      "position_x": 1920,
      "position_y": 260,
      "config": {
        "selector": "#uscraper-lamudi-rows .uscraper-lamudi-row",
        "timeout": 30,
        "visible": true
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export generated unique Lamudi listing rows",
      "position_x": 2280,
      "position_y": 260,
      "config": {
        "rowSelector": "#uscraper-lamudi-rows .uscraper-lamudi-row",
        "fileName": "lamudi-listados-scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "create",
        "columns": [
          {
            "name": "titulo",
            "selector": "",
            "attribute": "data-title"
          },
          {
            "name": "url",
            "selector": "",
            "attribute": "data-url"
          },
          {
            "name": "direccion",
            "selector": "",
            "attribute": "data-address"
          },
          {
            "name": "descripcion",
            "selector": "",
            "attribute": "data-description"
          },
          {
            "name": "precio",
            "selector": "",
            "attribute": "data-price"
          },
          {
            "name": "recamaras",
            "selector": "",
            "attribute": "data-bedrooms"
          },
          {
            "name": "numero_de_banos",
            "selector": "",
            "attribute": "data-bathrooms"
          },
          {
            "name": "superficie_construida",
            "selector": "",
            "attribute": "data-area"
          },
          {
            "name": "img",
            "selector": "",
            "attribute": "data-img"
          },
          {
            "name": "agente",
            "selector": "",
            "attribute": "data-agent"
          },
          {
            "name": "miembro_desde",
            "selector": "",
            "attribute": "data-member"
          },
          {
            "name": "whatsapp",
            "selector": "",
            "attribute": "data-whatsapp"
          }
        ]
      }
    },
    {
      "block_id": "end-1",
      "block_type": "output",
      "title": "End",
      "description": "Terminate execution flow",
      "position_x": 2640,
      "position_y": 260,
      "config": {}
    }
  ],
  "connections": [
    {
      "from_block_id": "set-window-size-1",
      "from_connector_id": "right",
      "to_block_id": "navigate-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "navigate-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-page-load-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-page-load-1",
      "from_connector_id": "right",
      "to_block_id": "sleep-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-1",
      "from_connector_id": "right",
      "to_block_id": "inject-javascript-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-element-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-1",
      "from_connector_id": "right",
      "to_block_id": "structured-export-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "structured-export-1",
      "from_connector_id": "right",
      "to_block_id": "end-1",
      "to_connector_id": "left"
    }
  ],
  "canvas_elements": [
    {
      "id": "group-entry",
      "element_type": "group",
      "title": "Entry & Setup",
      "color": "#4589ff",
      "position_x": 48,
      "position_y": 156,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "set-window-size-1"
        ]
      }
    },
    {
      "id": "group-load",
      "element_type": "group",
      "title": "Page Load",
      "color": "#08bdba",
      "position_x": 408,
      "position_y": 156,
      "width": 1760,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "sleep-1",
          "wait-for-element-1"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 1488,
      "position_y": 156,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "inject-javascript-1"
        ]
      }
    },
    {
      "id": "group-extract",
      "element_type": "group",
      "title": "Data Extraction",
      "color": "#42be65",
      "position_x": 2208,
      "position_y": 156,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "structured-export-1"
        ]
      }
    },
    {
      "id": "group-control",
      "element_type": "group",
      "title": "Control Flow",
      "color": "#8d8d8d",
      "position_x": 2568,
      "position_y": 156,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "end-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Scrapes Lamudi México listing pages such as https://www.lamudi.com.mx/sinaloa/mazatlan/for-sale/ and exports Octoparse-equivalent fields: title, URL, address, description, price, bedrooms, bathrooms, built area, image, agent, member-since and WhatsApp/contact. Navigation is handled as a best-effort load-all step: the browser scrolls and clicks visible Cargar más / Ver más controls if present, then creates a visible virtual row list with one row per unique Lamudi /detalle/ URL to avoid duplicate rows caused by multiple anchors per property card. The Octoparse sample detail URLs currently return Lamudi 404 pages, and Lamudi may use anti-bot/CAPTCHA or layout changes.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-inject-javascript-1",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `(async () => {\n  const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));\n  const norm...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 1760,
      "position_y": 240,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "inject-javascript-1"
      }
    },
    {
      "id": "note-block-structured-export-1",
      "element_type": "note",
      "title": "Note: Structured Export",
      "content": "Extracts rows matching `#uscraper-lamudi-rows .uscraper-lamudi-row`. Confirm row count > 0 before running at scale.",
      "color": "#ee5396",
      "position_x": 2480,
      "position_y": 240,
      "width": 340,
      "height": 118,
      "z_index": 22,
      "data": {
        "block_id": "structured-export-1"
      }
    }
  ]
}