本節 beautifulsoup 範例程式, Beauty 板爬蟲範例程式

網頁 = 由標籤 (tag) 所組成的階層式文件

你在瀏覽器看到的美觀網頁,主要由三個部分構成: HTML (網頁的骨架結構)、CSS (網頁的樣式) 與 JavaScript (在瀏覽器端執行,負責與使用者互動的程式功能)。對於網頁或爬蟲的初學者來說,最重要的觀念是了解:網頁就是由各式標籤 (tag) 所組成的階層式文件,要取得所需的網頁區塊資料,只要用 tag 與相關屬性去定位資料所在位置即可。例如以下是一個簡單的網頁及其原始碼:

2016-12-22-1

<html>
  <head>
    <title>我是網頁標題</title>
    <style>
    .large {
      color:blue;
      text-align: center;
    }
    </style>
  </head>
  <body>
    <h1 class="large">我是變色且置中的抬頭</h1>
    <p id="p1">我是段落一</p>
    <p id="p2" style="">我是段落二</p>
    <div><a href='http://blog.castman.net' style="font-size:200%;">我是放大的超連結</a></div>
  </body>
</html>

HTML 文件內不同的標籤 (例如 <title>, <h1>, <p>, <a> 有著不同的語義,表示建構網頁用的不同元件,且標籤可以有各種屬性 (例如 id, class, style 等通用屬性, 或 href 等專屬屬性),因此我們可以用標籤 + 屬性去定位資料所在的區塊並取得資料。關於網頁架構還有另外一件事,就是它是階層式文件,例如以上的網頁架構可以如下表示:

2016-12-22-2

雖然在我們的範例中不會用階層結構去定位資料區塊,但知道這件事有助於你閱讀及理解網頁文件。

BeautifulSoup 入門

BeautifulSoup 是好學易用,用來解構並擷取網頁資訊的 Python 函式庫。給定以上的網頁文件,

html_doc = """
<html>
  <head>
    <title>我是網頁標題</title>
    <style>
    .large {
      color:blue;
      text-align: center;
    }
    </style>
  </head>
  <body>
    <h1 class="large">我是變色且置中的抬頭</h1>
    <p id="p1">我是段落一</p>
    <p id="p2" style="">我是段落二</p>
    <div><a href='http://blog.castman.net' style="font-size:200%;">我是放大的超連結</a></div>
  </body>
</html>
"""

先創建一個 BeautifulSoup 物件,將網頁讀入

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc, 'html.parser')
print(soup)
# <html>
# <head>
# <title>我是網頁標題</title>
# <style>
# .large {
#   color:blue;
#   text-align: center;
# }
# </style>
# </head>
# <body>
# <h1 class="large" style="">我是變色且置中的抬頭</h1>
# <p id="p1">我是段落一</p>
# <p id="p2" style="">我是段落二</p>
# <div><a href="http://blog.castman.net" style="font-size:200%;">我是放大的超連結</a></div>
# </body>
# </html>

接著就可以用 find(), find_all() 搭配 tag 名稱及屬性去定位資料區塊

soup.find('p')            # 回傳第一個被 <p> </p> 所包圍的區塊
# <p id="p1">我是段落一</p>

soup.find('p', id='p2')   # 回傳第一個被 <p> </p> 所包圍的區塊且 id="p2"
# <p id="p2" style="">我是段落二</p>

soup.find(id='p2')        # 回傳第一個 id="p2" 的區塊
# <p id="p2" style="">我是段落二</p>

soup.find_all('p')        # 回傳所有被 <p> </p> 所包圍的區塊
# [<p id="p1">我是段落一</p>, <p id="p2" style="">我是段落二</p>]

soup.find('h1', 'large')  # 找尋第一個 <h1> 區塊且 class="large"
# <h1 class="large" style="">我是變色且置中的抬頭</h1>

find() 只回傳第一個找到的區塊,而 find_all() 會回傳一個 list, 包含所有符合條件的區塊。傳入的引數第一個通常是 tag 名稱,第二個引數若未指明屬性就代表 class 名稱,也可以直接使用 id 等屬性去定位區塊。定位到區塊後,可以取出其屬性與包含的字串值

paragraphs = soup.find_all('p')
for p in paragraphs:
    print(p['id'], p.text)
# p1 我是段落一
# p2 我是段落二

