你也是日劇迷嗎?每季都有追不完的新片,如果能有一個自動化的系統,幫我們整理好當季所有片單、海報和評分,並存入自己的資料庫,那該有多好
這篇文章將帶你一步步使用 n8n 這個自動化工具,串接 KKTV 的片單資料與 NocoDB 資料庫,打造一個專屬於你的資料庫
graph TD
A[手動觸發] --> B[Set: 設定目標]
B --> C[HTTP Request: GET]
C --> D[HTML: 擷取資料]
D --> E[Code: 清理與排序]
E --> F[HTTP Request: Image]
F --> G[NocoDB: 寫入資料]
來到儀表板,新增一個流程「Create Workflow」

初始節點選擇手動觸發的「Manual Trigger」,也可以設定排程每一季去抓,但不確定 KKTV 何時會公開新一季的片單,所以這邊還是手動

下個節點選擇「Edit Fields (Set)」來設定要抓的合輯變數,這邊以「2025 春季日劇」為例

下個節點選擇「HTTP Request」來設定要抓的目標網址,方法選「GET」
{{ `https://www.kktv.me/browse/tag/${$('Set').item.json.Collection}` }}

接著選擇「HTML」節點讓 n8n 去抓整個網頁的內容,並把片單資料用 JSON 傳出去
Operation: 選擇 Extract HTML Content
Extraction Values:
CSS Selector: 填入 #__NEXT_DATA__。這是一個 ID 選擇器,用來精準定位到含有我們所需資料的那個 <script> 標籤。
Return Value: 選擇 Html,我們需要標籤內的完整內容

下個節點選擇「Code」來整理這些資料,只抓出我們需要的欄位,並依照「評分人數」來進行排序
// 工具函數:替換圖片 URL 為高品質版本
function upgradeImageUrl(url, type) {
  const originEndsWithName = `.${type}.jpg`;
  if (typeof url === "string" && url.endsWith(`.${type}.jpg`)) {
    return url.replace(originEndsWithName, ".lg.jpg");
  }
  return url;
}
// 工具函數:處理 stills 陣列
function upgradeStillsArray(stills) {
  if (!Array.isArray(stills)) return stills;
  return stills.map(upgradeImageUrl);
}
// 工具函數:選擇並轉換指定欄位
function selectAndTransformFields(originalItem) {
  // 定義需要保留的欄位
  const fieldsToKeep = [
    "ratingUserCount",
    "name",
    "latestUpdateInfo",
    "userRating",
    "deeplink",
  ];
  const newItem = {};
  // 複製指定欄位
  fieldsToKeep.forEach((field) => {
    if (originalItem[field] !== undefined) {
      newItem[field] = originalItem[field];
    }
  });
  // 特殊處理:升級圖片 URL
  if (originalItem.cover !== undefined) {
    newItem.cover = upgradeImageUrl(originalItem.cover, "sm");
  }
  if (originalItem.stills !== undefined) {
    newItem.stills = upgradeStillsArray(originalItem.stills).map((newItem) =>
      upgradeImageUrl(newItem, "xs")
    );
  }
  return newItem;
}
// 工具函數:解析並處理單個輸入項目
function processInputItem(item) {
  const nextDataRaw = item.json[""];
  if (!nextDataRaw) {
    console.warn("輸入項目中未找到 '\"\"' 屬性,跳過此項目。");
    return [];
  }
  try {
    const jsonData = JSON.parse(nextDataRaw);
    // 安全地檢查巢狀屬性
    const collection = jsonData?.props?.initialState?.browse?.collection;
    if (!Array.isArray(collection)) {
      console.warn(
        "未找到預期的 JSON 路徑 'props.initialState.browse.collection' 或其不是陣列,跳過此項目。"
      );
      return [];
    }
    // 過濾並轉換項目
    return (
      collection
        .filter((collectionItem) => collectionItem.releaseYear !== undefined)
        .map(selectAndTransformFields)
        // .filter(item => item.ratingUserCount !== undefined) // 只保留有評分數量的項目
        .map((item) => ({ json: item }))
    ); // 包裝成 n8n 格式
  } catch (error) {
    console.error("解析 JSON 失敗:", error);
    return [];
  }
}
// 主要邏輯
const processedItems = $input
  .all()
  .flatMap(processInputItem)
  .sort((a, b) => {
    const ratingA = a.json.ratingUserCount || 0;
    const ratingB = b.json.ratingUserCount || 0;
    return ratingB - ratingA; // 降序排序
  });
