iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0

你也是日劇迷嗎?每季都有追不完的新片,如果能有一個自動化的系統,幫我們整理好當季所有片單、海報和評分,並存入自己的資料庫,那該有多好

這篇文章將帶你一步步使用 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: 寫入資料]

workflow

1. 建立 Workflow 並設定觸發方式

  • 來到儀表板,新增一個流程「Create Workflow」

    image 0.png

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

    image 1.png

2. 設定目標片單

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

    image 2.png

3. 抓取網頁原始碼

  • 下個節點選擇「HTTP Request」來設定要抓的目標網址,方法選「GET」

    {{ `https://www.kktv.me/browse/tag/${$('Set').item.json.Collection}` }}
    

    image 3.png

4. 從 HTML 中擷取資料

  • 接著選擇「HTML」節點讓 n8n 去抓整個網頁的內容,並把片單資料用 JSON 傳出去

    • Operation: 選擇 Extract HTML Content

    • Extraction Values:

      • CSS Selector: 填入 #__NEXT_DATA__。這是一個 ID 選擇器,用來精準定位到含有我們所需資料的那個 <script> 標籤。

      • Return Value: 選擇 Html,我們需要標籤內的完整內容

    image 4.png

5. 清理、轉換與排序資料

  • 下個節點選擇「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;
    

6. 下載海報圖片

  • 下個節點選擇「HTTP Request」來設定要抓的目標圖片網址,方法選「GET」

    image 5.png

7. 寫入 NocoDB 資料庫

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

    image 6.png

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

    image 7.png

  • 欄位資料設定如下:

    • 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

          • 這會告訴 NocoDB 節點,去尋找傳入項目中 binary 屬性下的 data 來作為檔案來源,也就是我們上一步下載的圖片

    image 8.png

  • 跑動流程後就可以在 NocoDB 看到整理好的內容囉

    2025-08-29 16.53.24 app.nocodb.com 5b22ce5ec899.png

  • 最後附上流程的 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"
      }
    }
    

上一篇
[Day09]_明天要不要帶傘
系列文
告別重複瑣事: n8n workflow 自動化工作實踐10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言