a = soup.find('a')
print(a['href'], a['style'], a.text)
# http://blog.castman.net font-size:200%; 我是放大的超連結

print(soup.find('h1')['class'])  # 因為 class 可以有多個值,故回傳 list
# ['large']

如果你要取得的屬性不存在,直接使用屬性名稱會出現錯誤訊息,因此若你不確定屬性是否存在,可以改用 get() 方法

print(soup.find(id='p1')['style'])      # 會出現錯誤訊息, 因為 <p id="p1"> 沒有 style 屬性
print(soup.find(id='p1').get('style'))  # None

其他詳細用法可參考 BeautifulSoup 的官方文件

使用 Chrome 的開發者工具找到資料區塊的 tag 及屬性

假設你有一個想爬的網頁,要怎麼知道資料區塊所在的標籤及屬性呢?在此我們使用 Chrome 的開發者工具,以 Ptt Web 版 Beauty 板首頁為例,用 Chrome 連上 https://www.ptt.cc/bbs/Beauty/index.html , 接著按下 F12 或從選單啟動開發者工具

2016-12-22-3

下方會跑出開發者工具的操作區,點選左上角的箭頭按鈕後,再點擊網頁上你想要定位的資料區塊,該區塊的 HTML 碼就會顯示在下方。當然你也可以直接檢視網頁原始碼或檢視上一篇教學中用 get_web_page() 所取得的網頁文件,但善用開發者工具可以加速你的搜尋。

2016-12-22-4

2016-12-22-5

PTT Beauty 板範例實戰

檢視網頁原始碼後我們知道,網頁上的每一篇貼文都是由 <div class=”r-ent”> 的區塊包圍起來,裡面分別由 <div class=”nrec”> 區塊顯示推文數,<div class=”title”> 區塊及 <a> 區塊顯示文章連結及文章標題,<div class=”date”> 區塊顯示發文日期

2016-12-20-2

因此,若已經取得網頁文件,我們可以用 find_all() 找出所有<div class=”r-ent”> 區塊,並逐一巡訪,取得資料:

def get_articles(dom, date):
    soup = BeautifulSoup(dom, 'html.parser')

    articles = []  # 儲存取得的文章資料
    divs = soup.find_all('div', 'r-ent')
    for d in divs:
        if d.find('div', 'date').string == date:  # 發文日期正確
            # 取得推文數
            push_count = 0
            if d.find('div', 'nrec').string:
                try:
                    push_count = int(d.find('div', 'nrec').string)  # 轉換字串為數字
                except ValueError:  # 若轉換失敗,不做任何事,push_count 保持為 0
                    pass

            # 取得文章連結及標題			
            if d.find('a'):  # 有超連結,表示文章存在,未被刪除
                href = d.find('a')['href']
                title = d.find('a').string
                articles.append({
                    'title': title,
                    'href': href,
                    'push_count': push_count
                })
    return articles

使用 get_articles() 及上一篇教學的 get_web_page(),取得今日文章資訊

page = get_web_page('https://www.ptt.cc/bbs/Beauty/index.html')
if page:
    date = time.strftime("%m/%d").lstrip('0')  # 今天日期, 去掉開頭的 '0' 以符合 PTT 網站格式
    current_articles = get_articles(page, date)
    for post in current_articles:
        print(post)
# {'push_count': 8, 'title': '[正妹] 韓國瑜的女兒', 'href': '/bbs/Beauty/M.1482411674.A.855.html'}
# {'push_count': 5, 'title': '[正妹] 佐々木優佳里 Happiness', 'href': '/bbs/Beauty/M.1482414319.A.C09.html'}
# {'push_count': 13, 'title': '[正妹] 甜美笑容 第一二五彈', 'href': '/bbs/Beauty/M.1482416491.A.656.html'}
# {'push_count': 0, 'title': '[帥哥] 佐々木小次郎', 'href': '/bbs/Beauty/M.1482417495.A.733.html'}
# {'push_count': 14, 'title': '[正妹] 短髮有可愛 FB女孩Round 255', 'href': '/bbs/Beauty/M.1482419748.A.D25.html'}
# {'push_count': 7, 'title': '[正妹] 筧美和子', 'href': '/bbs/Beauty/M.1482419973.A.32C.html'}
# {'push_count': 58, 'title': '[正妹] 教你公民好嗎', 'href': '/bbs/Beauty/M.1482420690.A.AE7.html'}
# {'push_count': 9, 'title': '[正妹] 熟女了', 'href': '/bbs/Beauty/M.1482420814.A.021.html'}
# {'push_count': 10, 'title': '[正妹] 佐々木琴子', 'href': '/bbs/Beauty/M.1482421163.A.C42.html'}
# {'push_count': 0, 'title': '[正妹] 野生日本賽車女神', 'href': '/bbs/Beauty/M.1482421895.A.F6B.html'}