return processedItems;
下個節點選擇「HTTP Request」來設定要抓的目標圖片網址,方法選「GET」

接著選擇「NocoDB」,將我們的資料寫入資料庫,它是一個開源的 Airtable 替代方案,可以快速建立資料庫與 API,同樣的也需要先建立憑證

可以在 NocoDB 的使用者設定裡面建立新的 API Token,並把它貼到 n8n 裡面

欄位資料設定如下:
Project & Table: 選擇你剛剛建立好的專案與資料表
Fields: 點擊 Add Field,將我們從 Code 節點和 Image 節點得到的資料,對應到 NocoDB 的欄位
季度: ={{ $('Set').item.json.Collection.replace(/(\d{4})([^\\d\\s]+)/, '$1-$2') }} (這裡用了一點正規表達式來整理)
台灣片名: ={{ $json.name }}
集數: ={{ $json.latestUpdateInfo }}
海報: ={{ $json.cover }}
劇照連結: ={{ $json.stills.map(item => item) }}
影片網址: ={{ $json.deeplink }}
評價: ={{ $json.userRating }}
評分人數: ={{ $json.ratingUserCount }}
海報附件 (這步最關鍵):
啟動旁邊的 Binary Data? 開關
Input Data Field Name: 填入 data
binary 屬性下的 data 來作為檔案來源,也就是我們上一步下載的圖片
跑動流程後就可以在 NocoDB 看到整理好的內容囉

