0%

[javascript] Lesson 2 API串接常見問題排除

API串接過程中,並非每次都能順利進行,尤其是當問題來自提供API的那一方時,常見的問題如下:

  • CORS問題
  • 資料數量過大
  • 資料格式為json以外的文字檔案(.txt,.csv,.xml…)

我提供幾個自己的經驗,做為遇到以上問題時的基本排除原則,具體做法還是要看API的狀況來決定。

CORS問題

CORS的問題來自於同源政策引起的,通常是因為提供資料的伺服器端沒有開放非同源存取所造成的,最快的解決方式是透過本地端的伺服器,使用curl或同性質的指令去摸擬http請求,藉此取得資料後再傳遞給前端js程式來使用

1
2
3
$.get("...url...",(data)=>{
console.log(data)
})

同源錯誤訊息

在php中,如果要取得的資料不需要太複雜的參數傳送,只需要 get 方法就可以取得的話,可以使用 file_get_contents() 來快速取得資料

1
echo file_get_contents($url);

如果是要使用POST或需要較複雜的存取行為,或者有效率考量的話,則使用 curl 指令會比較適合

1
2
3
4
5
6
7
8
9
10
11
12
13
// 初始化 CURL
$curl = curl_init();

curl_setopt($curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36");

curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);

curl_setopt($curl, CURLOPT_URL, $url);

$result = curl_exec($curl);

curl_close($curl);

修改前端js的網址為同源的php檔案,即可取得資料

1
2
3
4
$.get("get_data.php",(data)=>{
data=JSON.parse(data)
console.log(data)
})

資料數量過大

以上例來說,資料量有1700多筆,雖然透過網路取得的速度很快,但是在瀏灠器使用js處理大量資料時,還是有可能會造成效能的問題,有幾個解決方向:

  • 查看對方的API規格,是否有提供數量限制的參數,如果有的話,則可以透過傳遞參數的方式來限制每次取得的數量
  • 先在本地端使用後端程式語言處理過資料,縮小資料量或規範資料的內容,再丟回給前端
  • 在本地端使用後端程式語言取得資料後,將資料存入資料庫,再自行撰寫本地端的api供前端使用
    這裏解說存入資料庫的做法:
    先依照資料的欄位在地本端建立資料庫及資料表:

在本地端先取得資料,如果確認對方提供的資料格式是JSON,可以使用 json_decode() 來解析回傳資料

1
2
3
4
5
6
7
8
9
//利用 file_get_contents() 函式抓回資料,資料會以字串的型式回傳
$data=file_get_contents($url);

//使用json_decode()函式把json字串轉換成php陣列
$data=json_decode($data);

echo "<pre>";
print_r($data);
echo "</pre>";

我們發現json_decode後中,陣列中的資料以stdClass的型式存在陣列中,這表示每一項資料都是以類物件的方式被轉換出來,所以我們要以物件的方式來存取資料內容

開始撰寫寫入資料表的語法(再次強調,依據資料的不同,處理方式可能會有極大的差異):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$dsn="mysql:host=localhost;charset=utf8;dbname=culture";
$pdo=new PDO($dsn,'root','password');

//使用迴圈來取出每一筆資料
foreach($data as $d){
$sql="INSERT INTO `art_collections`
(`Identifier`, `MainTitle`, `Creator`, `CreatedYear`, `Size`,
`Type`, `Owner`, `AcquiredDate`, `OriginalUrl`, `OwnerCollectionsWebsite`,
`OwnerWebsite`, `ApplicationGuide`, `Description`, `ImageUrl`)
VALUES
('{$d->Identifier}', '{$d->MainTitle}', '{$d->Creator}', '{$d->CreatedYear}',
'{$d->Size}', '{$d->Type}', '{$d->Owner}', '{$d->AcquiredDate}',
'{$d->OriginalUrl}', '{$d->OwnerCollectionsWebsite}', '{$d->OwnerWebsite}',
'{$d->ApplicationGuide}', '{$d->Description}', '{$d->ImageUrl}')";
$pdo->exec($sql);
}

執行該php程式後,即可在取得資料後將資料寫入本地端的資料表中:

撰寫本地端的api程式,可以自行設定是否要代入參數來取得資料,或是在後端設定好分頁資訊,讓前端存取

1
2
3
4
5
6
$dsn="mysql:host=localhost;charset=utf8;dbname=culture";
$pdo=new PDO($dsn,'root','');

$rows=$pdo->query("SELECT * FROM `art_collections` LIMIT 25 ")->fetchAll(PDO::FETCH_ASSOC);

