{
  "version": "1.0.0",
  "exported_at": "2026-05-31T14:45:00.000Z",
  "project": {
    "name": "TikTok Search Scraper No Login Required",
    "description": "Best-effort no-login TikTok keyword/video URL scraper equivalent to the Octoparse TikTok Search Scraper. The Octoparse sample search URL https://www.tiktok.com/video?q=cooking currently returns a TikTok 404 in the attached analysis, so this template uses TikTok's current search-video route and includes the analyzed Octoparse sample video URL as a fallback seed. TikTok search uses infinite scrolling rather than numbered pagination; the workflow starts an infinite-scroll pass, waits for content, builds hidden export rows from TikTok video links or the current video detail page, and appends results across all navigate.urls entries. Results may be limited by TikTok anti-bot controls, CAPTCHA, regional restrictions, anonymous-session throttling, or DOM changes.",
    "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": 260,
      "config": {
        "urls": [
          "https://www.tiktok.com/search/video?q=cooking",
          "https://www.tiktok.com/@icookicleanicater/video/7548260928232459550?q=cooking"
        ],
        "color": "bg-[#4589ff]",
        "tags": [
          "tiktok",
          "search",
          "keyword",
          "fallback-detail-url"
        ]
      }
    },
    {
      "block_id": "wait-for-page-load-1",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for page to finish loading",
      "position_x": 480,
      "position_y": 260,
      "config": {
        "timeout": 45,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 840,
      "position_y": 260,
      "config": {
        "duration": 5,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Run custom JavaScript on the page",
      "position_x": 1200,
      "position_y": 260,
      "config": {
        "jsCode": "(() => { const clean = (s) => (s || '').replace(/\\s+/g, ' ').trim(); const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); window.__uscraperTikTokScrollDone = false; window.__uscraperTikTokScrollStarted = true; Array.from(document.querySelectorAll('button, div[role=\"button\"]')).filter((b) => /accept|allow|agree|close/i.test(clean(b.innerText) || b.getAttribute('aria-label') || '')).slice(0, 4).forEach((b) => { try { b.click(); } catch (e) {} }); (async () => { let last = -1; let stable = 0; for (let i = 0; i < 14 && stable < 3; i++) { window.scrollTo(0, document.body.scrollHeight); await sleep(1200); const count = document.querySelectorAll('a[href*=\"/video/\"]').length; if (count === last) { stable++; } else { stable = 0; last = count; } } window.__uscraperTikTokScrollDone = true; window.scrollTo(0, 0); })(); return 'tiktok-scroll-started'; })();",
        "waitForCompletion": true,
        "timeout": 10,
        "color": "bg-[#a56eff]"
      }
    },
    {
      "block_id": "sleep-2",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1560,
      "position_y": 260,
      "config": {
        "duration": 18,
        "color": "bg-[#ff832b]"
      }
    },
    {
      "block_id": "inject-javascript-2",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Run custom JavaScript on the page",
      "position_x": 1920,
      "position_y": 260,
      "config": {
        "jsCode": "(() => { const clean = (s) => (s || '').replace(/\\s+/g, ' ').trim(); const meta = (name) => document.querySelector(`meta[property=\"${name}\"], meta[name=\"${name}\"]`)?.getAttribute('content') || ''; const keywordFromUrl = () => { try { const u = new URL(location.href); const q = u.searchParams.get('q') || ''; if (q) return q; const tag = location.pathname.match(/\\/tag\\/([^/?#]+)/); return tag ? decodeURIComponent(tag[1]) : ''; } catch (e) { return ''; } }; const videoDateFromUrl = (url) => { const m = String(url || '').match(/\\/video\\/([0-9]+)/); if (!m) return ''; try { const ts = Number(BigInt(m[1]) >> 32n); if (ts > 946684800 && ts < 4102444800) return new Date(ts * 1000).toISOString().slice(0, 10); } catch (e) {} return ''; }; const normalizeVideoUrl = (href, keyword) => { try { const u = new URL(href, location.origin); const base = u.origin + u.pathname; if (keyword && !u.searchParams.get('q')) return base + '?q=' + encodeURIComponent(keyword); return base + (u.search || ''); } catch (e) { return href || ''; } }; const getCard = (a) => a?.closest('[data-e2e*=video], [data-testid*=video], article, div') || document.body; const getTitle = (a, card, url) => { const desc = meta('og:description') || meta('twitter:description') || meta('description'); const values = [a?.getAttribute('aria-label'), a?.getAttribute('title'), a?.querySelector('img[alt]')?.getAttribute('alt'), card?.querySelector('img[alt]')?.getAttribute('alt'), a?.innerText, card && card !== document.body ? card.innerText : '', desc, document.title]; let title = clean(values.find((v) => clean(v).length > 3) || ''); title = title.replace(/^\\d+(?:\\.\\d+)?[KMB]?\\s+Likes,\\s*\\d+(?:\\.\\d+)?[KMB]?\\s+Comments\\.\\s*/i, ''); title = title.replace(/^TikTok video from .*?:\\s*[“\"]?/i, '').replace(/[”\"]?\\s*$/, ''); return title; }; const getViews = (card) => { const text = clean((card && card.innerText) || '') + ' ' + clean(meta('description')); const patterns = [/([0-9]+(?:\\.[0-9]+)?\\s*[KMB]?)\\s*(?:views?)/i, /([0-9]+(?:\\.[0-9]+)?\\s*[KMB]?)\\s*(?:Likes?)/i]; for (const p of patterns) { const m = text.match(p); if (m) return clean(m[1]); } const generic = text.match(/\\b([0-9]+(?:\\.[0-9]+)?\\s*[KMB])\\b/i); return generic ? clean(generic[1]) : ''; }; const hashtagsFromText = (text) => Array.from(new Set((text.match(/#[\\p{L}\\p{N}_]+/gu) || []).map(clean))).join(' '); const getHashtags = (card) => { const fromLinks = Array.from((card || document).querySelectorAll('a[href*=\"/tag/\"]')).map((x) => clean(x.innerText) || ('#' + decodeURIComponent((x.href.match(/\\/tag\\/([^/?#]+)/) || [])[1] || ''))).filter(Boolean); const fromMeta = hashtagsFromText([meta('og:description'), meta('twitter:description'), meta('description'), document.body.innerText].join(' ')); return Array.from(new Set(fromLinks.concat(fromMeta ? fromMeta.split(' ') : []))).join(' '); }; const old = document.querySelector('#uscraper-tiktok-video-rows'); if (old) old.remove(); const container = document.createElement('div'); container.id = 'uscraper-tiktok-video-rows'; container.style.display = 'none'; const keyword = keywordFromUrl(); const pageTitle = document.title || meta('og:title') || ''; const pageUrl = location.href; const seen = new Set(); const addRow = (href, a, card) => { const raw = href || ''; const m = raw.match(/\\/@([^/]+)\\/video\\/([0-9]+)/); if (!m) return; const videoUrl = normalizeVideoUrl(raw, keyword); const dedupeKey = videoUrl.split('?')[0]; if (seen.has(dedupeKey)) return; seen.add(dedupeKey); const handle = m[1] || ''; const row = document.createElement('div'); row.setAttribute('data-uscraper-tiktok-video-row', 'true'); row.setAttribute('data-input-keyword', keyword); row.setAttribute('data-page-title', pageTitle); row.setAttribute('data-page-url', pageUrl); row.setAttribute('data-video-title', getTitle(a, card, videoUrl)); row.setAttribute('data-video-url', videoUrl); row.setAttribute('data-view-count', getViews(card)); row.setAttribute('data-hashtags', getHashtags(card)); row.setAttribute('data-tiktoker', handle); row.setAttribute('data-tiktoker-url', handle ? location.origin + '/@' + handle : ''); row.setAttribute('data-publish-date', videoDateFromUrl(videoUrl)); container.appendChild(row); }; if (/\\/@[^/]+\\/video\\/[0-9]+/.test(location.href)) { addRow(location.href, null, document.body); } Array.from(document.querySelectorAll('a[href*=\"/video/\"]')).forEach((a) => { addRow(a.href || a.getAttribute('href') || '', a, getCard(a)); }); document.body.appendChild(container); return container.children.length; })();",
        "waitForCompletion": true,
        "timeout": 20,
        "color": "bg-[#a56eff]"
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 2280,
      "position_y": 260,
      "config": {
        "rowSelector": "#uscraper-tiktok-video-rows > div[data-uscraper-tiktok-video-row='true']",
        "fileName": "tiktok-video-url-scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "color": "bg-[#42be65]",
        "columns": [
          {
            "name": "input_keyword",
            "selector": "",
            "attribute": "data-input-keyword"
          },
          {
            "name": "page_title",
            "selector": "",
            "attribute": "data-page-title"
          },
          {
            "name": "page_url",
            "selector": "",
            "attribute": "data-page-url"
          },
          {
            "name": "video_title",
            "selector": "",
            "attribute": "data-video-title"
          },
          {
            "name": "video_url",
            "selector": "",
            "attribute": "data-video-url"
          },
          {
            "name": "view_count",
            "selector": "",
            "attribute": "data-view-count"
          },
          {
            "name": "hashtags",
            "selector": "",
            "attribute": "data-hashtags"
          },
          {
            "name": "tiktoker",
            "selector": "",
            "attribute": "data-tiktoker"
          },
          {
            "name": "tiktoker_url",
            "selector": "",
            "attribute": "data-tiktoker-url"
          },
          {
            "name": "publish_date",
            "selector": "",
            "attribute": "data-publish-date"
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 2640,
      "position_y": 260,
      "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": "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"
    }
  ],
  "canvas_elements": [
    {
      "id": "group-load",
      "element_type": "group",
      "title": "Page Load",
      "color": "#08bdba",
      "position_x": 48,
      "position_y": 156,
      "width": 1760,
      "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": 1128,
      "position_y": 156,
      "width": 1040,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "inject-javascript-1",
          "inject-javascript-2"
        ]
      }
    },
    {
      "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-pagination",
      "element_type": "group",
      "title": "Pagination Loop",
      "color": "#ff832b",
      "position_x": 2568,
      "position_y": 156,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "loop-continue-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Best-effort no-login TikTok keyword/video URL scraper equivalent to the Octoparse TikTok Search Scraper. The Octoparse sample search URL https://www.tiktok.com/video?q=cooking currently returns a TikTok 404 in the attached analysis, so this template uses TikTok's current search-video route and includes the analyzed Octoparse sample video URL as a fallback seed. TikTok search uses infinite scrolling rather than numbered pagination; the workflow starts an infinite-scroll pass, waits for content, builds hidden export rows from TikTok video links or the current video detail page, and appends results across all navigate.urls entries. Results may be limited by TikTok anti-bot controls, CAPTCHA, regional restrictions, anonymous-session throttling, or DOM 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: `(() => { const clean = (s) => (s || '').replace(/\\s+/g, ' ').trim(); const sleep = (ms) => new Promi...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 1400,
      "position_y": 240,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "inject-javascript-1"
      }
    },
    {
      "id": "note-block-inject-javascript-2",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `(() => { const clean = (s) => (s || '').replace(/\\s+/g, ' ').trim(); const meta = (name) => document...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 2120,
      "position_y": 240,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "inject-javascript-2"
      }
    },
    {
      "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": 2840,
      "position_y": 240,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}