最後附上流程的 JSON
{
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -180,
        0
      ],
      "id": "1f6407fa-fb22-44df-bf5d-1e677e953650",
      "name": "When clicking ‘Execute workflow’"
    },
    {
      "parameters": {
        "operation": "extractHtmlContent",
        "extractionValues": {
          "values": [
            {
              "key": "=",
              "cssSelector": "#__NEXT_DATA__",
              "returnValue": "html"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.html",
      "typeVersion": 1.2,
      "position": [
        240,
        260
      ],
      "id": "100ba845-3218-431a-b83e-b9f67f8c60ed",
      "name": "HTML"
    },
    {
      "parameters": {
        "jsCode": "// 工具函數:替換圖片 URL 為高品質版本\nfunction upgradeImageUrl(url, type) {\n  const originEndsWithName = `.${type}.jpg`;\n  if (typeof url === \"string\" && url.endsWith(`.${type}.jpg`)) {\n    return url.replace(originEndsWithName, \".lg.jpg\");\n  }\n  return url;\n}\n\n// 工具函數:處理 stills 陣列\nfunction upgradeStillsArray(stills) {\n  if (!Array.isArray(stills)) return stills;\n  return stills.map(upgradeImageUrl);\n}\n\n// 工具函數:選擇並轉換指定欄位\nfunction selectAndTransformFields(originalItem) {\n  // 定義需要保留的欄位\n  const fieldsToKeep = [\n    \"ratingUserCount\",\n    \"name\",\n    \"latestUpdateInfo\",\n    \"userRating\",\n    \"deeplink\",\n  ];\n\n  const newItem = {};\n\n  // 複製指定欄位\n  fieldsToKeep.forEach((field) => {\n    if (originalItem[field] !== undefined) {\n      newItem[field] = originalItem[field];\n    }\n  });\n\n  // 特殊處理:升級圖片 URL\n  if (originalItem.cover !== undefined) {\n    newItem.cover = upgradeImageUrl(originalItem.cover, \"sm\");\n  }\n\n  if (originalItem.stills !== undefined) {\n    newItem.stills = upgradeStillsArray(originalItem.stills).map((newItem) =>\n      upgradeImageUrl(newItem, \"xs\"),\n    );\n  }\n\n  return newItem;\n}\n\n// 工具函數:解析並處理單個輸入項目\nfunction processInputItem(item) {\n  const nextDataRaw = item.json[\"\"];\n\n  if (!nextDataRaw) {\n    console.warn(\"輸入項目中未找到 '\\\"\\\"' 屬性,跳過此項目。\");\n    return [];\n  }\n\n  try {\n    const jsonData = JSON.parse(nextDataRaw);\n\n    // 安全地檢查巢狀屬性\n    const collection = jsonData?.props?.initialState?.browse?.collection;\n\n    if (!Array.isArray(collection)) {\n      console.warn(\n        \"未找到預期的 JSON 路徑 'props.initialState.browse.collection' 或其不是陣列,跳過此項目。\",\n      );\n      return [];\n    }\n\n    // 過濾並轉換項目\n    return (\n      collection\n        .filter((collectionItem) => collectionItem.releaseYear !== undefined)\n        .map(selectAndTransformFields)\n        // .filter(item => item.ratingUserCount !== undefined) // 只保留有評分數量的項目\n        .map((item) => ({ json: item }))\n    ); // 包裝成 n8n 格式\n  } catch (error) {\n    console.error(\"解析 JSON 失敗:\", error);\n    return [];\n  }\n}\n\n// 主要邏輯\nconst processedItems = $input\n  .all()\n  .flatMap(processInputItem)\n  .sort((a, b) => {\n    const ratingA = a.json.ratingUserCount || 0;\n    const ratingB = b.json.ratingUserCount || 0;\n    return ratingB - ratingA; // 降序排序\n  });\n\nreturn processedItems;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        260
      ],
      "id": "a75b331a-755e-43c6-910a-77be8841b7da",
      "name": "Code"
    },
    {
      "parameters": {
        "operation": "toJson",
        "options": {}
      },
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [
        700,
        120
      ],
      "id": "7ab31b70-cd3b-4867-b72a-2fdc84df76eb",
      "name": "Convert to File"
    },
    {
      "parameters": {
        "authentication": "nocoDbApiToken",
        "operation": "create",
        "workspaceId": "YOUR_WORKSPACE_ID",
        "projectId": "YOUR_PROJECT_ID",
        "table": "YOUR_TABLE_ID",
        "fieldsUi": {
          "fieldValues": [
            {
              "fieldName": "季度",
              "fieldValue": "={{ $('Set').item.json.Collection.replace(/(\\d{4})([^\\d\\s]+)/, '$1-$2') }}"
            },
            {
              "fieldName": "台灣片名",
              "fieldValue": "={{ $json.name }}"
            },
            {
              "fieldName": "集數",
              "fieldValue": "={{ $json.latestUpdateInfo }}"
            },
            {
              "fieldName": "海報",
              "fieldValue": "={{ $json.cover }}"
      _       },
            {
              "fieldName": "劇照連結",
              "fieldValue": "={{ $json.stills.map(item => item) }}"
            },
            {
              "fieldName": "影片網址",
              "fieldValue": "={{ $json.deeplink }}"
            },
            {
              "fieldName": "評價",
              "fieldValue": "={{ $json.userRating }}"
            },
            {
              "fieldName": "評分人數",
              "fieldValue": "={{ $json.ratingUserCount }}"
            },
            {
              "fieldName": "海報附件",
              "binaryData": true,
              "binaryProperty": "=data"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.nocoDb",
      "typeVersion": 3,
      "position": [
        940,
        340
      ],
      "id": "3d68bcda-e78c-436a-8afb-7b6da29e19cc",
      "name": "NocoDB"
    },
    {
      "parameters": {
        "url": "={{ `https://www.kktv.me/browse/tag/${$('Set').item.json.Collection}` }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        40,
        260
      ],
      "id": "78017075-b6c5-4702-ac0f-3fd4506d36ae",
      "name": "GET"
    },
    {
      "parameters": {
        "url": "={{ $json.cover }}",
        "options": {
          "batching": {
            "batch": {
              "batchSize": 5
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        700,
        340
      ],
      "id": "fcd9a6d8-83ab-478d-a9d3-1bce0e3e1db9",
      "name": "Image"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "90363807-556d-4932-9443-60c9dab4f4f4",
              "name": "Collection",
              "value": "2025春季日劇",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        40,
        0
      ],
      "id": "9d7e9202-97dc-42e3-8e13-60efa20f809d",
      "name": "Set"
    }
  ],
  "connections": {
    "When clicking ‘Execute workflow’": {
      "main": [
        [
          {
            "node": "Set",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTML": {
      "main": [
        [
          {
            "node": "Code",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code": {
      "main": [
        [
          {
            "node": "Convert to File",
            "type": "main",
            "index": 0
          },
          {
            "node": "Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert to File": {
      "main": [
        []
      ]
    },
    "NocoDB": {
      "main": [
        []
      ]
    },
    "GET": {
      "main": [
        [
          {
            "node": "HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Image": {
      "main": [
        [
          {
            "node": "NocoDB",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set": {
      "main": [
        [
          {
            "node": "GET",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "YOUR_INSTANCE_ID"
  }
}