這樣就取得今天全部的 Beauty 板文章了嗎?聰明的你一定想到了:如果不只首頁,前一頁還有今天的文章怎麼辦?這就留給各位自行練習了 (提示:找到前一頁的連結,連線並取得該頁資料後,一樣用 get_articles 爬取文章資料),我們會在教學結束後的範例提供完整程式碼。下一篇文章會說明如何連結到 current_articles 內的文章,抓圖並計算每一篇文章的貼圖數。


本節完整範例程式請點此處

requirement.txt

範例: PTT Beauty 板今日圖片下載器

PTT Beauty 板今日圖片下載器,會把表特板今天所有文章的圖片下載到本機端,同時儲存一些文章資訊。本系列文章藉由會實作這個範例,說明 Python 網頁爬蟲與資料分析的入門技巧。

套件安裝

首先請確定你的電腦已經安裝 Python 3 以及 pip (本文使用的環境是 Python 3.5.2 與 pip 9.0.1)

> python --version
Python 3.5.2

> pip --version
pip 9.0.1 from c:\virtualenv\ptt-beauty-py35-64\lib\site-packages (python 3.5)

接著安裝所需套件,你可以依照 requirement.txt 中所列的套件一一安裝,也可以一次全部安裝

pip install -r requirement.txt

接著在命令列輸入以下指令,若沒有任何訊息出現則代表套件安裝成功

python -c "import requests; import bs4; import matplotlib"

與網站 Server 溝通並取得網頁資料

PTT Web 版 Beauty 板首頁 https://www.ptt.cc/bbs/Beauty/index.html 在瀏覽器看起來是這樣的

2016-12-20-1

要透過 Python 取得該頁資料,我們使用 requests 套件的 requests.get() 方法, 首先定義 get_web_page() 函式

def get_web_page(url):
    resp = requests.get(
        url=url,
        cookies={'over18': '1'}
    )
    if resp.status_code != 200:
        print('Invalid url:', resp.url)
        return None
    else:
        return resp.text

requests.get() 需要提供網址作為引數, 而 cookies={'over18': '1'} 是 PTT 網站有些板會詢問你是否已滿 18 歲, 因此將回答先存在 cookie 中一併傳給 server. requests.get() 的結果是 request.Response 物件, 我們可以先透過該物件的 statu_code 屬性取得 server 回覆的狀態碼 (例如 200 表示正常, 404 表示找不到網頁等), 若狀態碼為 200, 代表正常回應, 再透過 text屬性取得 server 回覆的網頁內容. 若狀態碼異常則回覆 None.

定義好 get_web_page() 函式之後, 就能呼叫它來取得網頁內容:

page = get_web_page('https://www.ptt.cc/bbs/Beauty/index.html')
if page:
	print(page)

結果為

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>看板 Beauty 文章列表 - 批踢踢實業坊</title>
(...略)
</head>
<body>
<div id="topbar-container">
	<div id="topbar" class="bbs-content">
		<a id="logo" href="/">批踢踢實業坊</a>
		<span>&rsaquo;</span>
		<a class="board" href="/bbs/Beauty/index.html"><span class="board-label">看板 </span>Beauty</a>
		<a class="right small" href="/about.html">關於我們</a>
		<a class="right small" href="/contact.html">聯絡資訊</a>
	</div>
</div>
(...略)
		<div class="r-ent">
			<div class="nrec"><span class="hl f2">3</span></div>
			<div class="mark"></div>
			<div class="title">
				<a href="/bbs/Beauty/M.1482277860.A.10B.html">[正妹] 文學女孩</a>
			</div>
			<div class="meta">
				<div class="date">12/21</div>
				<div class="author">dan025</div>
			</div>
		</div>
		<div class="r-ent">
			<div class="nrec"></div>
			<div class="mark"></div>
			<div class="title">
				<a href="/bbs/Beauty/M.1482285364.A.D29.html">[正妹] 烏茲別克</a>
			</div>
			<div class="meta">
				<div class="date">12/21</div>
				<div class="author">panzer1224</div>
			</div>
		</div>
