先說結論:2025年1月26日,到現在只有靜態抓取成功,繼續努力。
一直有想自己從頭架個網站放自己的電繪作品(想當初也是推動我跨領域念資管所的一大原因),最近終於覺得好像有成功的可能性,故嘗試之。結果試了好幾天都沒辦法。
先在這記下過程以免重蹈覆轍,並希望和我一樣想從零串接前後端內容的人沒成功也別傷心,這裡有個人和你一起哭(喂
底下是花式失敗過程,歡迎指教qwqqq
之前補修大學部的課,和組員以firebase為後端建了一個場地預約系統;修碩士班的課則因為用了bigquery而接觸到了google console,發現「咦,有google drive api欸」——腦海浮現了一個念頭:我想做個展示作品集的網站,如果作品放在google drive裡某個資料夾,並運用神奇的方法讓網站讀取放進資料夾的圖片,這樣不是達成目標了嗎!
為了增加信心,我先嘗試了以下html,讓手動上傳的圖片可以以正方形型態顯示(說到這個,IG的排版似乎變成長方形,果然只要不是自己的網站就要承受環境變化的風險…)。
測試是否可運作的方法是vscode裡的live server,不出所料,成功。
//最原始、上傳圖片的版本
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>圖片上傳展示</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f9f9f9;
}
#upload-btn {
margin-bottom: 20px;
}
#gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 10px;
}
.image-item {
position: relative;
width: 100%;
padding-top: 100%; /* 1:1 ratio */
overflow: hidden;
border: 1px solid #ddd;
border-radius: 8px;
background-color: #fff;
}
.image-item img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
</head>
<body>
<h1>圖片上傳展示</h1>
<input id="upload-btn" type="file" accept="image/*" multiple>
<div id="gallery"></div>
<script>
const uploadBtn = document.getElementById('upload-btn');
const gallery = document.getElementById('gallery');
uploadBtn.addEventListener('change', (event) => {
const files = event.target.files;
Array.from(files).forEach(file => {
const reader = new FileReader();
reader.onload = function(e) {
const imgUrl = e.target.result;
const imageItem = document.createElement('div');
imageItem.classList.add('image-item');
imageItem.innerHTML = `<img src="${imgUrl}" alt="Uploaded Image">`;
gallery.appendChild(imageItem);
};
reader.readAsDataURL(file);
});
});
</script>
</body>
</html>
接下來進到了實在不想重新面對的時刻。F12的console裡的一堆錯誤訊息,實在頭很痛,這步我狂丟ChatGPT逼他解釋。這裡貼幾個給大家瞅瞅
錯誤 400:invalid_request
要求詳情: flowName=GeneralOAuthFlow
這通常是 OAuth 2.0 驗證或授權請求出問題了。
我一開始有在google console開project,並設定了 OAuth 2.0 的ID,也開了api key,但後來發現如果資料夾(以及裡面的檔案)設成公開,前者可以不用。
Unchecked runtime.lastError: The message port closed before a response was received.
據說這是個常見的錯誤訊息,特別是當開發Chrome擴充功能或使用瀏覽器api時會遇到。
奇怪的是,這裡我是使用有學校後綴的google帳號開啟api,用一般的google帳號開啟測試頁面、用學校後綴的測試就不會了。
後來發現,這個錯誤好像不會影響應用程式的主要功能,後來就先放著不管。
Requests from referer http://localhost are blocked
這裡我把測試頁面的網址(就是我的localhost和port號)新增在api key設定允許的HTTP引用網址(HTTP referrer)裡。
[Report Only] Refused to load the script 'https://www.gstatic.com/_/mss/boq-identity/_/js/k=boq-identity.IdpIFrameHttp.zh_TW.ERoUSIbVwj8.es5.O/am=BgM/d=1/rs=AOaEmlEePYgwwGOjUmaOblkA-zkOD6LYAA/m=base' because it violates the following Content Security Policy directive: "script-src 'unsafe-inline' 'unsafe-eval' blob: data:". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.
上面似乎是說因為內嵌script違反了Content Security Policy (CSP) settings,這裡我試著把html和js拆開,但還是失敗,還提到說不能用iframe云云(當初有個錯誤訊息的解方是用iframe或img標籤的Google Drive url,這該死又甜美的矛盾)。
試了好幾天,總算顯示出圖的名稱,但圖片本圖就是錯誤方塊、而且會很快閃現一個正方形的輪廓又消失。
變著法子盤問了ChatGPT,發現似乎跟 CORS(Cross-Origin Resource Sharing,跨來源資源共享)問題有關。
圖片url可能會受到CORS限制,尤其是在從本地或不同域名的網站加載圖片時。所以儘管我可以直接在瀏覽器中打開圖片,但用html載入圖片時,這些限制可能會導致無法顯示。
試著在html加了CORS標頭解決問題,但GPT說「如果只是單純從前端加載 Google Drive 的圖片,這通常很難繞過 CORS 限制」,我不知道有沒有道理,反正最後圖還是載入失敗。
最後的程式碼是這樣的
.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Google Drive Dynamic Image Gallery</title>
<style>
#image-gallery {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
img {
width: 150px;
height: 150px;
object-fit: cover;
border: 1px solid #ccc;
border-radius: 5px;
}
</style>
</head>
<body>
<h1>Dynamic Image Gallery</h1>
<div id="image-gallery"></div>
<img src="https://drive.google.com/uc?id=作品" alt="Test Image">
<iframe src="https://drive.google.com/file/d/作品/view?usp=sharing" width="500" height="500"></iframe>
<script>
// 您的 Google Drive 資料夾 ID
const folderId = "QAQ"; // 替換為您的資料夾 ID
const apiKey = "QAQQ"; // 替換為您的 API Key
const gallery = document.getElementById("image-gallery");
async function fetchImagesFromDrive() {
try {
const response = await fetch(
`https://www.googleapis.com/drive/v3/files?q='${folderId}'+in+parents+and+mimeType+contains+'image/'&key=${apiKey}&fields=files(id,name,mimeType)`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (!data.files || data.files.length === 0) {
console.error("No image files found in the folder.");
return;
}
data.files.forEach(file => {
const imgUrl = `https://drive.google.com/uc?id=${file.id}`;
const img = document.createElement("img");
img.src = imgUrl;
img.alt = file.name;
// 處理載入失敗
img.onerror = () => {
img.alt = "圖片載入失敗";
console.error(`Failed to load image: ${imgUrl}`);
};
const link = document.createElement("a");
link.href = `https://drive.google.com/file/d/${file.id}/view?usp=sharing`;
link.target = "_blank";
link.appendChild(img);
setTimeout(() => {
gallery.appendChild(link);
}, 500); // 延遲 500 毫秒
gallery.appendChild(link);
});
} catch (error) {
console.error("Error fetching images:", error);
}
}
// 呼叫函式
fetchImagesFromDrive();
</script>
</body>
</html>
.js
// 初始化 Google API 客戶端
function initClient() {
gapi.load('client', () => {
gapi.client.init({
apiKey: 'QAQ', // 替換為你的 API 金鑰
}).then(() => {
console.log("Google API client initialized.");
listFiles();
}).catch((error) => {
console.error("Error initializing Google API client:", error);
});
});
}
function listFilesInFolder() {
const folderId = 'QAQQ'; // 替換為你的資料夾 ID
const folder = DriveApp.getFolderById(folderId);
const files = folder.getFiles();
while (files.hasNext()) {
const file = files.next();
Logger.log(`Name: ${file.getName()}, MIME Type: ${file.getMimeType()}`);
}
}
// 列出指定資料夾中的檔案
function listFiles() {
const folderId = 'QAQQ'; // 替換為你的公開資料夾 ID
gapi.client.request({
path: `https://www.googleapis.com/drive/v3/files`,
method: 'GET',
params: {
q: `'${folderId}' in parents and mimeType contains 'image/'`,
fields: 'files(id, name, mimeType, webViewLink)',
key: 'QAQ', // 替換為你的 API 金鑰
},
}).then((response) => {
const files = response.result.files;
const gallery = document.getElementById('image-gallery');
if (files && files.length > 0) {
files.forEach((file) => {
const img = document.createElement('img');
img.src = `https://drive.google.com/uc?id=${file.id}`;
img.alt = file.name;
img.style.width = "150px";
img.style.height = "150px";
img.style.objectFit = "cover";
const link = document.createElement('a');
link.href = file.webViewLink;
link.target = "_blank";
link.appendChild(img);
gallery.appendChild(link);
});
} else {
gallery.innerHTML = '<p>No images found or permission denied.</p>';
}
}).catch((error) => {
console.error("Error listing files: ", error);
});
}
// 啟動 Google API 客戶端
gapi.load('client', initClient);
之後初步研沒辦法從google drive api討到便宜,決定先從讀取本機的圖片做起,這個除了因為路徑最前端多了一個「\」讓我抓狂了快半小時,倒是成功了。但想做到不輸入每張圖片的路徑、讓腳本自己抓取圖片,似乎還有很長一段路要走。
宮崎駿桑說過「越重要的事情就越麻煩」,現在只能先這樣安慰自己了(抱頭