8
n8n 中文网amn8n.com

房产搜索器爬虫助手

高级

这是一个Miscellaneous, AI Chatbot, Multimodal AI领域的自动化工作流,包含 18 个节点。主要使用 If, Set, Code, Merge, HttpRequest 等节点。 使用 PropertyFinder.ae、OpenRouter 和 SerpAPI 通过 AI 回答房地产问题

前置要求
  • 可能需要目标 API 的认证凭证
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "id": "hszZbSQbmoxuYStY",
  "meta": {
    "instanceId": "db306c8f66a7d588518aa1edf828f4170b2b2800a0674fff7140b2961ef113b0",
    "templateCredsSetupCompleted": true
  },
  "name": "房产搜索器爬虫助手",
  "tags": [],
  "nodes": [
    {
      "id": "d1428498-b9d0-40a6-aafe-a85095b2aeb3",
      "name": "查找链接",
      "type": "n8n-nodes-base.code",
      "position": [
        -1968,
        160
      ],
      "parameters": {
        "jsCode": "// This code finds the latest URL from one of two possible inputs.\n\n// Primary input: A simple text string from a specific n8n node.\nconst inputText = $('Chat input').first().json.chatInput;\n\n// Fallback input: A conversation history object.\nconst messages = $input.first().json.messages;\n\nlet lastLink = \"\";\n\n// A regex to find all URLs in a given text.\nconst urlRegex = /(https?:\\/\\/[^\\s\"'<>()]+)/g;\n\n// Function to find the last link in a piece of text.\nconst findLastLinkInText = (text) => {\n  if (typeof text !== 'string') {\n    return \"\";\n  }\n  const matches = text.match(urlRegex);\n  if (matches && matches.length > 0) {\n    // Get the last element from the array of matches.\n    let link = matches[matches.length - 1];\n    // Remove common punctuation from the end of the link.\n    return link.replace(/[,.]+$/, '');\n  }\n  return \"\";\n};\n\n// 1. First, try to find the link in the primary input (inputText).\nlastLink = findLastLinkInText(inputText);\n\n// 2. If no link was found in inputText, and messages array exists, search it.\nif (!lastLink && messages && Array.isArray(messages) && messages.length > 0) {\n  // Iterate backwards through messages to find the most recent human message with a link.\n  for (let i = messages.length - 1; i >= 0; i--) {\n    const humanMessage = messages[i].human;\n    const foundLink = findLastLinkInText(humanMessage);\n    if (foundLink) {\n      lastLink = foundLink;\n      // Once the most recent link is found, stop searching.\n      break;\n    }\n  }\n}\n\n// Return the data in the format n8n expects.\n// The output will be a JSON object: { \"link\": \"http://found-link.com\" }\n// or { \"link\": \"\" } if no link was found in either source.\nreturn [{\n  json: {\n    link: lastLink\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "27e897af-338f-486a-9caf-5bf2632bc9cf",
      "name": "爬取数据",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        -1200,
        -208
      ],
      "parameters": {
        "url": "={{ $('Find link').item.json.link }}",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "2db0d053-522e-4f08-850e-af6b2fcc19ca",
      "name": "总结",
      "type": "n8n-nodes-base.code",
      "position": [
        -1024,
        -208
      ],
      "parameters": {
        "jsCode": "// n8n Code Node\n// This code takes a large HTML string from a property listing, parses it,\n// and outputs a compact, formatted text summary. This summary is ideal for\n// direct use in an AI assistant's prompt, providing a comprehensive overview\n// without extraneous data like links or IDs.\n\n// The input to this node should be a JSON object with a 'data' property containing the HTML string.\n// For example: { \"data\": \"<!DOCTYPE html>...\" }\n\n// The output will be a single item with a 'summary' property containing the formatted string.\n// In case of an error, the 'summary' will state that the link is not supported.\n\n// Import the 'cheerio' library to parse HTML.\nconst cheerio = require('cheerio');\n\n// The main function that will be executed by the n8n node.\nconst [input] = items;\n\n// Wrap the entire logic in a try...catch block for robust error handling.\ntry {\n    // Extract the HTML string from the input data.\n    const html = input.json.data;\n\n    // If HTML is missing, throw an error to be caught below.\n    if (!html) {\n        throw new Error(\"Input data is missing or empty.\");\n    }\n\n    // Load the HTML into cheerio for parsing.\n    const $ = cheerio.load(html);\n\n    // --- Data Extraction ---\n\n    // Extract the main JSON data blob from the '__NEXT_DATA__' script tag.\n    const nextDataScript = $('#__NEXT_DATA__').html();\n    if (!nextDataScript) {\n        throw new Error(\"Could not find '__NEXT_DATA__' script tag. The HTML structure may have changed.\");\n    }\n    const nextData = JSON.parse(nextDataScript);\n    const pageProps = nextData.props.pageProps;\n    const propertyData = pageProps.propertyResult.property;\n    const insights = pageProps.insights;\n    const priceTrends = pageProps.priceTrendsData;\n    const similarTransactions = propertyData.similar_price_transactions;\n\n\n    // Helper function to safely access nested properties\n    const get = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o);\n\n    // --- Text Formatting ---\n\n    // Use template literals to build the formatted text string.\n    let summaryText = `**Property Listing Summary**\\n\\n`;\n\n    // Section: Overview\n    summaryText += `**Overview:**\\n`;\n    // Extract specific property type (e.g., 3M) from the description if available\n    const typeMatch = get(['description'], propertyData)?.match(/Type\\s*(\\w+\\d*)/i);\n    summaryText += `- **Type:** ${get(['property_type'], propertyData) || 'N/A'}${typeMatch ? ` (${typeMatch[0]})` : ''}\\n`;\n    const availabilityDate = get(['rental_availability_date'], propertyData);\n    summaryText += `- **Status:** For Rent, Available from ${availabilityDate ? new Date(availabilityDate).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) : 'N/A'}\\n\\n`;\n\n    // Section: Property Details\n    const plotSize = get(['description'], propertyData)?.match(/Plot\\s*([\\d,]+)\\s*sqft/i);\n    summaryText += `**Property Details:**\\n`;\n    const hasStudy = get(['description'], propertyData)?.toLowerCase().includes('study');\n    summaryText += `- **Bedrooms:** ${get(['bedrooms'], propertyData) || 'N/A'}${hasStudy ? ' + Study' : ''}\\n`;\n    summaryText += `- **Bathrooms:** ${get(['bathrooms'], propertyData) || 'N/A'}\\n`;\n    summaryText += `- **Size (BUA):** ${get(['size', 'value'], propertyData) || 'N/A'} sqft\\n`;\n    if (plotSize && plotSize[1]) {\n        summaryText += `- **Plot Size:** ${plotSize[1].replace(',', '')} sqft\\n`;\n    }\n    summaryText += `- **Furnishing:** ${get(['furnished'], propertyData) || 'N/A'}\\n`;\n    if (get(['description'], propertyData)?.includes('Fully Upgraded')) {\n        summaryText += `- **Condition:** Fully Upgraded with modern finishes\\n`;\n    }\n    summaryText += `\\n`;\n\n    // Section: Location & Community\n    summaryText += `**Location & Community:**\\n`;\n    summaryText += `- **Area:** ${get(['location', 'full_name'], propertyData) || 'N/A'}\\n`;\n    const communityInfo = get(['community'], insights);\n    if (communityInfo) {\n        summaryText += `- **Community Categories:** ${communityInfo.categories.join(', ')}\\n`;\n        summaryText += `- **Community Review:** ${communityInfo.review.score.toFixed(1)}/5 from ${communityInfo.review.count} reviews\\n`;\n    }\n    summaryText += `\\n`;\n\n\n    // Section: Pricing & Market Trends\n    summaryText += `**Pricing & Market Trends:**\\n`;\n    summaryText += `- **Rent:** ${get(['price', 'value'], propertyData)?.toLocaleString() || 'N/A'} ${get(['price', 'currency'], propertyData) || ''} per ${get(['price', 'period'], propertyData) || ''}\\n`;\n    const lastTrend = get(['graph', '1Y'], priceTrends)?.slice(-1)[0];\n    if (lastTrend) {\n        summaryText += `- **Community Avg. Rent (as of ${lastTrend.period}):** ${lastTrend.communityPrice?.toLocaleString() || 'N/A'} AED/year\\n`;\n        summaryText += `- **Sub-Community Avg. Rent (as of ${lastTrend.period}):** ${lastTrend.towerPrice?.toLocaleString() || 'N/A'} AED/year\\n`;\n    }\n    summaryText += `\\n`;\n\n    // Section: Key Features\n    const keyFeaturesMatch = get(['description'], propertyData)?.split('Key Features:')[1];\n    if (keyFeaturesMatch) {\n        const features = keyFeaturesMatch.split('\\n\\n')[0]\n            .split('\\n- ')\n            .map(item => item.trim())\n            .filter(item => item && !item.toLowerCase().includes('property ref'));\n        if (features.length > 0) {\n            summaryText += `**Key Features & Amenities:**\\n`;\n            features.forEach(feature => {\n                summaryText += `- ${feature}\\n`;\n            });\n            summaryText += `\\n`;\n        }\n    }\n\n    // Section: Agent & Broker\n    summaryText += `**Agent & Broker:**\\n`;\n    summaryText += `- **Agent:** ${get(['agent', 'name'], propertyData) || 'N/A'}\\n`;\n    summaryText += `- **Brokerage:** ${get(['broker', 'name'], propertyData) || 'N/A'}\\n`;\n    summaryText += `- **Agent Experience:** ${get(['agent', 'years_of_experience'], propertyData) || 'N/A'} years\\n`;\n    summaryText += `- **Agent Closed Deals:** ${get(['agent', 'transactions_count'], propertyData) || 'N/A'}\\n`;\n    summaryText += `- **Agent Rating:** ${get(['agent', 'avg_score'], propertyData) || 'N/A'}/5 from ${get(['agent', 'review_count'], propertyData) || 'N/A'} ratings\\n`;\n    const phone = get(['contact_options'], propertyData)?.find(c => c.type === 'phone')?.value;\n    const whatsapp = get(['contact_options'], propertyData)?.find(c => c.type === 'whatsapp')?.value;\n    const email = get(['contact_options'], propertyData)?.find(c => c.type === 'email')?.value;\n    if (phone || whatsapp || email) {\n        summaryText += `- **Contact Details:**\\n`;\n        if (phone) summaryText += `  - Phone: ${phone}\\n`;\n        if (whatsapp) summaryText += `  - WhatsApp: ${whatsapp}\\n`;\n        if (email) summaryText += `  - Email: ${email}\\n`;\n    }\n\n\n    // Return the formatted text summary on success.\n    return [{\n        json: {\n            summary: summaryText.trim()\n        }\n    }];\n\n} catch (error) {\n    // If any error occurs during the try block (e.g., parsing fails, data structure changes),\n    // catch it here and return a standardized error message.\n    // This prevents the node from failing and allows the workflow to handle the case gracefully.\n    return [{\n        json: {\n            summary: \"The provided link is not supported\"\n        }\n    }];\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "38eeeba0-d1ec-4afe-bcf9-4303827451a0",
      "name": "合并",
      "type": "n8n-nodes-base.merge",
      "position": [
        -672,
        -48
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "dba1da50-3e5b-4b45-97c6-eace9f60e3db",
      "name": "捕获传入消息",
      "type": "n8n-nodes-base.set",
      "position": [
        -1200,
        -32
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "b87137d3-5c9f-42e6-b447-5ade97f23983",
              "name": "message",
              "type": "string",
              "value": "={{ $('Chat input').item.json.chatInput }}"
            },
            {
              "id": "e3842972-39d2-46d9-8e06-688623b18af5",
              "name": "listing_summary",
              "type": "string",
              "value": ""
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "08bef4c9-9f59-44c6-9abf-9edc039294ce",
      "name": "捕获",
      "type": "n8n-nodes-base.set",
      "position": [
        -848,
        -208
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "b87137d3-5c9f-42e6-b447-5ade97f23983",
              "name": "message",
              "type": "string",
              "value": "={{ $('Chat input').item.json.chatInput }}"
            },
            {
              "id": "e3842972-39d2-46d9-8e06-688623b18af5",
              "name": "listing_summary",
              "type": "string",
              "value": "={{ $json.summary }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "bb16f4f4-3f1e-461b-8012-dc29320a756b",
      "name": "AI 代理",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -48,
        256
      ],
      "parameters": {
        "text": "={{ $json.message }}",
        "options": {
          "systemMessage": "=<role name=\"UAE Real Estate AI Agent\">\n    Your primary function is to assist users with inquiries about specific real estate properties and general real estate topics in the UAE. Provide accurate, relevant information as an expert assistant.\n</role>\n\n<persona>\n    Friendly, concise, and helpful. Your answers must be detailed, providing all necessary information without conversational filler. Get straight to the point.\n</persona>\n\n<knowledge_base>\n{{ $json.listing_summary }}\n</knowledge_base>\n\n<tool name=\"serp_search_tool\" description=\"A search tool for public UAE real estate information.\">\n    <usage>Use ONLY when the `knowledge_base` is insufficient to answer a query.</usage>\n    <critical_rule>You MUST formulate a comprehensive search query by combining the user's question with context from the `knowledge_base` (e.g., property location, type).</critical_rule>\n</tool>\n\n<rules>\n    <rule priority=\"1\">Always consult the `knowledge_base` first.</rule>\n    <rule priority=\"2\">If a user provides a URL, assume the `knowledge_base` has been updated with its content. ALWAYS prioritize this information for your response.</rule>\n    <rule priority=\"3\">If a user sends a URL with no other query, briefly acknowledge the content of the link in one sentence and then ask what specific information they need. (e.g., \"I see this is a 2-bedroom apartment in Dubai Marina. What would you like to know about it?\")</rule>\n    <rule priority=\"4\">If the `knowledge_base` is insufficient, use the `serp_search_tool`.</rule>\n    <rule priority=\"5\">For ambiguous queries (e.g., \"What are service charges?\"), ask for clarification: \"Are you asking about this specific property, or in general for this area?\"</rule>\n    <rule priority=\"6\">For off-topic queries, pivot the conversation back to real estate.</rule>\n</rules>\n\n<output>\n    - Produce clear, detailed user-facing messages.\n    - NEVER output explanations about internal processes, tool usage, or actions.\n    - NEVER include XML tags or meta-information in your final response to the user.\n    - ALWAYS use conversation history to maintain a smooth, natural dialogue.\n</output>"
        },
        "promptType": "define"
      },
      "typeVersion": 2.2
    },
    {
      "id": "7d96371d-57de-41e1-aa8d-3b4f94c33236",
      "name": "简单记忆",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        16,
        480
      ],
      "parameters": {
        "sessionKey": "={{ $('Chat input').item.json.sessionId }}",
        "sessionIdType": "customKey"
      },
      "typeVersion": 1.3
    },
    {
      "id": "9ed6be30-0b8a-452f-9f8d-b61dd9825565",
      "name": "OpenRouter Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        -128,
        448
      ],
      "parameters": {
        "model": "google/gemini-2.5-flash",
        "options": {}
      },
      "credentials": {
        "openRouterApi": {
          "id": "hrSKg3wcHKBURAkN",
          "name": "OpenRouter account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "346df70b-ebe1-496c-84fc-5bb0ceef02a4",
      "name": "SerpAPI",
      "type": "@n8n/n8n-nodes-langchain.toolSerpApi",
      "position": [
        144,
        480
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "serpApi": {
          "id": "DP4MeI2mpTA5Iq70",
          "name": "SerpAPI account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "47efdbc8-11d2-4288-983e-d09df7cb3354",
      "name": "如果消息包含链接",
      "type": "n8n-nodes-base.if",
      "position": [
        -1424,
        -128
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "39b33b64-dfe8-4245-a6cc-be34f48c33db",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.link }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "33ceb55b-5630-4ef3-9a3a-43ae8e49c64e",
      "name": "聊天记忆管理器",
      "type": "@n8n/n8n-nodes-langchain.memoryManager",
      "position": [
        -2320,
        160
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1.1
    },
    {
      "id": "ac0e0cef-44f5-4eb4-9e74-bb753efbc56d",
      "name": "简单记忆1",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        -2256,
        384
      ],
      "parameters": {
        "sessionKey": "={{ $('Chat input').item.json.sessionId }}",
        "sessionIdType": "customKey",
        "contextWindowLength": 30
      },
      "typeVersion": 1.3
    },
    {
      "id": "99afe6a9-cf57-42aa-b453-6ec7b6a176c2",
      "name": "便签7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3344,
        -768
      ],
      "parameters": {
        "width": 476,
        "height": 2032,
        "content": "# 使用 OpenRouter 和 SerpAPI 构建 AI 房地产代理,与来自 propertyfinder.ae 的房产对象对话"
      },
      "typeVersion": 1
    },
    {
      "id": "406164c1-2803-4556-9773-b41a62322968",
      "name": "便签6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2640,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 824,
        "height": 604,
        "content": "## 接收和准备数据"
      },
      "typeVersion": 1
    },
    {
      "id": "6976f31a-359d-477f-baf2-e4b6b1a75bdd",
      "name": "聊天输入",
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "position": [
        -2592,
        160
      ],
      "webhookId": "188cd4ff-f82d-4e24-aca8-245f90063af4",
      "parameters": {
        "public": true,
        "options": {},
        "initialMessages": "Hello!"
      },
      "typeVersion": 1.3
    },
    {
      "id": "cb85801e-8a1b-4469-847d-aaa78d25b502",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1472,
        -528
      ],
      "parameters": {
        "color": 7,
        "width": 936,
        "height": 748,
        "content": "## 爬取房源信息"
      },
      "typeVersion": 1
    },
    {
      "id": "6661e75b-cbb8-44e7-9009-204fe35d1635",
      "name": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -208,
        -288
      ],
      "parameters": {
        "color": 7,
        "width": 536,
        "height": 908,
        "content": "### AI 代理"
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "719e8723-1b77-42a4-9f6a-185f252540c0",
  "connections": {
    "Merge": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape": {
      "main": [
        [
          {
            "node": "Summarize",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Capture": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SerpAPI": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Find link": {
      "main": [
        [
          {
            "node": "If msg contains link",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Summarize": {
      "main": [
        [
          {
            "node": "Capture",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chat input": {
      "main": [
        [
          {
            "node": "Chat Memory Manager",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Simple Memory": {
      "ai_memory": [
        [
          {
            "node": "AI Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Simple Memory1": {
      "ai_memory": [
        [
          {
            "node": "Chat Memory Manager",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Chat Memory Manager": {
      "main": [
        [
          {
            "node": "Find link",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If msg contains link": {
      "main": [
        [
          {
            "node": "Scrape",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Capture Incoming Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Capture Incoming Message": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

高级 - 杂项, AI 聊天机器人, 多模态 AI

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
高级
节点数量18
分类3
节点类型12
难度说明

适合高级用户,包含 16+ 个节点的复杂工作流

作者
George Zargaryan

George Zargaryan

@zrgrn

Building AI Systems & Multi-Agent Assistants incorporating data collection, intent classification, human handoff, RAG, and API-driven CRM actions. 10+ years in software development including iOS and Android. Now in the process of founding my AI Automation Platform - Agenza AI.

外部链接
在 n8n.io 查看

分享此工作流