(...略)

回傳的內容的確是瀏覽器所看到的內容,而且以第一篇貼文為例,我們可以看到它包含了推文數、文章連結、文章標題、貼文日期等我們所需要的資訊。下一篇文章會說明如何使用 BeautifulSoup 套件解構網頁內容,將資料取出。

2016-12-20-2


前言

這系列文章是與 Pycone 松果城市合作,給初學者的網頁爬蟲與資料分析教學,如果你對於 Python 有粗淺認識 (知道 Python 的資料型態, 控制結構, 寫過一些小程式), 想進一步知道要怎麼使用 Python 擷取網頁資訊並簡單做些資料分析 (如圖表、統計資料、相關性等),這系列文章可以帶你入門。一般想要寫網頁爬蟲的人,不會只想要擷取資料,他們真正想要的通常是資料分析,找出資料能提供的資訊,或使用資料驗證自己的假設,Python 也有許多資料處理與展示的好用套件可以使用 (如 NumPy, scikit-learn, pandas),這系列文章會先略過這些套件,教你直接用程式計算統計資料與畫圖,以便讓你更了解套件底層的邏輯,之後學習這些套件時會更容易上手。

步驟拆解

網頁爬蟲與資料分析可以分成以下步驟:

  1. 資料來源: 資料來源可以是別人整理好的資料(如政府 open data, 整理好的 csv 或 json 等文字檔), 也可以是自行從公開網頁擷取的資料 (本文會使用 PTT 網站作為範例)
  2. 啟動爬蟲: 如果資料不是整理好的,而是必須從公開網頁爬取,就必須利用程式與網站 server 連線取得網頁資訊 (本文會示範用 request 套件與 PTT 網站溝通)
  3. 資料擷取與資料淨化: 從公開網頁爬取的整個頁面,通常只有一部分是你需要的,因此要利用程式解構網頁架構,取得所需資料 (本文使用 BeautifulSoup 套件解構網頁文件,擷取所需資料); 另外,在擷取過程中或擷取後,資料通常會有些雜訊 (例如錯誤的時間格式, 英文數字混雜等),此時也要利用程式做資料淨化以便後續分析 (本文會示範利用簡單的正規表示式 regular expression 做資料過濾與淨化)
  4. 資料分析: 爬蟲把 raw data 爬下來之後, 你可能會想要分析資料,例如跑些統計資訊或檢查資料維度間的相關性,驗證你的假設 (本文會示範計算文章內圖片數量與推文數的相關係數)
  5. 資料展示: 用圖表、網頁等展示資料 (本文會示範將 PTT Beauty 版文章內的圖片存到本機端,並畫出文章內圖片數量與推文數的分佈圖)

範例程式: PTT Beauty 板今日圖片下載器

本文會教你實作一個簡單的圖片下載器,它會連上 PTT Web 版的表特板首頁,然後把今天所有文章內含的圖片下載到本機端,同時儲存各文章的標題、推文數、內含圖片數,以便後續資料分析。我們會計算圖片數與推文數的相關係數(是否張貼越多圖片的文章會得到越多推?),並畫出資料分布圖。在過程中你會學到如何用 Python 連線到網站,如何解構網頁文件並擷取、儲存資料,以及資料分析與展示的基本技巧。範例成果如下:

2016-12-19-1

2016-12-19-2

2016-12-19-3

> python analyzer.py
推文數: [18, 20, 0, 0, 3, 6, 2, 12, 1, 13, 11, 5, 0, 20, 1, 7, 6, 2, 2, 0, 0, 32, 10, 13, 9, 2]
圖片數: [3, 7, 1, 12, 9, 1, 2, 13, 0, 5, 27, 5, 1, 8, 0, 1, 14, 2, 3, 2, 1, 25, 3, 14, 27, 2]
相關係數: 0.5259

2016-12-19-4


