---
# System prepended metadata

title: COVID-19 最新資訊 網頁產生器
tags: [java, covid-19]

---

# COVID-19 最新資訊 網頁產生器

###### tags: `java` `covid-19`

## 目錄

1. [需求定義](#需求定義)
   - [確認COVID-19資料來源](#確認COVID-19資料來源)
   - [確認規格](#確認規格)
2. [程式開發](#程式開發)
   - [完整程式碼](#完整程式碼)
3. [測試](#測試)
4. [發佈](#發佈)




## 需求定義

### 確認COVID-19資料來源

來自美國「約翰·霍普金斯大學（Johns Hopkins University）」分校「惠廷工程學院（Whiting School of Engineering）」的每日更新的新冠肺炎COVID-19全球疫情資訊，檔案為：CSV（Comma-Separated Values）格式。

> Whiting School of Engineering wiki: https://en.wikipedia.org/wiki/Whiting_School_of_Engineering
>
> CSV wiki: https://en.wikipedia.org/wiki/Comma-separated_values

##### GitHub位址

```
https://github.com/CSSEGISandData/COVID-19/tree/master/csse_covid_19_data/csse_covid_19_daily_reports
```

##### 原始擋下載位置

```
https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_daily_reports/{資料日期}.csv
```

資料日期格式為：月-日-西元年，且日、月只有個位數需要補零，例如：

```
08-23-2021
12-24-2020
```





## 網頁定版
再開始開發以前，通常會由網頁設計人員將網頁的版型做一個切版後再由程式設計師套上網頁的動態內容（如本例子的資料更新日期、確診人數以及死亡人數等等）。


#### HTML原始碼
```
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" 
          rel="stylesheet" 
          integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" 
          crossorigin="anonymous">
	<link rel="stylesheet"
    href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
    <style>
      body {text-align: center; background-color: aliceblue;} 
      h1   {font-size: 3em; margin: 0.8em;} 
      p    {font-size: 1.5em; margin: 1em;}
    </style>
  </head>
  <body>
    <h1 class="animate__animated animate__bounce">全球武漢肺炎狀況最新資訊</h1>
    <img class="animate__animated animate__slideInLeft" 
         src="https://www.cdc.gov.tw/Uploads/3d07c50d-5769-4cac-9b26-95a50589160e.png" 
         width="600px"/><br/>
    <p>資料更新日期: 2021-08-27<br/>
       確診人數: 212540765<br/>
       死亡人數: 4441418<br/>
    </p>
    <iframe width="540" height="260" 
            src="https://www.youtube.com/embed/FHSKtyfqGJU" frameborder="0" 
            allow="accelerometer; autoplay; clipboard-write; encrypted-media; 
            gyroscope; picture-in-picture" allowfullscreen></iframe>
</body>
</html>
```

1. 使用bootstrap介面套件，CDN：https://getbootstrap.com/
2. 使用animate動畫css套件，CDN：https://animate.style/
3. 嵌入台灣衛福部COVID-19宣傳圖片：https://www.cdc.gov.tw/Uploads/3d07c50d-5769-4cac-9b26-95a50589160e.png
4. 文字訊息：資料更新日期、確診人數、死亡人數。
5. 嵌入Youtube COVID-19 MV影片：https://www.youtube.com/watch?v=FHSKtyfqGJU

> **注意**
>
> 因為tag內的屬性值如果使用雙引號，會和Java的字串雙引號語法相衝突，所以建議在HTML的tag屬性值使用單引號，否則在Java字串內如果出現雙引號，就必須使用跳脫碼（\）來表示了。



#### 完成畫面

![](https://i.imgur.com/reiX3Me.png)



#### 規格確認

封裝成套件：covid19

類別名稱：Web

1. 依照目前日期組合成特定字串。
2. 下載遠端COVID-19 CSV檔案。
3. 解析CSV檔案取取出需要的資訊。
4. 將解析後產出的資料透過網頁樣板產生網頁。
5. 將網頁存成檔案：index.htm
6. main()方法建立Web物件並執行所有流程。



## 程式開發

##### 使用到的Java第三方套件

OpenCSV: https://sourceforge.net/projects/opencsv/

Apache common-lang3: https://mvnrepository.com/artifact/org.apache.commons/commons-lang3/3.12.0

> **備註**
>
> Apache common-lang為OpenCSV的依賴套件

> **提示**
>
> 將下載的jar檔複製到專案中的lib目錄下即可。



> **注意**
>
> 套件名稱不可以有符號，例如：「-」等等。

#### 完整程式碼

##### covid19/Web.java

```
package covid19;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Map;
import java.util.HashMap;

import com.opencsv.CSVReader;
import com.opencsv.exceptions.CsvValidationException;

public class Web {
    private final String HTML_TEMPLATE = "<!DOCTYPE html>" +
    "<html>" +
    "   <head>" +
    "      <meta charset='utf-8' />" +
    "      <link href='https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css' 
    "            rel='stylesheet' integrity='sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We'" + 
    "             crossorigin='anonymous'>    " +
    "      <style>" +
    "         body {text-align: center; background-color: aliceblue;}" +
    "         h1   {font-size: 3em; margin: 0.8em;}" +
    "         p    {font-size: 1.5em; margin: 1em;}" +
    "      </style>" +
    "      <link" +
    "         rel='stylesheet'" +
    "         href='https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css'" +
    "      />" +
    "   </head>" +
    "   <body>" +
    "      <h1 class='animate__animated animate__bounce'>全球新冠肺炎最新資訊</h1>" +
    "      <img class='animate__animated animate__slideInLeft'" +
    "           src='https://www.seccm.org.tw/files/index_banner/20200323_002.jpg'   />" +
    "           width='600px'" +
    "      <p>資料更新日期: %s<br/>" +
    "         確診人數: %d<br/>" +
    "         死亡人數: %d" +
    "      </p>" +
    "      <iframe width='540' height='260' src='https://www.youtube.com/embed/hZIRuOZkVtY' title='YouTube video player' frameborder='0' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' allowfullscreen></iframe>" +
    "   </body>" +
    "</html>";

    // 要下載的csv網址
    final String URL_TEMPLATE = 
                     "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_daily_reports/%s.csv";
    
    final String KEY_CONFIRMED  = "confirmed"; // 用在Map的確診人數的key
    final String KEY_DEATH      = "death";     // 用在Map的死亡人數的key
    
    final String HTML_FILENAME  = "index.html";// 網頁檔名

	// 建構式
    public Web() {

    }

    public static String dayStr(int offset) {
        final Calendar cal = Calendar.getInstance(); // 取得今天日期時間
        cal.add(Calendar.DATE, offset);

        // 將Calendar物件轉成日期字串並回傳
        // 1. new SimpleDateFormat("MM-dd-yyyy") 建立SimpleDataFormat並設定日期格式
        // 2. format() 將DateTime物件傳給SimpleDateFormat
        // 3. cal.getTime() 將Calendar物件轉成DateTime物件
        // 4. return 將轉換完成的自傳回傳
        return new SimpleDateFormat("MM-dd-yyyy").format(cal.getTime());
    }

    protected CSVReader downloadCsv(String dayStr) 
    						throws CsvValidationException, IOException {
        // 產生格式化後的網址
        String strUrl = String.format(URL_TEMPLATE, dayStr);
        System.out.println("URL=" + strUrl); // 測試用

        // 建立URL物件
        URL url = new URL(strUrl);
        
        // 透過HttpURLConnection開始連線
        HttpURLConnection connection = (HttpURLConnection)url.openConnection();
        
        // 判斷連線使否成功(HTTP回應碼為200)
        if (connection.getResponseCode() == 200) {
	        // 取得連線的stream並建立stream reader
            InputStreamReader isr = new InputStreamReader(connection.getInputStream());

			// 將stream reader傳給CSVReader
            CSVReader csvReader = new CSVReader(isr);
            csvReader.readNext(); // 讀出標題

            return csvReader;
        }

        System.out.println("下載失敗");
        return null; // 連線失敗，回傳空物件
    }

    protected Map<String, Integer> parseCsv(CSVReader csvReader)
                            throws IOException, CsvValidationException {
        int confirmed = 0; // 全球確診人數總和
        int death = 0;     // 全球死亡人數
        
        String[] line = null;
        
        // 遍歷全球新冠肺炎CSV資料
        while ((line = csvReader.readNext()) != null) {
            // System.out.println(Arrays.toString(line) + line.length);

            confirmed += Integer.parseInt(line[7]); // 將確診人數加起來
            death += Integer.parseInt(line[8]);     // 將死亡人數加起來
        }

        // 測試用
        System.out.println("確診人數:" + confirmed);
        System.out.println("死亡人數:" + death);

		// 關閉CSVReader，檔案相關操作，都需要做close動作
        csvReader.close();

        Map<String, Integer> result = new HashMap<>();
        result.put(KEY_CONFIRMED, confirmed); // 存入確診人數資料
        result.put(KEY_DEATH, death);         // 存入死亡人數資料

        return result;
    }

    protected boolean exportToHtmlFile(String date, int confirmed, int death) {
        // 將日期字串，確診人數、死亡人數帶入網頁樣板並產生最終網頁
        String htmlStr = String.format(HTML_TEMPLATE, date, confirmed, death);

        try {
        	// 輸出到檔案，建立FileWriter物件
            FileWriter fw = new FileWriter(filename);
            
            // 將網頁內容寫到檔案
            fw.write(htmlStr);
            
            // 關閉檔案
            fw.close();
        } catch(IOException e) {
            System.out.println("輸出網頁失敗:" + e.getMessage());
            return false;
        }
        
        return true;
    }

    // 產生網頁
    public void gen() {
        try {
            CSVReader csvReader = null;
            int times = 0; // 下載CSV的次數
            String dayStr = "";
            
            // 從今天日期開始下載CSV，一直到成功或是超過前9天為止
            while(csvReader == null && times++ < 10) {
                dayStr = dayStr(-times); // 組合日期字串
                System.out.println("第" + times + "嘗試下載COVID-19資料(" + dayStr + ")"); 
                
                // 嘗試下載CSV檔案
                csvReader = downloadCsv(dayStr);
            }

            if(csvReader == null) {
                System.out.println("無法取得COVID-19資料");
                return; // 結束這個方法
            }

			// 解析CSV資料並算出全球確診人數和死亡人數
            Map<String, Integer> result = parseCsv(csvReader);

            if(exportToHtmlFile(dayStr, 
            					result.get(KEY_CONFIRMED), 
            					result.get(KEY_DEATH)))
                System.out.println("成功建立COVID-19網頁(index.html).");
        } catch(IOException e) {
            System.out.println("錯誤: " + e.getMessage());
        } catch(CsvValidationException e) {
            System.out.println("錯誤: " + e.getMessage());
        }
    }
}

```

##### App.java

```
import covid19.Web;

public class App {
    public static void main(String[] args) {
        new Web().gen();
    }
}
```



## 測試

因為是純靜態網頁，可以透過Google Chrome瀏覽器直接開啟index.html檔案即可。



## 發佈

免費雲端網站**000webhost**：https://www.000webhost.com/



## 練習

1. 將日期顯示格式改成台灣人習慣的閱讀方式，例如:2021年8月27日
2. 將台灣的確診人數和死亡人數也加到網頁。

### 程式碼

#### HTML樣板
```
<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8' />
    <link href='https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css' 
          rel='stylesheet'
          integrity='sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We' 
          crossorigin='anonymous'>
    <style>
        body {
            text-align: center;
            background-color: aliceblue;
        }

        h1 {
            font-size: 3em;
            margin: 0.8em;
        }

        h3 {
            font-size: 2em;
            margin: 0.8em;
        }

        p {
            font-size: 1.5em;
            margin: 1em;
        }

        table {
            width: 100%;
            font-size: 1.5em;
            margin-bottom: 1em;
        }
    </style>
    <link rel='stylesheet' 
    href='https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css' />
</head>

<body>
    <h1 class='animate__animated animate__bounce'>全球新冠肺炎最新資訊</h1> 
    <img width='600px'
         src='https://www.seccm.org.tw/files/index_banner/20200323_002.jpg' />
    <h3>資料更新日期: 2021年8月27日</h3>
    <table>
        <tr>
            <td style='text-align: right; padding-right: 0.5em;'>全球確診人數:</td>
            <td style='text-align: left;'>215448137</td>
        </tr>
        <tr>
            <td style='text-align: right; padding-right: 0.5em;'>全球死亡人數:</td>
            <td style='text-align: left;'>4486020</td>
        </tr>
        <tr>
            <td style='text-align: right; padding-right: 0.5em;'>台灣確診人數:</td>
            <td style='text-align: left;'>15954</td>
        </tr>
        <tr>
            <td style='text-align: right; padding-right: 0.5em;'>台灣死亡人數:</td>
            <td style='text-align: left;'>833</td>
        </tr>
    </table> 
    
    <iframe width='540' height='260' 
        src='https://www.youtube.com/embed/hZIRuOZkVtY'
        title='YouTube video player' frameborder='0'
        allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
        allowfullscreen></iframe>
</body>
</html>
```

#### App.java
```
import covid19.Web;

public class App {
    public static void main(String[] args) {
        new Web().gen();
    }
}
```

#### Web.java
```
package covid19;

import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;

import com.opencsv.CSVReader;
import com.opencsv.exceptions.CsvValidationException;

public class Web {
    private final String HTML_TEMPLATE = "<!DOCTYPE html>" +
    "<html>" +
    "   <head>" +
    "      <meta charset='utf-8' />" +
    "      <link href='https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css' rel='stylesheet' integrity='sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We' crossorigin='anonymous'>    " +
    "      <style>" +
    "         body {text-align: center; background-color: aliceblue;}" +
    "         h1   {font-size: 3em; margin: 0.8em;}" +
    "         h3   {font-size: 2em; margin: 0.8em;}" +
    "         p    {font-size: 1.5em; margin: 1em;}" +
    "         table{width: 100%%; font-size: 1.5em; margin-bottom: 1em;}" +
    "      </style>" +
    "      <link" +
    "         rel='stylesheet'" +
    "         href='https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css'" +
    "      />" +
    "   </head>" +
    "   <body>" +
    "      <h1 class='animate__animated animate__bounce'>全球新冠肺炎最新資訊</h1>" +
    "      <img width='600px' src='https://www.seccm.org.tw/files/index_banner/20200323_002.jpg'   />" +
    "      <h3>資料更新日期: %s</h3>" +
    "      <table>" +
    "          <tr><td style='text-align: right; padding-right: 0.5em;'>全球確診人數:</td><td style='text-align: left;'>%d</td></tr>" +
    "          <tr><td style='text-align: right; padding-right: 0.5em;'>全球死亡人數:</td><td style='text-align: left;'>%d</td></tr>" +
    "          <tr><td style='text-align: right; padding-right: 0.5em;'>台灣確診人數:</td><td style='text-align: left;'>%d</td></tr>" +
    "          <tr><td style='text-align: right; padding-right: 0.5em;'>台灣死亡人數:</td><td style='text-align: left;'>%d</td></tr>" +
    "      </table>" +
    "      <iframe width='540' height='260' src='https://www.youtube.com/embed/hZIRuOZkVtY' title='YouTube video player' frameborder='0' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' allowfullscreen></iframe>" +
    "   </body>" +
    "</html>";

    // 要下載的csv網址
    private final String urlTemplate = 
                      "https://raw.githubusercontent.com/CSSEGISandData/" +
                      "COVID-19/master/csse_covid_19_data/" + 
                      "csse_covid_19_daily_reports/%s.csv";

                                      
    public static final String COUNTRY_TAIWAN = "Taiwan";
    public static final String KEY_CONFIRMED = "confirmed"; // 用在Map的確診人數的key
    public static final String KEY_DEATH     = "death";     // 用在Map的死亡人數的key

    // 用在Map的確診人數的key
    public static final String KEY_TAIWAN_CONFIRMED = "taiwan_confirmed"; 
    
    // 用在Map的死亡人數的key
    public static final String KEY_TAIWAN_DEATH     = "taiwan_death";
    private final String HTML_FILENAME = "index.html";// 網頁檔名

    // 建構式
    public Web() {

    }

    // 取得指定天的字串，格式為: 月-日-西元年
    public static String dayStr(int offset) {
        Calendar cal = Calendar.getInstance(); // 取得今天日期時間
        cal.add(Calendar.DATE, offset);

        // 將Calendar物件轉成日期字串並回傳
        // 1. new SimpleDateFormat("MM-dd-yyyy") 建立SimpleDataFormat並設定日期格式
        // 2. format() 將DateTime物件傳給SimpleDateFormat
        // 3. cal.getTime() 將Calendar物件轉成DateTime物件
        // 4. return 將轉換完成的自傳回傳
        return new SimpleDateFormat("MM-dd-yyyy").format(cal.getTime());
    }

    // 取得指定天的字串，格式為: 月-日-西元年
    public static String dayStrFriendly(int offset) {
        Calendar cal = Calendar.getInstance(); // 取得今天日期時間
        cal.add(Calendar.DATE, offset);

        // 將Calendar物件轉成日期字串並回傳
        // 1. new SimpleDateFormat("MM-dd-yyyy") 建立SimpleDataFormat並設定日期格式
        // 2. format() 將DateTime物件傳給SimpleDateFormat
        // 3. cal.getTime() 將Calendar物件轉成DateTime物件
        // 4. return 將轉換完成的自傳回傳
        return new SimpleDateFormat("yyyy年M月d日").format(cal.getTime());
    }

    public CSVReader downloadCsv(String dayStr) 
            throws MalformedURLException, IOException, CsvValidationException {
        // 產生格式化後的網址
        String strUrl = String.format(urlTemplate, dayStr);
        System.out.println(strUrl); // 測試用

        // 建立URL物件
        URL url = new URL(strUrl);

        // 透過HttpURLConnection開始連線
        HttpURLConnection connection = (HttpURLConnection)url.openConnection();

        // 判斷連線使否成功(HTTP回應碼為200)
        if(connection.getResponseCode() == 200) {
            // 取得連線的stream並建立stream reader
            InputStreamReader isr = new InputStreamReader(connection.getInputStream());

            // 將stream reader傳給CSVReader
            CSVReader csvReader = new CSVReader(isr);

            String[] title = csvReader.readNext(); // 讀出標題
            System.out.println(Arrays.toString(title)); // 測試用

            return csvReader;
        }

        System.out.println("下載失敗");
        return null; // 連線失敗，回傳空物件
    }

    public Map<String, Integer> parseCsv(CSVReader csvReader) 
                                throws IOException, CsvValidationException {
        int confirmed = 0; // 全球確診人數總和
        int death = 0;     // 全球死亡人數
        
        String[] line = null;
        Map<String, Integer> result = new HashMap<>();

        // 遍歷全球新冠肺炎資料
        while((line = csvReader.readNext()) != null) {
            confirmed += Integer.parseInt(line[7]); // 將確診人數加起來
            death += Integer.parseInt(line[8]);     // 將死亡人數加起來

            if(line[3].indexOf(COUNTRY_TAIWAN) == 0) {
                result.put(KEY_TAIWAN_CONFIRMED, Integer.parseInt(line[7]));
                result.put(KEY_TAIWAN_DEATH, Integer.parseInt(line[8]));
            }
        }

        // 測試用
        System.out.println("全球確診人數:" + confirmed);
        System.out.println("全球死亡人數:" + death);
        System.out.println("台灣確診人數:" + result.get(KEY_TAIWAN_CONFIRMED));
        System.out.println("台灣死亡人數:" + result.get(KEY_TAIWAN_DEATH));

        // 關閉CSVReader，檔案相關操作，都需要做close動作
        csvReader.close();
        
        result.put(KEY_CONFIRMED, confirmed); // 存入確診人數資料
        result.put(KEY_DEATH, death);         // 存入死亡人數資料

        return result;
    }

    public boolean exportToHtmlFile(String date, 
                                    int confirmed, 
                                    int death, 
                                    int taiwanConfirmed, 
                                    int taiwanDeath) {
        // 將日期字串，確診人數、死亡人數帶入網頁樣板並產生最終網頁
        String htmlStr = String.format(HTML_TEMPLATE, date, confirmed, death, taiwanConfirmed, taiwanDeath);

        // 輸出到檔案，建立FileWriter物件
        try {
            FileWriter fw = new FileWriter(HTML_FILENAME);
            
            // 將網頁內容寫到檔案
            fw.write(htmlStr);

            // 關閉檔案
            fw.close();
        } catch(IOException e) {
            System.out.println("輸出網頁失敗:" + e.getMessage());
            return false;
        }

        return true;
    }

    public void gen() {
        try
        {
            CSVReader csvReader = null;
            int times = 0; // 下載CSV的次數
            String dayStrFriendly = "";

            // 從今天日期開始下載CSV，一直到成功或是超過前9天為止
            while(csvReader == null && times++ < 10) {
                String dayStr = dayStr(-times); // 組合日期字串
                dayStrFriendly = dayStrFriendly(-times);
                System.out.println("第" + times + "次嘗試下載COVID-19資料(" + dayStr + ")");

                // 嘗試下載CSV檔案
                csvReader = downloadCsv(dayStr);
            }

            if(csvReader == null) {
                System.out.println("無法取得COVID-19資料");
                return; // 結束這個方法
            }

            // 解析CSV資料並算出全球確診人數和死亡人數
            Map<String, Integer> result = parseCsv(csvReader);
           

            if(exportToHtmlFile(dayStrFriendly, result.get(KEY_CONFIRMED), result.get(KEY_DEATH), 
                                        result.get(KEY_TAIWAN_CONFIRMED), result.get(KEY_TAIWAN_DEATH))) {
                System.out.println("成功建立COVID-19網頁(index.html).");
            }
        } catch(IOException e) {
            System.out.println("錯誤:" + e.getMessage());
        } catch(CsvValidationException e) {
            System.out.println("錯誤:" + e.getMessage());
        }

    }
}
```