echo json_encode($rows);

前端改以本地端的api程式來取得資料:

1
2
3
4
$.get("art_collections.php",(data)=>{
data=JSON.parse(data)
console.log(data)
})

改以本地端資料表的方式來取得資料,則資料量和呈現的方式,都可以在本地端自行決定

資料格式非JSON

如果想串接的資料不是回覆json或xml,或者該資料僅有一個獨立的檔案,沒有參數可供調整回傳的內容,這樣其實已經不太算是串接api了,反而比較接近爬資料的方式。
爬蟲和串API的最大差異在於是否提供參數做為資料的區隔,以 中央氣象局開放資料平臺之資料擷取API為例,每一項api的網址點開後,都還有各種詳細的參數可供選擇,透過傳遞不同的參數,同一個API網址可以獲得不同的資料內容。

如果對像資料只是一個資料檔案,而且也不是json格式的資料,那麼可以採取的對應做法就是以檔案處理的方式來進行,也就是先把檔案抓下來本地端,再依照後端程式語言或前端程式語言中的檔案處理機制對檔案進行 解析 ,再看是要存入資料表還是做成json的格式回應給前端,這邊以臺北市立動物園_動物資料為例,該資料僅提供 .csv 版本的資料格式,同時有 CORS 的問題無法直接以JS取得。

.csv 格式有個問題是資料中僅有第一列有標題,其它資料並沒有像 json 一樣的 key-value 格式

因此我們採用後端PHP程式來把資料抓下來,並將資料先存在本地端形成一個檔案,接著我們依照檔案的格式,使用php程式重新整理每一筆資料為json的資料格式,最後再丟給前端。

1
2
3
4
5
6
7
8
9
10
11
//利用 file_get_contents() 函式抓回資料,資料會以字串的型式回傳
$data=file_get_contents($url);

//建立一個本地端的檔案,並且以可寫入的方式開啟
$file=fopen('animal.csv','w');

//使用fwrite() 函式把抓回的資料寫入到本地端檔案中
fwrite($file,$data);

//關閉檔案
fclose($file);

可以在命令列直接執行這個檔案,執行完畢後即可在目錄中看到抓下來的 .csv 檔案

另外撰寫一支PHP程式用來處理這個抓下來的檔案,作為提供給前端呼叫之用,這邊要處理的重點在於幫每一筆資料加上對應的鍵名,以形成一個 json 的格式,前端才能進行處理,由於檔案格式為 .csv 因此我們使用php函式中的 fgetcsv() 來對每一筆資料做解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//以讀取模式開啟檔案
$file=fopen('animal.csv','r');

//先執行fgetcsv($file)一次,用來取得第一列的標題資料
$keyName=fgetcsv($file);

//建立一個陣列變數來存放處理後的資料
$animal=[];

/**
* 使用while的方式在每一次迴圈中重新取得新一列的資料,
* 如果取不到新的資料,表示已到檔案的最後,會回傳false,
* 一但while的判斷式中收到false,就會停止迴圈
*/
while(($data=fgetcsv($file)) !== false){
//每一次的回圈都先建立一個空陣列
$tmp=[];

/**
* fgetcsv()的運作原理是每次取出一列的資料,並根據逗號將資料轉成陣列
* 因此每個$data變數都是一個陣列,而這個$data陣列的元素內容和$keyName
* 的陣列內容應該是互相對應的
* 所以我們使用$keyName去跑一次迴圈,並把對應的$data陣列中的元素加入到
* 暫時的$tmp陣列中,讓$tmp陣列形成一個 key-value 型式的陣列
*/
foreach($keyName as $key => $name){
$tmp[$name]=$data[$key];
}

//將$tmp陣列加入到$animal陣列中
$animal[]=$tmp;

}

//關閉檔案
fclose($file);

//使用json_encode()把陣列轉換為json格式
echo json_encode($animal);


處理成功的資料會以PHP陣列中的 key-value 方式呈現,再使用 json_encode($array) 把陣列轉成 json 格式傳給前端即可

1
2
3
4
5
$.get("animal.php",(data)=>{
data=JSON.parse(data)
console.log(data)
})

補充

這類非json格式的資料,通常的處理方式會傾向抓下來後存到資料庫,再自行撰寫api來使用,但隨之而來的問題是我們不會知道對方的資料何時會更新,因此配套措施是利用作業系統的定時執行功能,每隔一周或一個月就自動去重新抓一次資料並更新資料庫。