房产搜索器爬虫助手
高级
这是一个Miscellaneous, AI Chatbot, Multimodal AI领域的自动化工作流,包含 18 个节点。主要使用 If, Set, Code, Merge, HttpRequest 等节点。 使用 PropertyFinder.ae、OpenRouter 和 SerpAPI 通过 AI 回答房地产问题
前置要求
- •可能需要目标 API 的认证凭证
使用的节点 (18)
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 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)可能需要您自行付费。
相关工作流推荐
Chatwoot 多通道 AI 助手演示
使用Chatwoot和OpenRouter构建多通道客户支持AI助手
If
Set
Code
+5
13 节点George Zargaryan
杂项
✨🩷自动化社交媒体内容发布工厂 + 系统提示组合
基于动态系统提示和GPT-4o的AI驱动多平台社交媒体内容工厂
If
Set
Code
+20
100 节点Amit Mehta
内容创作
Telegram论坛脉搏:使用Gemini和Groq AI模型的社区监控
Telegram论坛脉搏:使用Gemini和Groq AI模型的社区监控
If
Set
Code
+13
59 节点Nguyen Thieu Toan
杂项
Dream 100 研究代理
使用 Perplexity AI 研究和 Google Sheets 生成 Dream 100 潜在客户列表
If
Set
Slack
+11
29 节点Brandon True
杂项
✨🩷自动化社交媒体内容发布工厂 + 系统提示组合
使用GPT-4o为6个平台生成平台优化的社交媒体内容
If
Set
Code
+20
100 节点Luan Correia
社交媒体
多智能体Telegram机器人
使用Telegram和Google Gemini的多智能体个人助手机器人
Set
Code
Merge
+18
85 节点Akil A
杂项
工作流信息
难度等级
高级
节点数量18
分类3
节点类型12
作者
George Zargaryan
@zrgrnBuilding 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 查看 →
分享此工作流