你也是日劇迷嗎?每季都有追不完的新片,如果能有一個自動化的系統,幫我們整理好當季所有片單、海報和評分,並存入自己的資料庫,那該有多好
這篇文章將帶你一步步使用 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"
}
}