來了之後我覺得收穫最大的,就是資格考的準備課程,一般 CS 學生大概不覺得考試有什麼困難,我在台大更是第一個學期就考完了資格考 (離散/線代/OS/Archi/Algo/DS)。但是因為我念的 Software Engineering program 隸屬 Informatics department 底下,這整個 department 都怪怪的…嗯我是說這是一個跨領域的學門,即使是軟體工程,也特別偏重 human factor, human computer interaction, social impact 等與社會科學較相關的因素,所以我們的資格考不考 CS 專業科目,相反的,你必須用六個小時回答四道情境或申論題,然後在答案裡展現出你在整個軟體工程領域的學術與實務知識,考試結果有 PhD Pass, Master Pass 與 Fail 三種,教授們對於 PhD Pass 的答案更是有高度的期待,你必須呈現出你的抽象思考與解題框架,只引用參考文獻裡面的知識是不夠的。這是什麼意思呢?假設你要回答以下問題:

你負責規劃開發一個醫療資訊系統,而在軟體開發實務中有所謂的 A 流程/技術與 B 流程/技術,你覺得以這個系統來說,採用 A 或 B 那個比較好?為什麼?

身為一個工程師,我們解決問題的流程通常如下:

  1. 大概了解要解決什麼問題,需要什麼核心功能
  2. 直接 google 網路上最常見的解法,找最多人使用/文件最齊全的技術或工具
  3. 直接把找到的解法套下去用,看看效果如何
  4. 問題不一定要完全解決,只要使用者/利害關係人沒意見,就可以結案。然後接著處理下一個問題,因為現實世界中要解決的問題不只這一個。

好了,套用工程師思維,我很可能會先描述一下我對這個系統的假設,再依我對 A/B 流程或技術的實務經驗或相關論文的理解,選擇其中一個,並說明選擇的理由,為什麼我的選擇比較好,大概幾百字或頂多一頁 A4 就可以答完了。

這個理所當然的回答,大概只能得個 Master Pass.

真正有病的…嗯我是說真正完整的回答,不能只回答問題本身,你要回答的,是問題背後的問題。比如說:

什麼叫做”好”?在你假設的系統需求下,好的定義是什麼?它有可能改變嗎? A/B 流程或技術的步驟或核心是什麼?是它們的哪一個環節比較符合你對於好的定義?就算你選擇了其中一個,另外一個在什麼情況下會比較好?甚至,要符合你所定義的好,採用哪個流程/技術根本不是重點,重點在其他地方?

看出來了嗎?你必須小題大作,題目要你選邊站,但你不能真的只選一邊,你必須要兼顧到兩邊,甚至有可能選哪一邊根本不是重點。所以首先你要把題目不清楚的地方都做好假設,接著拆解問題中專有名詞的概念,架構你答題的框架,最後把問題放入你的框架中回答,過程中參考文獻不是拿來答題用的,你必須先有自己的論點,然後引用文獻支持你的論點。這沒有個兩千字是寫不完的。

如果是在公司或與人口頭溝通時搬出這套,大概就直接被轟出門了,工程師思維的我內心的 OS 也是:誰有空聽你在那邊嘴這麼多?我甚至覺得雖然這跟寫論文所需的 critical thinking 有關,但很多時候論文只是起源於一個待解的問題,把解法想出來,related work, motivation 跟 evaluation 補一補就寫好了,要嘴到這樣也太走火入魔了吧!但是,我從一開始的不以為然,慢慢地也學著接受這個考驗,除了考試就是這麼現實以外,同時它也是一種思考訓練,最重要的是,它是訓練我運用文字跟嘴砲的好機會,因為從古希臘以降的西方哲學告訴我們,這個世界就是嘴出來的,你雖然不屑,但你會發現:你想嘴的時候還嘴不出來,因為你念的書不夠多

這對我來說比考數學或專業科目難多了,根本是知識累積跟英文寫作的雙重難題,但我很樂意接受這個挑戰,畢竟這就是我來的目的。


在 UCI 的第一個 quarter 結束了。身為一個在台灣也念過一陣子博士班的人,對於在國內外念書尤其是做研究的環境差異有些粗淺的經驗分享。

學習動機

