{
  "version": "1.0.0",
  "exported_at": "2026-06-03T00:00:00.000Z",
  "project": {
    "name": "Expedia Japan Flight Scraper",
    "description": "Best-effort scraper for one-way economy flight listings from Expedia Japan. Default route/date is NRT to JFK on 2026-10-31, matching the Octoparse sample. The workflow opens Expedia Japan flight search results, optionally accepts cookies, waits for dynamic content, scrolls, attempts a Show More / さらに表示 load-more loop when present, then extracts Expedia flight result cards to 1737.csv. It exports boarding date, departure time, arrival time, route, flight duration, stop/layover information, airline, and one-way price. Selectors are intentionally broad because Expedia frequently changes result-card markup and may show bot/CAPTCHA pages.",
    "color": "bg-[#4589ff]",
    "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": {
        "url": "https://www.expedia.co.jp/Flights-Search?trip=oneway&leg1=from:NRT,to:JFK,departure:2026-10-31TANYT&passengers=adults:1,children:0,seniors:0,infantinlap:N&mode=search&options=cabinclass:economy&sort=PRICE_INCREASING",
        "color": "bg-[#4589ff]",
        "tags": [
          "expedia",
          "japan",
          "flights",
          "one-way",
          "economy"
        ]
      }
    },
    {
      "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
      }
    },
    {
      "block_id": "element-exists-1",
      "block_type": "process",
      "title": "Element Exists",
      "description": "Check if element exists",
      "position_x": 792,
      "position_y": 220,
      "config": {
        "selector": "//button[contains(., '同意') or contains(., 'Accept') or contains(., '承諾') or contains(., 'すべて許可') or contains(., '許可')]"
      }
    },
    {
      "block_id": "click-1",
      "block_type": "process",
      "title": "Click",
      "description": "Click on element",
      "position_x": 1128,
      "position_y": 520,
      "config": {
        "selector": "//button[contains(., '同意') or contains(., 'Accept') or contains(., '承諾') or contains(., 'すべて許可') or contains(., '許可')]",
        "timeout": 8
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1464,
      "position_y": 520,
      "config": {
        "duration": 2
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 1800,
      "position_y": 520,
      "config": {
        "selector": "body",
        "timeout": 30,
        "visible": true
      }
    },
    {
      "block_id": "sleep-5",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 2136,
      "position_y": 520,
      "config": {
        "duration": 8
      }
    },
    {
      "block_id": "scroll-1",
      "block_type": "process",
      "title": "Scroll",
      "description": "Scroll the page or element",
      "position_x": 2472,
      "position_y": 520,
      "config": {
        "direction": "down",
        "amount": 1200
      }
    },
    {
      "block_id": "sleep-2",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 2808,
      "position_y": 520,
      "config": {
        "duration": 3
      }
    },
    {
      "block_id": "element-exists-2",
      "block_type": "process",
      "title": "Element Exists",
      "description": "Check if element exists",
      "position_x": 3144,
      "position_y": 520,
      "config": {
        "selector": "//button[contains(., 'さらに表示') or contains(., 'もっと見る') or contains(., 'Show more') or contains(., 'More results') or contains(., 'その他')]"
      }
    },
    {
      "block_id": "click-2",
      "block_type": "process",
      "title": "Click",
      "description": "Click on element",
      "position_x": 3480,
      "position_y": 520,
      "config": {
        "selector": "//button[contains(., 'さらに表示') or contains(., 'もっと見る') or contains(., 'Show more') or contains(., 'More results') or contains(., 'その他')]",
        "timeout": 10
      }
    },
    {
      "block_id": "sleep-3",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 3816,
      "position_y": 520,
      "config": {
        "duration": 3
      }
    },
    {
      "block_id": "scroll-2",
      "block_type": "process",
      "title": "Scroll",
      "description": "Scroll the page or element",
      "position_x": 4152,
      "position_y": 520,
      "config": {
        "direction": "down",
        "amount": 1200
      }
    },
    {
      "block_id": "sleep-4",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 4488,
      "position_y": 758,
      "config": {
        "duration": 2
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 3144,
      "position_y": 520,
      "config": {
        "rowSelector": "li[data-test-id=\"offer-listing\"], div[data-test-id=\"offer-listing\"], [data-stid^=\"FLIGHTS_DETAILS_AND_FARES\"]",
        "fileName": "1737.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "create",
        "columns": [
          {
            "name": "boarding_date",
            "selector": "(() => { const qs = new URL(location.href).search; const direct = (qs.match(/departure:([^,T&]+)/)||[])[1] || (qs.match(/[?&](?:fromDate|d1)=([^&]+)/)||[])[1] || ''; return decodeURIComponent(direct).replace(/-/g,'/'); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "departure_time",
            "selector": "(() => { const txt = ((ROW.getAttribute('aria-label') || '') + ' ' + Array.from(ROW.querySelectorAll('[aria-label]')).map(e=>e.getAttribute('aria-label')||'').join(' ') + ' ' + (ROW.innerText || '')).replace(/\\s+/g,' '); const m = txt.match(/departing at\\s*([0-9]{1,2}:[0-9]{2}\\s*(?:am|pm)?)/i) || txt.match(/出発[^0-9]*([0-9]{1,2}:[0-9]{2})/) || txt.match(/\\b([0-9]{1,2}:[0-9]{2}\\s*(?:am|pm)?)\\b/i); return m ? m[1].replace(/\\s+/g,'') : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "arrival_time",
            "selector": "(() => { const txt = ((ROW.getAttribute('aria-label') || '') + ' ' + Array.from(ROW.querySelectorAll('[aria-label]')).map(e=>e.getAttribute('aria-label')||'').join(' ') + ' ' + (ROW.innerText || '')).replace(/\\s+/g,' '); const m = txt.match(/arriving at\\s*([0-9]{1,2}:[0-9]{2}\\s*(?:am|pm)?)/i) || txt.match(/到着[^0-9]*([0-9]{1,2}:[0-9]{2})/); if (m) return m[1].replace(/\\s+/g,''); const times = Array.from(txt.matchAll(/\\b([0-9]{1,2}:[0-9]{2}\\s*(?:am|pm)?)\\b/ig)).map(x=>x[1].replace(/\\s+/g,'')); return times[1] || ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "departure_arrival",
            "selector": "(() => { const qs = decodeURIComponent(new URL(location.href).search); const leg = qs.match(/leg1=from:([^,]+),to:([^,]+)/); if (leg) return leg[1].replace(/:.*/, '') + ' → ' + leg[2].replace(/:.*/, ''); const txt = ROW.innerText || ''; const codes = txt.match(/\\b[A-Z]{3}\\b/g); return codes && codes.length >= 2 ? codes[0] + ' → ' + codes[codes.length - 1] : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "flight_duration",
            "selector": "(() => { const txt = ((ROW.getAttribute('aria-label') || '') + ' ' + Array.from(ROW.querySelectorAll('[aria-label]')).map(e=>e.getAttribute('aria-label')||'').join(' ') + ' ' + (ROW.innerText || '')).replace(/\\s+/g,' '); let m = txt.match(/([0-9]+\\s*hours?\\s*[0-9]+\\s*minutes?)\\s*total travel time/i) || txt.match(/([0-9]+\\s*h\\s*[0-9]*\\s*m?)(?![^;]{0,40}layover)/i) || txt.match(/([0-9]+\\s*時間\\s*[0-9]*\\s*分?)(?![^;]{0,40}乗継時間)/i); if (m) return m[1].replace(/\\s+/g,' ').trim(); const line = (ROW.innerText || '').split('\\n').map(s=>s.trim()).find(s => /(\\d+\\s*h\\b|\\d+\\s*hours?|\\d+\\s*時間)/i.test(s) && !/layover|乗継時間|経由|stop|select|priced|traveler|￥|¥|left at/i.test(s)); return line || ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "stop_info",
            "selector": "(() => { const txt = ((ROW.getAttribute('aria-label') || '') + ' ' + Array.from(ROW.querySelectorAll('[aria-label]')).map(e=>e.getAttribute('aria-label')||'').join(' ') + ' ' + (ROW.innerText || '')).replace(/\\s+/g,' '); const stop = (txt.match(/Nonstop|Direct|One stop|Two stops|Three stops|[0-9]+\\s+stops?|乗継\\s*[0-9]+\\s*回/i) || [''])[0]; const lay = (txt.match(/Layover for\\s*[^.,]+(?:\\s+in\\s+[^.,]+)?/i) || txt.match(/[^。\\n,]*経由[^。\\n,]*/) || [''])[0]; return [stop, lay].filter(Boolean).join('; '); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "airline",
            "selector": "(() => { const txt = ((ROW.getAttribute('aria-label') || '') + ' ' + Array.from(ROW.querySelectorAll('[aria-label]')).map(e=>e.getAttribute('aria-label')||'').join(' ') + ' ' + (ROW.innerText || '')).replace(/\\s+/g,' '); let m = txt.match(/Select and show fare information for\\s+(.+?)\\s+flight/i) || txt.match(/Select\\s+(.+?)\\s+flight/i); if (m) return m[1].replace(/^Cheapest,\\s*/i,'').trim(); const alt = Array.from(ROW.querySelectorAll('img[alt]')).map(e=>e.getAttribute('alt')).find(a => a && !/logo|icon|expedia/i.test(a)); if (alt) return alt.trim(); const line = (ROW.innerText || '').split('\\n').map(s=>s.trim()).find(s => /航空|Airlines?|Airways?|Air Canada|Cathay|Delta|United|Korean|Asiana|Hawaiian|Philippine|EVA|China Airlines|JAL|ANA/i.test(s)); return line || ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "one_way_price",
            "selector": "(() => { const txt = ((ROW.getAttribute('aria-label') || '') + ' ' + Array.from(ROW.querySelectorAll('[aria-label]')).map(e=>e.getAttribute('aria-label')||'').join(' ') + ' ' + (ROW.innerText || '')).replace(/\\s+/g,' '); const m = txt.match(/[￥¥]\\s?[0-9,]+|JPY\\s?[0-9,]+|[0-9,]+\\s*円/i); return m ? m[0].replace(/\\s+/g,'') : ''; })()",
            "attribute": "text",
            "isJs": true
          }
        ]
      }
    },
    {
      "block_id": "end-1",
      "block_type": "output",
      "title": "End",
      "description": "Terminate execution flow",
      "position_x": 1296,
      "position_y": 800,
      "config": {}
    }
  ],
  "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": "element-exists-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "element-exists-1",
      "from_connector_id": "true",
      "to_block_id": "click-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "click-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": "wait-for-element-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "element-exists-1",
      "from_connector_id": "false",
      "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": "sleep-5",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-5",
      "from_connector_id": "right",
      "to_block_id": "scroll-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "scroll-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": "element-exists-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "element-exists-2",
      "from_connector_id": "true",
      "to_block_id": "click-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "click-2",
      "from_connector_id": "right",
      "to_block_id": "sleep-3",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-3",
      "from_connector_id": "right",
      "to_block_id": "scroll-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "scroll-2",
      "from_connector_id": "right",
      "to_block_id": "sleep-4",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-4",
      "from_connector_id": "right",
      "to_block_id": "element-exists-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "element-exists-2",
      "from_connector_id": "false",
      "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-load",
      "element_type": "group",
      "title": "Page Load",
      "color": "#08bdba",
      "position_x": 48,
      "position_y": 116,
      "width": 4688,
      "height": 834,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "sleep-1",
          "wait-for-element-1",
          "sleep-5",
          "sleep-2",
          "sleep-3",
          "sleep-4"
        ]
      }
    },
    {
      "id": "group-pagination",
      "element_type": "group",
      "title": "Pagination Loop",
      "color": "#ff832b",
      "position_x": 720,
      "position_y": 116,
      "width": 3008,
      "height": 596,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "element-exists-1",
          "click-1",
          "element-exists-2",
          "click-2"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 2400,
      "position_y": 416,
      "width": 2000,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "scroll-1",
          "scroll-2"
        ]
      }
    },
    {
      "id": "group-extract",
      "element_type": "group",
      "title": "Data Extraction",
      "color": "#42be65",
      "position_x": 3072,
      "position_y": 416,
      "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": 1224,
      "position_y": 696,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "end-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Best-effort scraper for one-way economy flight listings from Expedia Japan. Default route/date is NRT to JFK on 2026-10-31, matching the Octoparse sample. The workflow opens Expedia Japan flight search results, optionally accepts cookies, waits for dynamic content, scrolls, attempts a Show More / さらに表示 load-more loop when present, then extracts Expedia flight result cards to 1737.csv. It exports boarding date, departure time, arrival time, route, flight duration, stop/layover information, airline, and one-way price. Selectors are intentionally broad because Expedia frequently changes result-card markup and may show bot/CAPTCHA pages.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-element-exists-1",
      "element_type": "note",
      "title": "Note: Element Exists",
      "content": "Condition block: checks `//button[contains(., '同意') or contains(., 'Accept') or contains(., '承諾') or contains(., 'すべて許可') or contains(., '許可')]`. True / False branches control which path runs next. Keep enough space between branches so both connector lines are visible.",
      "color": "#ee5396",
      "position_x": 992,
      "position_y": 200,
      "width": 340,
      "height": 169,
      "z_index": 22,
      "data": {
        "block_id": "element-exists-1"
      }
    },
    {
      "id": "note-block-element-exists-2",
      "element_type": "note",
      "title": "Note: Element Exists",
      "content": "Condition block: checks `//button[contains(., 'さらに表示') or contains(., 'もっと見る') or contains(., 'Show more') or contains(., 'More results') or cont`. True / False branches control which path runs next. Keep enough space between branches so both connector lines are visible.",
      "color": "#ee5396",
      "position_x": 3344,
      "position_y": 500,
      "width": 340,
      "height": 170,
      "z_index": 22,
      "data": {
        "block_id": "element-exists-2"
      }
    },
    {
      "id": "note-block-click-2",
      "element_type": "note",
      "title": "Note: Click",
      "content": "Uses XPath `//button[contains(., 'さらに表示') or contains(., 'もっと見る') or contains(., 'Show more') or contains(., 'Mo`. XPath breaks easily if DOM structure changes.",
      "color": "#ee5396",
      "position_x": 3680,
      "position_y": 500,
      "width": 340,
      "height": 133,
      "z_index": 22,
      "data": {
        "block_id": "click-2"
      }
    },
    {
      "id": "note-block-structured-export-1",
      "element_type": "note",
      "title": "Note: Structured Export",
      "content": "Structured export with JS columns (boarding_date, departure_time, arrival_time, departure_arrival, flight_duration). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 3344,
      "position_y": 500,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "structured-export-1"
      }
    }
  ]
}