{
  "version": "1.0.0",
  "exported_at": "2026-06-01T00:00:00.000Z",
  "project": {
    "name": "Airbnb Japan Review Details Scraper",
    "description": "Best-effort Airbnb Japan review-details scraper inferred from the Octoparse template. Processes Airbnb.jp listing/review URLs via navigate.urls[] and appends only strict real review rows to CSV. For each URL, the template attempts Airbnb review API extraction, strict embedded JSON review-object traversal, and DOM/modal review extraction. It deliberately does not export diagnostic or amenity rows as reviews. If Airbnb blocks automation, requires login/CAPTCHA, changes its API/React DOM, or the supplied listing has no accessible public reviews, the run may export zero rows. Replace the default sample URL with target Airbnb Japan room or room/reviews URLs for production.",
    "color": "bg-[#ff5a5f]",
    "template_id": "ai-generated"
  },
  "blocks": [
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 120,
      "position_y": 220,
      "config": {
        "urls": [
          "https://www.airbnb.jp/rooms/50633275/reviews"
        ],
        "color": "bg-[#ff5a5f]",
        "tags": [
          "airbnb",
          "japan",
          "reviews",
          "multi-url",
          "strict-review-only"
        ]
      }
    },
    {
      "block_id": "wait-for-page-load-1",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for page to finish loading",
      "position_x": 456,
      "position_y": 220,
      "config": {
        "timeout": 45,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 792,
      "position_y": 220,
      "config": {
        "duration": 8,
        "color": "bg-[#8d8d8d]"
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 1128,
      "position_y": 220,
      "config": {
        "jsCode": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const buttons = Array.from(document.querySelectorAll('button, [role=\"button\"], a[role=\"button\"]')); const consent = buttons.find(el => /^(Accept|I agree|Agree|OK|同意|同意する|承諾|許可|すべて許可|続行|閉じる)/i.test(clean(el.textContent || el.innerText))); if (consent) { consent.click(); return 'clicked consent/cookie button'; } return 'no consent/cookie button detected'; })();",
        "waitForCompletion": true,
        "timeout": 10,
        "color": "bg-[#a56eff]"
      }
    },
    {
      "block_id": "sleep-2",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1464,
      "position_y": 220,
      "config": {
        "duration": 3,
        "color": "bg-[#8d8d8d]"
      }
    },
    {
      "block_id": "inject-javascript-2",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 1800,
      "position_y": 220,
      "config": {
        "jsCode": "(async () => { const sleep = ms => new Promise(r => setTimeout(r, ms)); const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const hash = s => { let h = 0; s = String(s || ''); for (let i = 0; i < s.length; i++) { h = ((h << 5) - h) + s.charCodeAt(i); h |= 0; } return Math.abs(h).toString(36); }; document.querySelectorAll('.uscraper-review-row').forEach(e => e.remove()); let container = document.querySelector('#uscraper-normalized-airbnb-reviews'); if (!container) { container = document.createElement('div'); container.id = 'uscraper-normalized-airbnb-reviews'; container.style.display = 'none'; document.body.appendChild(container); } const getListingId = () => { const hrefMatch = location.href.match(/\\/rooms\\/(\\d{5,})/); if (hrefMatch) return hrefMatch[1]; const linkMatch = Array.from(document.querySelectorAll('a[href*=\"/rooms/\"]')).map(a => a.href.match(/\\/rooms\\/(\\d{5,})/)).find(Boolean); return linkMatch ? linkMatch[1] : ''; }; const listingId = getListingId(); const listingUrl = listingId ? 'https://www.airbnb.jp/rooms/' + listingId : location.href.split('?')[0]; const badTextRe = /敷地内にあるオープンスペース|amenit|設備|駐車場|Wi-?Fi|キッチン|寝室|ベッド|バスルーム|キャンセル|ハウスルール|チェックイン|チェックアウト|予約|料金|清掃|セルフチェックイン|AirCover|滞在|ホスト|所在地|地図|返金|追加料金|安全|アクセシビリティ/i; const dateRe = /(20\\d{2}\\s*年\\s*\\d{1,2}\\s*月(?:\\s*\\d{1,2}\\s*日)?|\\d{4}\\s*年|January\\s+\\d{4}|February\\s+\\d{4}|March\\s+\\d{4}|April\\s+\\d{4}|May\\s+\\d{4}|June\\s+\\d{4}|July\\s+\\d{4}|August\\s+\\d{4}|September\\s+\\d{4}|October\\s+\\d{4}|November\\s+\\d{4}|December\\s+\\d{4}|Jan\\.?\\s+\\d{4}|Feb\\.?\\s+\\d{4}|Mar\\.?\\s+\\d{4}|Apr\\.?\\s+\\d{4}|Jun\\.?\\s+\\d{4}|Jul\\.?\\s+\\d{4}|Aug\\.?\\s+\\d{4}|Sep\\.?\\s+\\d{4}|Oct\\.?\\s+\\d{4}|Nov\\.?\\s+\\d{4}|Dec\\.?\\s+\\d{4})/i; const isValidReviewText = text => { text = clean(text); if (text.length < 35 || text.length > 3000) return false; if (badTextRe.test(text)) return false; if (/NO_REVIEW_ROWS_EXTRACTED|diagnostic/i.test(text)) return false; return true; }; const addRow = data => { const text = clean(data.review || data.comments || data.text || ''); if (!isValidReviewText(text)) return false; const reviewerName = clean(data.reviewer_name || data.reviewer || ''); const submissionDate = clean(data.submission_date || data.created_at || data.localized_date || ''); const method = clean(data.extraction_method || 'unknown'); const reviewId = clean(data.review_id || data.id || ('generated-' + hash([listingId, reviewerName, submissionDate, text, method].join('|')))); if (!reviewId) return false; const existing = Array.from(container.querySelectorAll('.uscraper-review-row')).some(r => r.getAttribute('data-review-id') === reviewId || r.getAttribute('data-review-text') === text); if (existing) return false; const row = document.createElement('div'); row.className = 'uscraper-review-row'; row.setAttribute('data-review-id', reviewId); row.setAttribute('data-submission-date', submissionDate); row.setAttribute('data-review-text', text); row.setAttribute('data-reviewer-name', reviewerName); row.setAttribute('data-rating', data.rating == null ? '' : clean(String(data.rating))); row.setAttribute('data-listing-id', clean(data.listing_id || listingId || '')); row.setAttribute('data-listing-url', clean(data.listing_url || listingUrl)); row.setAttribute('data-source-url', location.href); row.setAttribute('data-extraction_method', method); row.setAttribute('data-row-index', String(container.querySelectorAll('.uscraper-review-row').length + 1)); container.appendChild(row); return true; }; const apiKeys = ['d306zoyjsyarp7ifhu67rjxn52tv0t20', '3092nxybyb0otqw18e8nh5nty']; const looksLikeReviewObject = obj => { if (!obj || typeof obj !== 'object') return false; const text = clean(obj.comments || obj.localized_comments || obj.reviewText || obj.review_text || obj.text || ''); if (!isValidReviewText(text)) return false; const hasReviewer = !!(obj.reviewer || obj.author || obj.reviewer_name || obj.reviewerName || obj.reviewer_id); const hasDate = !!(obj.localized_date || obj.created_at || obj.submitted_at || obj.date || obj.createdAt); const hasId = !!(obj.id || obj.review_id || obj.reviewId); return hasReviewer && (hasDate || hasId); }; const normalizeReviewObject = (r, method) => { const reviewer = r.reviewer || r.author || {}; return addRow({ review_id: r.id || r.review_id || r.reviewId, submission_date: r.localized_date || r.created_at || r.createdAt || r.submitted_at || r.date, review: r.comments || r.localized_comments || r.reviewText || r.review_text || r.text, reviewer_name: reviewer.first_name || reviewer.name || r.reviewer_name || r.reviewerName, rating: r.rating || r.average_rating || '', listing_id: listingId, listing_url: listingUrl, extraction_method: method }); }; const tryApi = async () => { if (!listingId) return 0; let added = 0; for (const key of apiKeys) { for (const origin of [location.origin, 'https://www.airbnb.jp', 'https://www.airbnb.com']) { let offset = 0; for (let page = 0; page < 8; page++) { const url = origin + '/api/v2/pdp_listing_reviews?_format=for_p3&_limit=50&_offset=' + offset + '&listing_id=' + encodeURIComponent(listingId) + '&role=guest&key=' + encodeURIComponent(key) + '&locale=ja&currency=JPY'; try { const res = await fetch(url, { credentials: 'include', headers: { accept: 'application/json,text/plain,*/*' } }); if (!res.ok) break; const json = await res.json(); const reviews = Array.isArray(json.reviews) ? json.reviews : Array.isArray(json.data && json.data.reviews) ? json.data.reviews : []; if (!reviews.length) break; reviews.forEach(r => { if (looksLikeReviewObject(r) && normalizeReviewObject(r, 'airbnb_api_v2')) added++; }); if (reviews.length < 50) break; offset += 50; await sleep(250); } catch (e) { break; } } if (added > 0) return added; } } return added; }; const tryEmbeddedJson = () => { let added = 0; const visited = new Set(); const walk = obj => { if (!obj || added > 500) return; if (typeof obj !== 'object') return; if (visited.has(obj)) return; visited.add(obj); if (looksLikeReviewObject(obj)) { if (normalizeReviewObject(obj, 'embedded_json_object')) added++; } if (Array.isArray(obj)) { for (const item of obj) walk(item); } else { for (const key of Object.keys(obj)) { if (/amenit|photo|image|availability|price|calendar|map|cancellation|houseRule|description|section|layout|seo|pdpTitle|roomType/i.test(key)) continue; walk(obj[key]); } } }; const scripts = Array.from(document.querySelectorAll('script[type=\"application/json\"], script#__NEXT_DATA__, script[data-state]')); for (const script of scripts) { const txt = script.textContent || ''; if (!txt || !/(comments|localized_comments|reviewText|review_text)/.test(txt)) continue; try { walk(JSON.parse(txt)); } catch (e) {} } try { walk(window.__NEXT_DATA__); walk(window.__AIRBNB_DATA__); walk(window.__APOLLO_STATE__); } catch (e) {} return added; }; const tryDom = async () => { let added = 0; const buttons = Array.from(document.querySelectorAll('button, a, [role=\"button\"]')); const reviewButton = buttons.find(el => /レビュー.*すべて表示|すべて.*レビュー|Show all.*reviews|show.*reviews|all.*reviews/i.test(clean(el.innerText || el.textContent))); if (reviewButton) { reviewButton.scrollIntoView({ block: 'center' }); await sleep(700); reviewButton.click(); await sleep(2500); } for (let i = 0; i < 18; i++) { const root = document.querySelector('[role=\"dialog\"]') || document; const scrollables = Array.from(root.querySelectorAll('*')).filter(el => el.scrollHeight > el.clientHeight + 150); const scroller = scrollables.sort((a, b) => b.scrollHeight - a.scrollHeight)[0] || document.scrollingElement || document.documentElement; if (scroller) { scroller.scrollTop = scroller.scrollHeight; scroller.dispatchEvent(new Event('scroll', { bubbles: true })); } window.scrollBy(0, Math.round(window.innerHeight * 0.75)); await sleep(400); } const root = document.querySelector('[role=\"dialog\"]') || document; const candidates = Array.from(root.querySelectorAll('[data-review-id], [data-testid*=\"review\" i], [id*=\"review\" i], article, li')).filter(el => { const t = clean(el.innerText || el.textContent); if (!isValidReviewText(t)) return false; if (!dateRe.test(t)) return false; const nested = Array.from(el.children || []).some(ch => { const ct = clean(ch.innerText || ch.textContent); return ct.length > 45 && ct.length < t.length - 20 && dateRe.test(ct); }); return !nested; }); const seen = new Set(); candidates.forEach(el => { const fullText = clean(el.innerText || el.textContent); if (!fullText || seen.has(fullText)) return; seen.add(fullText); const dateMatch = fullText.match(dateRe); const lines = fullText.split(/\\n|\\r/).map(clean).filter(Boolean); const reviewerName = lines.find(line => line.length > 1 && line.length < 80 && !dateRe.test(line) && !/レビュー|review|rating|星|star|show more|もっと見る/i.test(line)) || ''; const ratingMatch = fullText.match(/(\\d+(?:\\.\\d+)?)\\s*(?:つ星|星|stars?|rating)/i); addRow({ review_id: el.getAttribute('data-review-id') || el.id || '', submission_date: dateMatch ? dateMatch[0] : '', review: fullText, reviewer_name: reviewerName, rating: ratingMatch ? ratingMatch[1] : '', listing_id: listingId, listing_url: listingUrl, extraction_method: 'dom_fallback' }); }); return added; }; await tryApi(); if (container.querySelectorAll('.uscraper-review-row').length === 0) tryEmbeddedJson(); if (container.querySelectorAll('.uscraper-review-row').length === 0) await tryDom(); window.__uscraperAirbnbReviewRows = container.querySelectorAll('.uscraper-review-row').length; return 'strict Airbnb review rows available for export: ' + window.__uscraperAirbnbReviewRows; })();",
        "waitForCompletion": true,
        "timeout": 240,
        "color": "bg-[#a56eff]"
      }
    },
    {
      "block_id": "element-exists-1",
      "block_type": "process",
      "title": "Element Exists",
      "description": "Check if element exists",
      "position_x": 2136,
      "position_y": 220,
      "config": {
        "selector": ".uscraper-review-row",
        "color": "bg-[#ff832b]"
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 2472,
      "position_y": 520,
      "config": {
        "rowSelector": ".uscraper-review-row",
        "fileName": "airbnb_jp_review_details_scraper_strict.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "color": "bg-[#42be65]",
        "columns": [
          {
            "name": "review_id",
            "selector": "",
            "attribute": "data-review-id"
          },
          {
            "name": "submission_date",
            "selector": "",
            "attribute": "data-submission-date"
          },
          {
            "name": "review",
            "selector": "",
            "attribute": "data-review-text"
          },
          {
            "name": "reviewer_name",
            "selector": "",
            "attribute": "data-reviewer-name"
          },
          {
            "name": "rating",
            "selector": "",
            "attribute": "data-rating"
          },
          {
            "name": "listing_id",
            "selector": "",
            "attribute": "data-listing-id"
          },
          {
            "name": "listing_url",
            "selector": "",
            "attribute": "data-listing-url"
          },
          {
            "name": "source_url",
            "selector": "",
            "attribute": "data-source-url"
          },
          {
            "name": "extraction_method",
            "selector": "",
            "attribute": "data-extraction_method"
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 2808,
      "position_y": 520,
      "config": {
        "color": "bg-[#8d8d8d]"
      }
    },
    {
      "block_id": "loop-continue-2",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 2136,
      "position_y": 520,
      "config": {
        "color": "bg-[#8d8d8d]"
      }
    }
  ],
  "connections": [
    {
      "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": "sleep-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-2",
      "from_connector_id": "right",
      "to_block_id": "inject-javascript-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-2",
      "from_connector_id": "right",
      "to_block_id": "element-exists-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "element-exists-1",
      "from_connector_id": "true",
      "to_block_id": "structured-export-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "structured-export-1",
      "from_connector_id": "right",
      "to_block_id": "loop-continue-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "element-exists-1",
      "from_connector_id": "false",
      "to_block_id": "loop-continue-2",
      "to_connector_id": "left"
    }
  ],
  "canvas_elements": [
    {
      "id": "group-load",
      "element_type": "group",
      "title": "Page Load",
      "color": "#08bdba",
      "position_x": 48,
      "position_y": 116,
      "width": 1664,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "sleep-1",
          "sleep-2"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 1056,
      "position_y": 116,
      "width": 992,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "inject-javascript-1",
          "inject-javascript-2"
        ]
      }
    },
    {
      "id": "group-pagination",
      "element_type": "group",
      "title": "Pagination Loop",
      "color": "#ff832b",
      "position_x": 2064,
      "position_y": 116,
      "width": 992,
      "height": 596,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "element-exists-1",
          "loop-continue-1",
          "loop-continue-2"
        ]
      }
    },
    {
      "id": "group-extract",
      "element_type": "group",
      "title": "Data Extraction",
      "color": "#42be65",
      "position_x": 2400,
      "position_y": 416,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "structured-export-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Best-effort Airbnb Japan review-details scraper inferred from the Octoparse template. Processes Airbnb.jp listing/review URLs via navigate.urls[] and appends only strict real review rows to CSV. For each URL, the template attempts Airbnb review API extraction, strict embedded JSON review-object traversal, and DOM/modal review extraction. It deliberately does not export diagnostic or amenity rows as reviews. If Airbnb blocks automation, requires login/CAPTCHA, changes its API/React DOM, or the supplied listing has no accessible public reviews, the run may export zero rows. Replace the default sample URL with target Airbnb Japan room or room/reviews URLs for production.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-navigate-1",
      "element_type": "note",
      "title": "Note: Navigate",
      "content": "Multi-URL loop over 1 pages. Pair with loop-continue at the end of each iteration.",
      "color": "#ee5396",
      "position_x": 320,
      "position_y": 200,
      "width": 328,
      "height": 107,
      "z_index": 22,
      "data": {
        "block_id": "navigate-1"
      }
    },
    {
      "id": "note-block-inject-javascript-2",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `(async () => { const sleep = ms => new Promise(r => setTimeout(r, ms)); const clean = s => (s || '')...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 2000,
      "position_y": 200,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "inject-javascript-2"
      }
    },
    {
      "id": "note-block-element-exists-1",
      "element_type": "note",
      "title": "Note: Element Exists",
      "content": "Condition block: checks `.uscraper-review-row`. True / False branches control which path runs next. Keep enough space between branches so both connector lines are visible.",
      "color": "#ee5396",
      "position_x": 2336,
      "position_y": 200,
      "width": 340,
      "height": 137,
      "z_index": 22,
      "data": {
        "block_id": "element-exists-1"
      }
    },
    {
      "id": "note-block-loop-continue-1",
      "element_type": "note",
      "title": "Note: Loop Continue",
      "content": "Loop Continue advances a multi-URL or multi-text loop. Place at the end of the loop body with a clear back-edge to the loop start.",
      "color": "#ee5396",
      "position_x": 3008,
      "position_y": 500,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}