首先是(國際)學生的學習動機。漂洋過海來的國際學生,在繁複的申請流程中就已經初步確認了動機,畢業後的目標也很明確,到校後就算沒有動機,為了畢業,課業上也不太會打混,這很明顯的反映在全班平均成績上。在大家都滿認真的情況下,自己相對地也會比較辛苦。另一方面,我最喜歡的是 lab meeting 時討論的氣氛,因為美國碩士可以不用寫論文,而大部分目標放在工作的碩士生,沒興趣也不需要做研究,換句話說,會來參與討論的都是對研究有興趣的人。這個制度一方面讓博士生有更大的 lab 空間 (碩士生沒有座位),一方面又可以減少參加 lab meeting 的人數,讓會議得以更有效率及建設性地進行。在台灣因為碩士也要寫論文,如果學生對研究沒興趣卻被逼著做研究,跟他們在研究上的互動簡直是對彼此的折磨 (報個 paper 報得亂七八糟,問個問題也一問三不知)。但是這是否代表台灣也該引進 course track 的碩士制度?不一定,因為台灣跟美國的環境不同,來美國念碩士的國際學生,很多人根本已經有數年工作經驗,或這是他們的第二碩士,換言之,他們已經有解決問題的能力,只是需要學位當作工作的敲門磚而已;而台灣碩士班的學生大多是大學直升,學習動機也有差,寫論文能學到的解決問題的能力也的確是修課(聽講、作業、考試)學不到的。

修課壓力

其次是修課,UCI 是 quarter 制,以一般 CS 相關課程來說,在 10 週之內會完成 3 次程式作業加期中期末考,所以通常一開學就會處在備戰狀態了,跟學期制前兩週還在收心閒晃決定要修什麼課的氣氛差很多,也因為節奏緊湊的關係,修個三門主課就大概會耗掉全部的課餘時間,因為你沒有太多時間慢慢準備作業跟考試。壓力是有的,但我個人反而比較喜歡這種節奏,可以早點把 coursework 處理完,然後專心在研究上。對於有工作經驗的人來說,修課不是什麼難題;對博士生來說,修課對研究也不一定有幫助,所以能早點結束比較好。對於只是需要學位求職的人來說,碩士通常 4 個 quarter (1年3個月) 就可以拿到,也比較理想。

討論課與大量閱讀

上面提到的”修課不是什麼難題”指的是類似我們在台灣上的傳統課程(聽講、作業、考試),但是這裡有另外一種課程是類似討論課,也就是讓你課前讀 paper 寫摘要評論,課堂上發言討論的,對我這種習慣傳統 CS 課程的學生來說就會痛苦很多,一方面是讀寫說的速度都不如 native speaker,一方面是不習慣上課發言。像我這個 quarter 剛好修到兩門都是這種的,一個禮拜要讀 6-8 篇 paper,課前要繳交摘要評論,上課太安靜也會被點名發言。尤其如果你的 program 或 department 有些跨到社會科學領域,像我所在的 Software Engineering program / Informatics department,就一堆這種課,有的甚至是混合,不只要讀 paper 寫評論,程式作業也沒少的。我只能說我的修課壓力大部分來自這種課,不過經過第一個 quarter 的洗禮之後,我體認到這種課才是我,或說一個博士生需要的,因為這剛好可以增加我的知識廣度、訓練我的思考跟表達。當然對於碩士生來說就不一定需要。

研究發表

以博士生最主要的工作跟畢業門檻:發 paper 來說,雖然我還沒有資格多嘴,但我可以肯定的是在台灣念絕對能發 top conference or journal paper,我在台大跟清大實驗室的同學跟學長們就是最好的實例,我自己之前在台大做的東西最近也上了不錯的 conference;找教職方面則不得而知,但台大電機與清大資工近年也聘了幾個土博助理教授。單純就發表來說在台灣念是沒有問題的。

經濟支持

最後還是要回到現實面,我覺得支持博士生自保生活無虞,應該是對於學校最基本的要求,這已經跟直接就業能賺到的錢比起來少很多了。我個人經驗是:在美國雖然拿的錢仍是低收入戶等級,一般博士生的 support package (e.g. 學費全免, 做 TA 或 RA 賺生活費) 至少能自保生活無虞;在台灣則很難說,跟你研究的領域是否熱門、老闆接計畫的能力、組織是否有錢 (例如中研院的博士學程) 高度相關,大好大壞都有,而我是偏壞的那邊。或許我也該感謝這個因素推了我一把。 (待續)