You are on page 1of 111

1 準備工作 1

1 準備工作

在我們開始應用前,由於我們接下來會常常使用到第三方函式庫,那請你一定要了解 Python
環境該如何安裝函式庫。以下如果有使用輔助工具,都以 PyCharm 為主

1.1 必要安裝

請在開始前準備好下方兩個軟體

1. Python 翻譯器: 請到官網 https://www.python.org/ (如果 Windows 直接在官網下


載會是 32 位元版本) 或者直接 Google 版本 (e.g. 3.6 版 https://www.python.org/
downloads/release/python-360/ ) 安裝,建議 Windows 讀者安裝 64 位元版本,因
為許多函式庫 (e.g. TensorFlow) 只支援 64 位元版本

2. PyCharm 輔助工具: 我個人比較習慣使用 PyCharm 作為我的開發輔助工具,如果讀者有自


己的喜好也沒關係,請到 https://www.jetbrains.com/pycharm/ 下載 Community 版

以下幫大家快速帶過安裝方式,如果有的讀者就可以跳過下面兩個小節

1.1.1 (參考) Python 32bit v.s. 64bit (Windows 作業系統)

Windows 的同學要特別注意版本,雖然在基礎語法我建議你使用 32-bit,那是因為你還不熟,


現在你比較熟了,也知道 32 位元和 64 位元的差別了,我會建議你改使用 64 位元,因為有些函式
庫只支援 64 位元,如果在官網首頁下載會是 32-bit,請牢記一下!
1 準備工作 2

圖: 3.4.3 官網版本

1.1.2 (參考) PyCharm

以下是開發環境的參考安裝步驟
https://www.jetbrains.com/pycharm/ 官方網站下載 community 版本
接著安裝步驟如下
1 準備工作 3

圖: 這裡要記得做出選擇

選好以後啟動程式
1 準備工作 4

圖: 選完佈景主題 skip

1.2 虛擬環境

在開始前,我們要探討一下『虛擬環境』和『非虛擬環境』,因為這會影響我們安裝函式庫的
方式,PyCharm 在開啟 Python 專案的時候預設會使用『虛擬環境』,簡單來說『虛擬環境』就是
『為每一個專案準備一個乾淨的初始環境』,先用下面這張圖表示一下不用虛擬環境的情況
1 準備工作 5

圖: 不用虛擬環境的情況

你會發現有以下重大優缺點

1. 優點: 安裝過的函式庫不用重複安裝,開多少個『專案』都可以吃得到
2. 缺點: 當你要告訴別人如何架設環境的時候,我們會直接對你的 Python 翻譯器產生一份『需
求文件』
,這份需求文件會記錄所有安裝過的函式庫,問題你給別人的專案根本只用到其中幾

1 準備工作 6

圖: 使用虛擬環境的情況

那優缺點剛好反過來

1. 優點: 每個專案都是『乾淨』的,
『需求文件』也是
2. 缺點: 每個專案都必須重新安裝函式庫

,所以 PyCharm 預設
那由於我們在真正開發的時候,對於『乾淨』的需求通常是『最高優先』
,如果你想偷懶,不想使用,可以使用下面的方式調整 (讀者自行決定是否使
也會使用『虛擬環境』
用虛擬環境)
1 準備工作 7

圖: 不用虛擬環境步驟 1

圖: 不用虛擬環境步驟 2
1 準備工作 8

1.3 安裝函式庫方式

以下都以 PyCharm 為主要講解,如果有設定 PATH 的讀者請自行換成命令列模式

1.3.1 方式 1. 使用圖形介面

PyCharm 請你先來到 File - Settings,MAC 電腦的同學請按上面的 PyCharm - Preference


接下來照著下面的圖選到 Project XXX - Project Interpreter

圖: Project Interpreter

接著點擊下面的 + 來搜尋函式庫
1 準備工作 9

圖: 選下面的 +

搜尋 jieba 並且按下 install package


1 準備工作 10

圖: 選擇 install package

最後會出現綠色的成功,就是安裝完畢了
1 準備工作 11

圖: 成功安裝

1.3.2 方式 2. 使用命令

上面的方式在搜尋函式庫的時候其實是到 https://pypi.org/ 這網站去搜尋,但是有的時


候出 bug 的時候,pypi 會更新的不夠即時,這時候我們就需要到作者的 github 直接安裝,建議可
以配合 PyCharm 執行命令,這樣不管虛擬或者非虛擬環境皆可使用,我以最後一章節的 pytube 為
例,請先選到 PyCharm 下方的 Terminal

圖: 下的命令
1 準備工作 12

圖: 成功安裝

簡單來說下的指令就是

python -m pip install git+(你找到的作者 github 網址).git

整個意思是執行 python.exe 並且命令 python.exe 執行 pip 模組做一個 install 的動作

1.3.3 方式 3. 使用 whl (安裝 64 位元 Python 比較不會需要)

有些使用 32 位元的讀者會發現在安裝某些函式庫的時候會遇到 Microsoft Visual C++ 14.0 is


required. 錯誤,這問題是有些 Python 32 位元函式庫裡面有 C 程式碼,並且還未被編譯過 (64 位元
的版本通常都會已經準備好編譯過的檔案)。理論上安裝新版的 Builds Tools 即可解決問題,但有時
候會因為你 Windows 版本而難以成功,那只好去網路上尋找好心人幫我們編過的檔案,你可以到
https://www.lfd.uci.edu/~gohlke/pythonlibs/ (Google “whl lfd”) 找尋你想要的函式庫
並下載

圖: 完整步驟
1 準備工作 13

1.4 Jupyter Notebook

有時候我們在做一個複雜程式的時候,很難一次寫到完,或者是你想要把你所有的實驗步驟
寫成一份文件,這時候 Jupyter Notebook 就會是你最佳的利器,他可以讓你一個步驟一個步驟執
行程式,本書的程式也是由 Jupyter Notebook 撰寫,那這邊教學一下如何用 PyCharm 跟 Jupyter
Notebook 連動,請先利用上面的方式安裝 jupyter 函式庫,再接著下面的圖操作

圖: 開啟 jupyter 伺服器步驟

Jupyter Notebook 的原理是在某個資料夾幫你架設一個伺服器 (你只看得到這資料夾以後的東


西),好處是你可以用瀏覽器直接來當你的輔助工具使用,額外的好處是經過設置後你到處都可以
連回來寫程式,也因此,他每次開伺服器的時候都會給出一個不一樣的 token,你就可以利用網址
+token 直接開始寫程式,另外一個要注意的點!!!是如果你瀏覽器也開啟同一支程式,請你把
PyCharm 的檔案關掉,不然會出現『有兩個狀態』的問題,稍一不慎選錯後續的選擇,你辛辛苦苦
的程式就會被覆蓋,另外一個要注意的是,請千萬寫到一個階段利用 CheckPoint 幫你保存!
1 準備工作 14

圖: 絕對要記得的兩個重點
2 PANDAS 基本使用 15

2 Pandas 基本使用

2.1 介紹

不管要抓取什麼樣的資料,把你辛苦的成果儲存下來絕對是最重要的一件事,所以在抓取前,
我們要先教你如何處理表格還有除存成最萬用的 CSV 格式!

2.2 用途

1. 處理各種表格 (csv, excel…等等)


2. 拿來表示機器學習的資料集 (每個列就是一個物品,每個行代表物品的每個特徵
3. 可以快速結合常用的繪圖函式庫,直接畫出漂亮的圖形

2.3 安裝方法

1. 使用 PyCharm: PyCharm -> Settings -> Project -> Project Interpreter -> + -> (搜索)pandas
-> Install Packages
2. 使用命令列: cd 到你安裝 Python 的資料夾 -> 輸入 python -m pip install pandas

2.4 官方文件

http://pandas.pydata.org/pandas-docs/stable/

2.5 可以處理的表格形式

http://pandas.pydata.org/pandas-docs/stable/io.html
2 PANDAS 基本使用 16

圖: 可以處理的表格形式

2.6 目標

我們使用 Kaggle 的 TED 資料集來教你 Pandas 的基本操作

2.7 資料集位置

Kaggle 是我們資料科學家常常使用的一個充滿著資料集和 AI 比賽的一個網站,我們今次先使


用老師以前蠻喜歡的 TED 演講的資料集來教學大家使用 Pandas
https://www.kaggle.com/rounakbanik/ted-talks

1. 需要登入才能下載
2. 只取裡面的 ted_main.csv 來做分析

2.8 CSV 標準表格形式

CSV 是一個標準的表格形式 (而非 Excel),我們一起看看 CSV 的格式規定


❤ 用『逗號』(,) 來分隔每個格子
❤ 用『換行』來分隔沒一筆資料
❤ 如果格子裡的資料有『逗號』,用『雙引號』(”) 括起來
例子:
姓名, 身高, 座右銘
Elwing,175,“程式, 好有趣”
2 PANDAS 基本使用 17

2.9 Pandas 基本資料

Pandas 只有兩種基本資料,一種叫做 DataFrame,一種叫做 Series

1. 多個行 * 多個列 -> DataFrame


2. 一個行 * 多個列或者一個列 * 多個行 -> Series

你一定要記得這兩個稱呼,看官方文件的時候才有辦法看得懂!

[程式]: import pandas as pd


# 為了顯示的漂亮, 我刻意的把印出來的 row 和 column 只顯示六個
# 大家練習的時候可以去掉下面兩行
pd.set_option('display.max_rows', 10)
pd.set_option('display.max_columns', 4)
# 設定每一個格子最大的列印字數
pd.options.display.max_colwidth = 20

2.10 讀取表格操作

使用 read_ 表格形式來讀取,記得最好明確表示用 utf-8 來讀取網站檔案 (網路上的檔案通常


使用 utf-8 來儲存)
注意: 如果是 windows 的一些檔案,內建的儲存編碼是 ANSI,用 utf-8 會失效,我們留待編碼
篇好好說

2.10.1 read_csv 重要參數:

1. 必要參數: 檔案位置
2. encoding 選用參數 (有預設值): 讀取使用編碼

2.10.2 read_csv 回傳值:

一個 DataFrame

[程式]: df = pd.read_csv("ted_main.csv", encoding = "utf-8")


df

[輸出]: comments description … url views


0 4553 Sir Ken Robinson… … https://www.ted… 47227110
1 265 With the same hu… … https://www.ted… 3200520
2 124 New York Times c… … https://www.ted… 1636292
3 200 In an emotionall… … https://www.ted… 1697550
4 593 You've never see… … https://www.ted… 12005869
… … … … … …
2545 17 Between 2008 and… … https://www.ted… 450430
2 PANDAS 基本使用 18

2546 6 How can you stud… … https://www.ted… 417470


2547 10 Science fiction … … https://www.ted… 375647
2548 32 In an unmissable… … https://www.ted… 419309
2549 8 With more than h… … https://www.ted… 391721

[2550 rows x 17 columns]

2.11 DataFrame 大小

1. 由於我們有兩個維度,所以以前習慣的 len 不能使用了,我們要使用.shape 來取得兩個維度


2. .shape 是一個 tuple,所以第一個元素 [0] 就是你的列數,第二個元素 [1] 就是你的行數

[程式]: df.shape

[輸出]: (2550, 17)

2.12 表格行篩選

篩選行的時候,我們就像在操作字典一樣,對你的 DataFrame 加上 [ ]

2.12.1 單行操作

直接在 [ ] 加上你想要的標籤名字,由於一個維度變成 1,所以你會發現從 DataFrame 變成


Series 了 (印出來是不一樣的)

[程式]: df["comments"]

[輸出]: 0 4553
1 265
2 124
3 200
4 593

2545 17
2546 6
2547 10
2548 32
2549 8
Name: comments, Length: 2550, dtype: int64

2.12.2 多行操作

你必須把想要的標籤集合成一個 list 傳入行操作的 [ ],所以這裡兩個 [ ] 代表截然不同的意思


1. 外面的 [ ]: DataFrame 的行操作 2. 裡面的 [ ]: 把標籤集合起來的 list
2 PANDAS 基本使用 19

[程式]: df[ ["comments", "description", "url"] ]

[輸出]: comments description url


0 4553 Sir Ken Robinson… https://www.ted…
1 265 With the same hu… https://www.ted…
2 124 New York Times c… https://www.ted…
3 200 In an emotionall… https://www.ted…
4 593 You've never see… https://www.ted…
… … … …
2545 17 Between 2008 and… https://www.ted…
2546 6 How can you stud… https://www.ted…
2547 10 Science fiction … https://www.ted…
2548 32 In an unmissable… https://www.ted…
2549 8 With more than h… https://www.ted…

[2550 rows x 3 columns]

2.13 表格列篩選

1. 篩選列的時候,我們使用的是.loc(少用, 如果有自己創造列標籤才用得上),iloc(常用, 使用
pandas 幫你創的 0 開始的列標籤)
2. 使用 iloc 的時候會得到一個像是 list 的資料,接著就可以使用類似 list 的操作來操作
3. .iloc -> [“第一筆資料”, “第二筆資料”, “第三筆資料”, …, “最後一筆資料”]

2.13.1 單列篩選

在.iloc 這個列表加上 [ 座號]

[程式]: df.iloc[0]

[輸出]: comments 4553


description Sir Ken Robinson…
duration 1164
event TED2006
film_date 1140825600

speaker_occupation Author/educator
tags ['children', 'cr…
title Do schools kill …
url https://www.ted…
views 47227110
Name: 0, Length: 17, dtype: object
2 PANDAS 基本使用 20

2.13.2 多列篩選

.iloc 後使用 [頭部座號 (包括): 尾部座號 (不包括)]

[程式]: # 取 10, 11, 12, 13, 14 共五筆資料


df.iloc[10:15]

[輸出]: comments description … url views


10 79 Accepting his 20… … https://www.ted… 1211416
11 55 Jehane Noujaim u… … https://www.ted… 387877
12 71 Accepting the 20… … https://www.ted… 693341
13 242 Jeff Han shows o… … https://www.ted… 4531020
14 99 Nicholas Negropo… … https://www.ted… 358304

[5 rows x 17 columns]

2.13.3 頭幾列篩選

Pandas 也提供一些讓你偷懶的函式,如果是要篩選頭幾列的話用 head 函式來篩選

[程式]: # 頭五列
df.head(5)

[輸出]: comments description … url views


0 4553 Sir Ken Robinson… … https://www.ted… 47227110
1 265 With the same hu… … https://www.ted… 3200520
2 124 New York Times c… … https://www.ted… 1636292
3 200 In an emotionall… … https://www.ted… 1697550
4 593 You've never see… … https://www.ted… 12005869

[5 rows x 17 columns]

2.13.4 尾幾列篩選

使用 tail 函式來做尾部的篩選

[程式]: # 尾五列
df.tail(5)

[輸出]: comments description … url views


2545 17 Between 2008 and… … https://www.ted… 450430
2546 6 How can you stud… … https://www.ted… 417470
2547 10 Science fiction … … https://www.ted… 375647
2548 32 In an unmissable… … https://www.ted… 419309
2549 8 With more than h… … https://www.ted… 391721

[5 rows x 17 columns]
2 PANDAS 基本使用 21

2.14 表格行+列篩選

1. 只要是 DataFrame 就可以使用上面的行或者列篩選


2. 所以你可以任意組合行列篩選,先 [] 再.iloc[],或者先.iloc[] 再 []

[程式]: # 如果: 後面不寫就是到最底, : 前面不寫就是從最頭開始


df[ ["comments", "description", "duration"] ].iloc[ :5 ]

[輸出]: comments description duration


0 4553 Sir Ken Robinson… 1164
1 265 With the same hu… 977
2 124 New York Times c… 1286
3 200 In an emotionall… 1116
4 593 You've never see… 1190

2.15 列過濾

1. 過濾操作是把符合我們期待的列留下來,不符合期待的列丟掉的一個操作
2. 核心概念是做一個跟我們的資料筆數一樣大的布林 list,對到 True 的資料留下,對到 False
的資料丟掉
3. (特別) 這時候一樣是對你的 DataFrame 加上 [ ] , 把布林 list 丟進你的 [ ] 裡

[程式]: # 先做個實驗給你看, 只取三列資料


test = df.iloc[:3]
test

[輸出]: comments description … url views


0 4553 Sir Ken Robinson… … https://www.ted… 47227110
1 265 With the same hu… … https://www.ted… 3200520
2 124 New York Times c… … https://www.ted… 1636292

[3 rows x 17 columns]

[程式]: # 過濾, 創一個三個大小的 True, False list


# 對到 True(第一三筆) 留下, 對到 False(第二筆) 丟掉
test[ [True, False, True] ]

[輸出]: comments description … url views


0 4553 Sir Ken Robinson… … https://www.ted… 47227110
2 124 New York Times c… … https://www.ted… 1636292

[2 rows x 17 columns]

[程式]: # 但我們不可能自己用手創一個 2000 多個元素的布林 list


# 所以我們藉由 pandas 的函式幫我們
# 先取出一個 Series 取 str 屬性得到字串 list
2 PANDAS 基本使用 22

# 藉由 pandas 定義的 contains 對裡面每個元素做出布林判斷


bool_filter = df["description"].str.contains("Sir")
bool_filter

[輸出]: 0 True
1 False
2 False
3 False
4 False

2545 False
2546 False
2547 False
2548 False
2549 False
Name: description, Length: 2550, dtype: bool

[程式]: # 帶入 DataFrame
df[bool_filter]

[輸出]: comments description … url views


0 4553 Sir Ken Robinson… … https://www.ted… 47227110
15 325 Violinist Sirena… … https://www.ted… 2702470
54 203 Speaking as both… … https://www.ted… 2121177
692 1234 In this poignant… … https://www.ted… 7266316
833 473 In this talk fro… … https://www.ted… 1854997
… … … … … …
1502 634 Sir Ken Robinson… … https://www.ted… 6657858
1802 59 Sir Tim Berners-… … https://www.ted… 1054600
1978 64 The founder of S… … https://www.ted… 1304737
2192 61 Trust: How do yo… … https://www.ted… 1437353
2503 20 How smart can ou… … https://www.ted… 1139827

[11 rows x 17 columns]

[程式]: # 你仔細對照, 你會發現 contains 是只要有 contains 那個字串就可以


# 並不一定是完整的一個字 (Sirena 也算有 contains Sir)
# 但我們可以使用格式 (正規表示式) 來結合 contains
# 記得在你格式字串前加上 r(不轉換任何東西, 原始字串)
# 不然\b 會被當成 backspace, 而不是兩個字
df[ df["description"].str.contains(r"\bSir\b") ]

[輸出]: comments description … url views


0 4553 Sir Ken Robinson… … https://www.ted… 47227110
54 203 Speaking as both… … https://www.ted… 2121177
692 1234 In this poignant… … https://www.ted… 7266316
833 473 In this talk fro… … https://www.ted… 1854997
2 PANDAS 基本使用 23

1502 634 Sir Ken Robinson… … https://www.ted… 6657858


1802 59 Sir Tim Berners-… … https://www.ted… 1054600

[6 rows x 17 columns]

2.16 儲存表格

1. 非常簡單!!!就跟 read 一樣,你想儲存什麼就讓你的 DataFrame 使用 to_ 儲存格式


2. 一樣建議指定使用 utf-8 做儲存

2.16.1 to_csv 重要參數

1. 必要參數: 檔案位置
2. 選用參數 encoding(有預設值): 讀取使用編碼
3. 選用參數 index(有預設值 True): 要不要把 pandas 幫你產生的列編號寫進檔案, True: 寫,
False: 不寫, 通常我會選 False

[程式]: # 用剛剛的 filter 過後的東西做個例子


filter_df = df[ df["description"].str.contains(r"\bSir\b") ]
# 儲存成 csv
filter_df.to_csv("filter.csv", encoding = "utf-8", index = False)

[程式]: # 把剛剛儲存的東西讀出來給你看看
pd.read_csv("filter.csv", encoding = "utf-8")

[輸出]: comments description … url views


0 4553 Sir Ken Robinson… … https://www.ted… 47227110
1 203 Speaking as both… … https://www.ted… 2121177
2 1234 In this poignant… … https://www.ted… 7266316
3 473 In this talk fro… … https://www.ted… 1854997
4 634 Sir Ken Robinson… … https://www.ted… 6657858
5 59 Sir Tim Berners-… … https://www.ted… 1054600

[6 rows x 17 columns]

2.17 刪除行

你可以使用 drop 來刪除多行,不過記得如果你想要讓 df 變成刪除過後的樣子要記得設定回去

[程式]: # 刪除多行, axis = 1 指的是刪除行的意思, axis = 0 是刪除列的意思


df.drop(["url", "views"], axis = 1)

[輸出]: comments description … tags \


0 4553 Sir Ken Robinson… … ['children', 'cr…
2 PANDAS 基本使用 24

1 265 With the same hu… … ['alternative en…


2 124 New York Times c… … ['computers', 'e…
3 200 In an emotionall… … ['MacArthur gran…
4 593 You've never see… … ['Africa', 'Asia…
… … … … …
2545 17 Between 2008 and… … ['TED Residency'…
2546 6 How can you stud… … ['Mars', 'South …
2547 10 Science fiction … … ['AI', 'ants', '…
2548 32 In an unmissable… … ['Internet', 'TE…
2549 8 With more than h… … ['cities', 'desi…

title
0 Do schools kill …
1 Averting the cli…
2 Simplicity sells
3 Greening the ghetto
4 The best stats y…
… …
2545 What we're missi…
2546 The most Martian…
2547 What intelligent…
2548 A black man goes…
2549 How a video game…

[2550 rows x 15 columns]

2.18 轉換類別

apply 是一個最重要的可以幫你轉換一行裡面所有格子的方便函式,大家在學習 Pandas 的時


候一定要好好學會 apply 的使用方式,我們發現我們在 TED 資料集裡看到的時間都是一些奇怪形
式,譬如這樣,這是什麼呢?
[程式]: df["film_date"]

[輸出]: 0 1140825600
1 1140825600
2 1140739200
3 1140912000
4 1140566400

2545 1496707200
2546 1492992000
2547 1492992000
2548 1499472000
2549 1492992000
Name: film_date, Length: 2550, dtype: int64
2 PANDAS 基本使用 25

這叫做 UNIX 時間或者 POSIX 時間,是從 1970 年 1 月 1 日 0 時 0 分 0 秒起至現在的總秒數,


也是電腦常常使用的時間,我們可以輕易的把它轉換成我們熟悉的西元時間,這裡我們使用內建的
datetime 模組來轉換

[程式]: # 我們先對一個格子做一次看看
from datetime import datetime
import pytz
print("原始:", df["film_date"][0])
print("轉換 (當地時間):", datetime.fromtimestamp(df["film_date"][0]))
print("轉換 (標準時間):", datetime.utcfromtimestamp(df["film_date"][0]))

原始: 1140825600
轉換 (當地時間): 2006-02-25 08:00:00
轉換 (標準時間): 2006-02-25 00:00:00

apply 只要把你定義的流程名字丟進去,就會自動對每一個格子做一次

[程式]: # 在使用 apply 前, 我們要定義一個流程


# 接著就可以把這流程對每一個格子做一次
def timeflow(data):
# 這裡要記得, python 在 print 的時候會先做一次 str 的轉換
# 所以我們要做一次 str 轉換
return str(datetime.utcfromtimestamp(data))
# 不用帶入 data, apply 會自動幫你把每個格子裡的資料帶入
df["film_date"].apply(timeflow)

[輸出]: 0 2006-02-25 00:00:00


1 2006-02-25 00:00:00
2 2006-02-24 00:00:00
3 2006-02-26 00:00:00
4 2006-02-22 00:00:00

2545 2017-06-06 00:00:00
2546 2017-04-24 00:00:00
2547 2017-04-24 00:00:00
2548 2017-07-08 00:00:00
2549 2017-04-24 00:00:00
Name: film_date, Length: 2550, dtype: object

[程式]: # 設定回去原表格
df["film_date(datetime)"] = df["film_date"].apply(timeflow)
df[ ["film_date", "film_date(datetime)"] ]

[輸出]: film_date film_date(datetime)


0 1140825600 2006-02-25 00:00:00
2 PANDAS 基本使用 26

1 1140825600 2006-02-25 00:00:00


2 1140739200 2006-02-24 00:00:00
3 1140912000 2006-02-26 00:00:00
4 1140566400 2006-02-22 00:00:00
… … …
2545 1496707200 2017-06-06 00:00:00
2546 1492992000 2017-04-24 00:00:00
2547 1492992000 2017-04-24 00:00:00
2548 1499472000 2017-07-08 00:00:00
2549 1492992000 2017-04-24 00:00:00

[2550 rows x 2 columns]

2.19 進階列過濾

1. 有時候利用 pandas 有的函式難以完成我想要的過濾, 這時候我可以自定義我的過濾流程


2. 對你的 Series 使用 apply 來過濾
3. (重要) 你的過濾流程最後一定要回傳 True or False
[程式]: # 我想把 tag 欄位裡的字串拿出來並且轉換成一個 list
# 再檢查某個字串也沒有在 list 裡
# 你的過濾流程的第一個參數, pandas 會幫你傳入
# 就是你每一格的資料, 也就是 element = 每一格的資料
def tag_filter(element):
# 利用 eval 把字串當成 python 程式執行, 變成一個 list
tag_list = eval(element)
if 'children' in tag_list:
return True
else:
return False
# 讓你看看做出來的 bool list
bool_filter = df["tags"].apply(tag_filter)
bool_filter

[輸出]: 0 True
1 False
2 False
3 False
4 False

2545 False
2546 False
2547 False
2548 False
2549 False
Name: tags, Length: 2550, dtype: bool
2 PANDAS 基本使用 27

[程式]: # 來過濾你的 DataFrame, 並且我們只看 tags, description 兩列


df[bool_filter][ ['description', 'tags'] ]

[輸出]: description tags


0 Sir Ken Robinson… ['children', 'cr…
14 Nicholas Negropo… ['children', 'de…
152 Author and illus… ['art', 'childre…
171 At TED U, Gever … ['children', 'de…
180 Bill Strickland … ['MacArthur gran…
… … …
2475 Meet Sharon Terr… ['Bioethics', 'D…
2477 Aspirations are … ['Africa', 'Inte…
2479 Sixty-five milli… ['TED Books', 'a…
2482 "We have seen ad… ['children', 'gl…
2525 Could it be wron… ['TEDx', 'activi…

[143 rows x 2 columns]

[程式]: # 更進階定義, 讓你在使用的時候可以再多帶入參數


def tag_filter(element, filter_tag):
tag_list = eval(element)
if filter_tag in tag_list:
return True
else:
return False
# 你放在 apply 後面的參數, pandas 會幫你丟進你的過濾過程
# 但是參數名字就要對到過濾流程的參數
bool_filter = df["tags"].apply(tag_filter, filter_tag = 'Asia')
bool_filter

[輸出]: 0 False
1 False
2 False
3 False
4 True

2545 False
2546 False
2547 False
2548 False
2549 False
Name: tags, Length: 2550, dtype: bool

[程式]: df[bool_filter][ ["description", "tags"] ]

[輸出]: description tags


4 You've never see… ['Africa', 'Asia…
2 PANDAS 基本使用 28

117 Researcher Hans … ['Africa', 'Asia…


359 Reporter Jennife… ['Asia', 'busine…
448 Nandan Nilekani,… ['Asia', 'busine…
484 TED Fellow Sopha… ['Asia', 'advent…
… … …
1537 It's a standard … ['Asia', 'busine…
1615 Dong Woo Jang ha… ['Asia', 'cultur…
1621 The developed wo… ['Africa', 'Asia…
1948 The former prime… ['Asia', 'United…
2310 Americanization … ['Africa', 'Asia…

[26 rows x 2 columns]


3 GOOGLE 動圖下載 29

3 Google 動圖下載

我們馬上進入正題,來我們的第一個練習吧!我們這個練習最重要的目的是先讓你理解一下網
路的概念,接下來的爬蟲就會對你非常簡單,另外也會教你如何透過網路爬蟲大量的抓取圖片!

3.1 什麼是網路

我心中的網路就如同下圖的樣子,你的電腦就彷彿是一個人,伺服器就像是一個服務台,人會
不斷地向服務台詢問問題,也會得到對應的答案

圖: 網路的樣子

那只剩最後一個要釐清的東西了!什麼是『問題』呢?
『問題』就是所謂的一個『網址』
!也就
是說,我們只要懂得我們平常使用的網址,我們就可以懂大半的網路世界了!
3 GOOGLE 動圖下載 30

3.2 網址解析

我們先用最簡單的網址來做個解析
https://www.google.com.tw/search?q=apple&oq=apple&aqs=chrome.
.69i57j69i61j69i65l3j69i60.1283j0j4&sourceid=chrome&ie=UTF-8
上面這個網址是我在 Google 搜尋 Apple 這字眼得到的網址
如果你仔細看,這網址其實可以被分成四個部分
❤ https: 這部分是我們在跟服務台溝通使用的語言,順帶一提,https 的 s 代表 secure,代表
傳輸過程會經過『加密成密碼』的動作,才可以預防別人在傳輸線中進行偷聽的動作
❤ www.google.com.tw: 到第一個/前,你可能看到兩種形式,一種就是我們現在看到的,我們
叫做 Domain Name(域名),另外一種是由四個數字構成的 IP,先說 IP,IP 就是你的電腦在網路世
界的位址,而 Domain Name 背後一定會對到一個 IP,我喜歡稱 Domain Name 為地標,以例子來
說就是: Domain Name(小巨蛋) = IP(臺北市松山區南京東路四段 2 號) 的關係
❤ search: 到? 前則是我們的第三個部分,我喜歡稱他為向『服務台』遞出的『問題本身』
,我
們這裡遞出的問題就是一個叫做『search』的問題
❤ q=apple&op=apple: 第四個部分則是在? 後的一連串東西,事實上這是由許多組組成的,你
可以用 & 符號把他們分開,我們仔細看一組 q=apple,你應該也發現了,這其實就是 search 這個
問題所需要的一些必要資訊,每組必要資訊都以附加資訊名 = 附加資訊值的方式存在

3.3 網路抓取兩步驟

3.3.1 步驟 1: 查看原始碼

首先,請你先使用 Chrome 瀏覽器跟著我一起操作


先來到首頁,並點選『好手氣』的按鈕

圖: 點選好手氣
3 GOOGLE 動圖下載 31

我們的目標是抓取https://www.google.com/doodles的所有動圖
請先幫我在空白的地方點選『查看原始碼』

圖: 點選查看原始碼

你應該會看到這樣的圖

圖: 跳出的頁面

那為什麼我們要查看『原始碼』呢?
其實這才是最重要的問題,這裡我告訴你答案!!
!一定要好好記得唷
『原始碼是你跟服務台搭訕的第一個問題 (網址列) 的答案』
『第一個問題的答案』『第一個問題的答案』『第一個問題的答案』很重要所以說三次!
接下來我們要做什麼呢?
查看我們想要的答案有沒有在『第一個問題的答案』裡,如果有的話,我們等等就讓程式也詢
問你『網址列上的問題』
3 GOOGLE 動圖下載 32

圖: 只有六張動圖在上面

你可以把圖片上面的文字當作搜尋的關鍵字,在原始碼那頁按 CTRL + F 搜尋,你會驚訝的發


現,雖然原始碼裡有,但只有六張我們想要的圖片,很顯然,如果我們的目標是所有的圖片,網址
列這個『問題』是不能滿足我的!

3.3.2 步驟 2: 找出真正問題

在步驟 1 的時候,我們已經知道藉由『第一個問題』,也就是網址列,是沒有我們想要的答案
的,事實上,這在現代的網頁挺常發生的,第一個問題只拿得到一點點東西,必須補上後續的問題
才會拿到更多東西!那問題怎麼觀察後續的問題呢?請跟著下圖打開我們 Chrome 的 Network 工具
3 GOOGLE 動圖下載 33

圖: 更多工具-開發人員工具

接著選擇上方的 Network 分類,Network 這個工具可以幫助你監聽所有跟服務器問出的問題,


不過要記得,他是開啟後才監聽,所以要記得重整一下你的網頁
3 GOOGLE 動圖下載 34

圖: Network

接著由於我們初學乍到,我們稍微做點弊,上方的分類是根據問題以及答案的不同分成不同類,
我們先將它固定在 XHR

圖: 先固定成 XHR

接著點擊一則來看看,並且觀察右邊的 Header 部分
3 GOOGLE 動圖下載 35

圖: 觀察右邊的 Header

我想聰慧的你應該已經發現些什麼了吧?這網址大有蹊蹺,因為『年份』和『月份』歷歷在目,
那接下來該如何做呢?旁邊還有兩個可以按的,一個叫做『response』(回應),一個叫做『preview』
(漂亮化的回應),我們按按看 preview

圖: Network Preview

應該不用多說了!你試試看 url 那個網址,就是對應的圖片位址


3 GOOGLE 動圖下載 36

3.3.3 此網站的結論

所以我們得到什麼結論呢?就是!我要拿取的網址應該是後面我在 Network 觀察到的真正問題


https://www.google.com/doodles/json/2018/11?hl=zh_TW
再把裡面的一個一個網址萃取出來就好!

3.4 開始寫程式

那首先我們就把我們觀察到的網址送出,這裡我們先使用最基本的內建函式庫 urllib 完成這個


操作

[程式]: # 我們使用內建的 urllib.request 裡的 urlopen 這個功能來送出網址


from urllib.request import urlopen
# 如果是 MAC 電腦, 請務必加入下面兩行, 因為 MAC 有視 htpps 的 ssl 證書無效的 bug
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
# 你可以看到我們打開的網址是步驟 2 的網址, 我們完全沒有使用網址列的網址
response = urlopen("https://www.google.com/doodles/json/2018/11?hl=zh_TW")
print(response)

<http.client.HTTPResponse object at 0x104b69208>

3.4.1 JSON 回應

我們看到已經得到回應了!但是我們要看回應是哪種形態來用對應的函式庫處理,我們看看剛
剛 response 的部分

圖: 回應的樣式

如果你仔細的觀察,會有兩個我們在基礎語法挺熟悉的兩個符號,中括號 (List) 和大括號 (Dic-


tionary),這邊幫大家複習一下這兩個資料型態!
❤ List: List 裡的每一個資料是『同樣類型』的資料,簡單來說,就是一種『排隊』,例如: [1,
2, 3] (一個數字的排隊)
❤ Dictionary: Dictionary 是一個用『不同資料』組合成一個『複雜資料』的方式!例如:
{“name”:“Elwing”, “height”:175, “weight”:75}(三個資料組合成一個人的複雜資料),要記得每個小
配件都由『名字: 資料』組合
為何要使用這兩種資料呢?你仔細看剛剛上面的回應
3 GOOGLE 動圖下載 37

Doodles 清單 = [動圖 1, 動圖 2, ...]


每一個動圖 = {"title":"xxx", "url":"xxxx"}

你可以看到用 List 和 Dictionary 就可以組合成非常複雜的資料形式來表示任何東西,當然每一


個動圖你可以看 preview 的地方更清楚

圖: 再看一次 preview

這種用 List 和 Dictionary 組合的形式,我們叫做『JSON 格式』。我們對於 JSON 的處理方式


是把它轉換成 Python 的 List 和 Dictionary 來處理

# 使用內建的 json 函式庫


import json
# 內建還有一個 loads
# load 是針對還未閱讀的東西 (資料要實作 read 功能)
# loads 是針對已經讀出來的東西 (例子: 字串)
# 如果是 urlopen 的回應, 或者 file 的 open 我們都用 load
json.load(response)

[程式]: from urllib.request import urlopen


import ssl
ssl._create_default_https_context = ssl._create_unverified_context
# 使用內建的 json 函式庫
import json
response = urlopen("https://www.google.com/doodles/json/2018/11?hl=zh_TW")
doodles = json.load(response)
print(doodles)

[{'alternate_url': 'https://lh3…wl9KAnHh-2msu', 'hires_height': 427, 'hires_url':


'//www.google…928.3-2xa.gif', 'hires_width': 863, …}, {'alternate_url':
3 GOOGLE 動圖下載 38

'https://lh3…310-J38RrKdIJ', 'hires_height': 400, 'hires_url':


'//www.google…944512-2x.png', 'hires_width': 816, …}, {'alternate_url':
'https://lh3…OEvjRgkXvp04A', 'hires_height': 400, 'hires_url':
'//www.google…007104-2x.png', 'hires_width': 1000, …}, {'alternate_url':
'https://lh3…ls7TQ1dc9DTJA', 'hires_height': 400, 'hires_url':
'//www.google…63744-2xa.gif', 'hires_width': 1000, …}, {'alternate_url':
'https://lh3…HBJKbPaLoxhf3', 'hires_height': 460, 'hires_url':
'//www.google…897280-2x.jpg', 'hires_width': 1007, …}, {'alternate_url':
'https://lh3…UhqyWXLz5sPA8', 'hires_height': 400, 'hires_url':
'//www.google…968128-2x.jpg', 'hires_width': 1000, …}, …]

複習一下,List 我們常使用的操作是 for…in 走過操作,Dictionary 我們使用 [名字] 來取得對應


資料

[程式]: from urllib.request import urlopen


import ssl
ssl._create_default_https_context = ssl._create_unverified_context
import json
response = urlopen("https://www.google.com/doodles/json/2018/11?hl=zh_TW")
doodles = json.load(response)
# for...in 操作把一個一個動圖拿出來, d 是我們給動圖一個一個暫時的名字
for d in doodles:
# 補上 https, 使用 [名字] 來取得對應的資料
url = "https:" + d["url"]
title = d["title"]
print("圖片標題:", title)
print("圖片網址:", url)

圖片標題: 亞歷山大鮑羅定 185 歲冥誕


圖片網址: https://www.google.com/logos/doodles/2018/alexander-borodins-185th-
birthday-5489090051964928-law.gif
圖片標題: 欣德羅斯特姆 87 歲冥誕
圖片網址: https://www.google.com/logos/doodles/2018/hind-rostoms-87th-
birthday-6635652106944512-l.png
圖片標題: 2018 年父親節 (印尼)
圖片網址: https://www.google.com/logos/doodles/2018/fathers-
day-2018-indonesia-5013018697007104-l.png
圖片標題: 2018 年退伍軍人節
圖片網址: https://www.google.com/logos/doodles/2018/veterans-
day-2018-5536434952863744.2-l.png
圖片標題: 2018 年波蘭獨立紀念日
圖片網址: https://www.google.com/logos/doodles/2018/poland-independence-
day-2018-4924131932897280-l.png
圖片標題: 克里斯緹埃辛伊格博克威 58 歲冥誕
圖片網址: https://www.google.com/logos/doodles/2018/christy-essien-igbokwes-58th-
3 GOOGLE 動圖下載 39

birthday-4646625505968128-l.png
圖片標題: 米莉安特拉利 85 歲冥誕
圖片網址: https://www.google.com/logos/doodles/2018/miriam-tlalis-85th-
birthday-5099732341882880-l.png
圖片標題: 2018 年父親節 (愛沙尼亞、挪威)
圖片網址: https://www.google.com/logos/doodles/2018/fathers-day-2018-estonia-
norway-5766574735622144-l.png
圖片標題: 伊莉莎李昂那多扎馬菲拉斯 131 歲冥誕
圖片網址: https://www.google.com/logos/doodles/2018/elisa-leonida-zamfirescus-131st-
birthday-6181104577937408-l.png
圖片標題: 紀念阿曼達克羅維
圖片網址: https://www.google.com/logos/doodles/2018/celebrating-amanda-
crowe-5837715529531392-l.png
圖片標題: 2018 年柬埔寨獨立紀念日
圖片網址: https://www.google.com/logos/doodles/2018/cambodia-independence-
day-2018-5069486645313536-l.png
圖片標題: 2018 年美國大選
圖片網址: https://www.google.com/logos/doodles/2018/united-states-
elections-2018-5184312390451200.6-l.png
圖片標題: 麥可德托羅斯 82 歲冥誕
圖片網址: https://www.google.com/logos/doodles/2018/michael-dertouzos-82nd-
birthday-5050461148151808-l.png
圖片標題: 2018 年巴拿馬獨立紀念日
圖片網址: https://www.google.com/logos/doodles/2018/panama-independence-
day-2018-5325969073111040-law.gif
圖片標題: 2018 年亡靈節
圖片網址: https://www.google.com/logos/doodles/2018/day-of-the-
dead-2018-5705827255058432-law.gif
圖片標題: 約瑟夫蒂勒爾 160 歲冥誕
圖片網址: https://www.google.com/logos/doodles/2018/joseph-burr-tyrrells-160th-
birthday-6697443532996608-l.png

3.4.2 下載圖片

下載圖片的時候,你可以使用 urlretrieve 來下載

urlretrieve(" 圖片網址", " 檔案名")

[程式]: # 多 import 一個 urlretrieve 的功能


from urllib.request import urlopen, urlretrieve
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
import json
response = urlopen("https://www.google.com/doodles/json/2018/11?hl=zh_TW")
3 GOOGLE 動圖下載 40

doodles = json.load(response)
for d in doodles:
url = "https:" + d["url"]
title = d["title"]
# 你可以把網址用/切出, 會發現我們可以得到一個 list, 最後一個東西就是我們要的檔案名字
# print(url.split("/"))
# 這裡必須自行建立你的資料夾在你的 Project 底下
fpath = "doodles/" + url.split("/")[-1]
urlretrieve(url, fpath)

我們看看儲存的結果

圖: 剛剛我建立的 doodles 裡面的資料夾裡

3.4.3 結論

我們已經完成了一個月的 Doodles 下載,你會發現,其實教電腦下載東西並沒有這麼困難,我


們使用大概 20 行程式碼即可完成,重要的是你一定要理解什麼是『向服務台問問題』這個比喻!你
才能確實理解網路
3 GOOGLE 動圖下載 41

3.5 整年份下載

更進階的,我們除了搞定整年份以外,順便把創造資料夾的工作讓程式來處理

# 創造資料夾
import os
# 在建立前必須先檢查資料夾有沒有存在
# 不存在才可以創建
if not os.path.exists(" 資料夾名"):
os.mkdir(" 資料夾名")

[程式]: from urllib.request import urlopen, urlretrieve


import ssl
ssl._create_default_https_context = ssl._create_unverified_context
import json
# os 函式庫可以幫你處理檔案的各種管理
import os
# 讓你的網址可以動態, 使用 range(1, 13) 建立 [1, 2, ..., 12] 的 list
for m in range(1, 13):
# 把 m 丟進去製造月份網址, 要記得 m 是數字, 必須轉換成字串
url = "https://www.google.com/doodles/json/2018/" + str(m) + "?hl=zh_TW"
print("處理月份:", url)
response = urlopen(url)
doodles = json.load(response)
for d in doodles:
url = "https:" + d["url"]
title = d["title"]
# 改進一下, 我們使用 m 來建立資料夾
dirname = "doodles/" + str(m)
if not os.path.exists(dirname):
os.mkdir(dirname)
fpath = dirname + "/" + url.split("/")[-1]
urlretrieve(url, fpath)

處理月份: https://www.google.com/doodles/json/2018/1?hl=zh_TW
處理月份: https://www.google.com/doodles/json/2018/2?hl=zh_TW
處理月份: https://www.google.com/doodles/json/2018/3?hl=zh_TW
處理月份: https://www.google.com/doodles/json/2018/4?hl=zh_TW
處理月份: https://www.google.com/doodles/json/2018/5?hl=zh_TW
處理月份: https://www.google.com/doodles/json/2018/6?hl=zh_TW
處理月份: https://www.google.com/doodles/json/2018/7?hl=zh_TW
處理月份: https://www.google.com/doodles/json/2018/8?hl=zh_TW
處理月份: https://www.google.com/doodles/json/2018/9?hl=zh_TW
處理月份: https://www.google.com/doodles/json/2018/10?hl=zh_TW
處理月份: https://www.google.com/doodles/json/2018/11?hl=zh_TW
3 GOOGLE 動圖下載 42

處理月份: https://www.google.com/doodles/json/2018/12?hl=zh_TW

你可以看到所有的圖片依照月份好好的儲存了,而且只是簡單的加了幾行程式碼而已!試試看
吧!

圖: 完整的圖片儲存

3.6 進階 - Jupyter Notebook

這裡比較進階一點,如果你不想儲存的話,也可以單純印製在 Jupyter Notebook 上,建議讀者


先不要閱讀此章節,等待對於 Python 函式庫以及語法更為熟悉再來試試看,你可以把整個圖片下
載,是一個非常震撼的效果

3.6.1 安裝函式庫

請安裝

1. matplotlib 函式庫: 畫圖函式庫


2. Pillow 函式庫: 圖片處理函式庫

[程式]: from urllib.request import urlopen, urlretrieve


import ssl
ssl._create_default_https_context = ssl._create_unverified_context
import json
import os
# import pillow 和 matplotlib
from PIL import Image
import matplotlib.pyplot as plt
# 使用這行可以不用.show 來最後 show 出來
%matplotlib inline
# 準備一個空的 list 來儲存
urllist = []
3 GOOGLE 動圖下載 43

# 3 個月就好!! 不然圖太大了
for m in range(1, 3):
url = "https://www.google.com/doodles/json/2018/" + str(m) + "?hl=zh_TW"
response = urlopen(url)
doodles = json.load(response)
for d in doodles:
url = "https:" + d["url"]
# 把所有要儲存的 url 儲存起來
urllist.append(url)

print("總共圖片數:", len(urllist))
# 把大圖準備成 寬 * 高 個小圖
# 我想要每列五個圖
width = 5
# 算出來應該會是多少個列
height = int(len(urllist) / width) + 1
# 順便調整整個大圖大小, 我用 20 英吋 * 30 英吋
plt.figure(figsize=(20, 30))
# enumerate 可以回傳 (index, 資料) 這樣的 tuple
for (index, url) in enumerate(urllist):
plt.subplot(height, width, index + 1)
response = urlopen(url)
# 利用 PIL 讀取圖片
img = Image.open(response)
# 不需要座標軸
plt.axis("off")
# 把小圖畫出來
plt.imshow(img)

總共圖片數: 69
3 GOOGLE 動圖下載 44
4 TABELOG 餐廳收集 45

4 Tabelog 餐廳收集

在上個章節,你學會了如何什麼叫做『向服務台送出的問題』,並且學會『解析 JSON 格式』,


在這章節,我們要教會你另外一種回應的解析方式,就是『網頁 (HTML)』的解析方式

4.1 HTML 解析

在開始前,我們要先教你什麼是網頁的表示形式,請你記住下面的這個比喻,首先我們先講個
故事:
『有一座城堡,城堡裡有公主和王子,公主養了一隻狗!』
要怎麼讓這故事有一個固定的表示形式呢?我們想到一個聰明的方法,把這故事表示的有『結
構性』,也就是把『擁有』的概念表示出來

4.1.1 元素

結構性的第一個概念,把一個一個物體用包含的形式表示出來,我喜歡叫他是一個一個『盒
子』,就像惡作劇禮物一樣,打開一個盒子還有另外一個盒子,我們用來表示一個盒子的開始,代
表盒子的結束,順帶一提,比較文言的說法是『元素』

<城 堡>
<公 主>
<狗>
</ 狗 >
</ 公 主 >
<王 子>
</ 王 子 >
</ 城 堡 >

特殊元素 請你一定要記住幾個特殊的盒子品牌,因為我們在抓取的時候特別常用,瀏覽器看到這
些品牌的時候會特別處理
❤ img: 圖片
4 TABELOG 餐廳收集 46

❤ video: 影片
❤ a: 超連結

4.1.2 屬性

如果你今天有兩個公主,我們的一大問題就是我們分不出哪個公主是哪個!譬如說今天有兩個
超連結,你並沒有不一樣的地方來讓他們可以把他們不同的連結顯示出來,也是一個大問題,所以
我們會在盒子的一開始的地方把這不一樣的部分加進去,這個加進去不一樣的地方,我喜歡叫他做
『盒子的特徵』,比較文言的說法是『屬性』!

< 元 素 特 徵 名 1=" 特 徵 值 1" 特 徵 名 2=" 特 徵 值 2">

</ 元 素 >

特殊屬性 這裡有些『特殊特徵』
,這些『特殊特徵』一定要配上『特殊盒子』才會起作用,我們一
起來看看有哪些
❤ img 和 video 配上 src 屬性:

<img src=" 圖 片 讀 取 網 址 ">

❤ a 配上 href 屬性:

<a href=" 連 結 到 的 網 址 ">

通用屬性 除了一定要跟特定元素配合的特殊特徵以外,還有兩個所有人都可以使用的『通用特
徵』,這兩個特徵一開始存在的目的其實是為了在網頁開發的時候可以透過這些不同點選到我要的
元素,而我們在教電腦爬過網頁的時候也可以利用這兩個特徵來定位我想要的盒子!這兩個特徵分
別為 class(職業) 和 id(身分證),要注意一點,兩個職業以上的時候我們會用『空白鍵』分開職業!

< 任 意 元 素 class=" 職 業 1 職 業 2 職 業 3">

< 任 意 元 素 id=" 身 分 證 ">

4.1.3 內容

我們有了『盒子』
,有了『特徵』
,但是我們始終沒有把東西秀給使用者看,所以我們還需要最
後一個東西,我喜歡叫他做『紙條』,因為就好像是你打開這個惡作劇禮物的時候,在某些盒子裡
面看到了紙條提示,『紙條』就會直接秀在頁面上讓使用者看到,比較文言的稱呼叫做『內容』

< 元 素 特 徵 名 1=" 特 徵 值 1" 特 徵 名 2=" 特 徵 值 2">


紙條內容
</ 元 素 >
4 TABELOG 餐廳收集 47

4.1.4 HTML 語法總結

請記得這三個比喻!我們的完整的 HTML 語法就由『盒子』(元素),『特徵』(屬性) 和『紙條』


(內容構成)

4.2 網站分析

4.2.1 目標網頁

我們今天的目標 https://tabelog.com/tw/tokyo/rstLst/?SrtT=rt 是東京的餐廳排


行,我想要把排行榜的所需東西整理成一個表格,跟著帶去旅遊!

圖: 目標網頁
4 TABELOG 餐廳收集 48

圖: 來到目標網址的流程

4.2.2 步驟 1: 打開原始碼

別忘了我們之前說的唷!網路就是一個『遊客中心』
,你應該跟『服務台』(server) 遞出你想問
的問題,拿到答案!而『原始碼』就是第一個問題,也就是『網址列』的答案

圖: 右鍵點選空白的地方查看原始碼

你會發現跟上一個練習不一樣的是,我們的原始碼裡面就包含了我們想要的答案,而且從第一
則到最後一則全部都在上面
4 TABELOG 餐廳收集 49

圖: 發現想要的答案

4.2.3 此網站的結論

於是,我們應該可以跳過第二個步驟了!因為我們發現『原始碼』就包含著我們需要的東西,
而想要得到『原始碼』,只要用『網址列』拿到答案即可!

4.3 開始寫程式

這個程式我們打算教會你如何處理網頁形式的回應,請記得先安裝對應函式庫唷!

4.3.1 必要函式庫

❤ beautifulsoup4 : 請使用 PyCharm 或者 Pip 安裝 beautifulsoup4 函式庫 (請不要忽略 4,如


果你沒有選 4 版的話,會無法順利安裝),這函式庫的功用是一個解析器,他可以把網頁形式的文
字轉換成『盒子』的概念,我們就可以利用『找盒子』的方式把我們想要的內容萃取出來!

4.3.2 送出問題

請把我們上們分析完的結論,『問題就是網址列』真正的送出去,並且得到答案以後,轉換成
我們可以處理的『盒子』形式

[程式]: # 一樣使用內建的 urllib.request 裡的 urlopen 這個功能來送出網址


from urllib.request import urlopen
# 在你剛剛安裝的 beautifulsoup4 函式庫裡使用 BeautifulSoup 這個解析器
from bs4 import BeautifulSoup
# 如果是 MAC 電腦, 請務必加入下面兩行, 因為 MAC 有視 htpps 的 ssl 證書無效的 bug
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
# 送出網址, 這次我送出的是網址列的網址, 回應則是一個網頁的形式
response = urlopen("https://tabelog.com/tw/tokyo/rstLst/?SrtT=rt")
# 把回應丟到解析器
html = BeautifulSoup(response)
# 你可以把它印出來, 不過由於篇幅關係, 我就不把得到的網頁印出了
# print(html)
4 TABELOG 餐廳收集 50

/Users/Elwing/Library/Python/3.6/lib/python/site-packages/bs4/__init__.py:181:
UserWarning: No parser was explicitly specified, so I'm using the best available HTML
parser for this system ("lxml"). This usually isn't a problem, but if you run this
code on another system, or in a different virtual environment, it may use a different
parser and behave differently.

The code that caused this warning is on line 193 of the file
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/runpy.py. To get rid
of this warning, change code that looks like this:

BeautifulSoup([your markup])

to this:

BeautifulSoup([your markup], "lxml")

markup_type=markup_type))

上方的 warning 請無視他就好,BeautifulSoup 其實會在你的電腦找尋本來就有的解析器來幫


助他分析,這裡是說他使用的是他覺得系統裡最好的解析器,問你要不要固定成這個解析器,如果
你固定住的話,如果你的程式碼移到別的系統內,剛好那系統沒有那解析器的話會直接無法執行!
所以讓 BeautifulSoup 自動去尋找他心中的『最佳』解析器就好

4.3.3 找元素

接著你最需要學會的事情就是尋找包含你想要的答案的『盒子』(元素),在 BeautifulSoup 最
基本的兩個操作就是:
❤ find: 找第一個符合條件的元素
❤ find_all: 找所有符合條件的元素,回傳答案是一個 List
在找尋元素的時候,你可以使用下面的兩個『特徵』幫忙做篩選,也就是上面我們提到的『class』
和『id』

# 根據 class 來找, 以 find 為例, find_all 同理


# 這裡參數要特別加一個 _ 底線, 為的是避掉 Python 關鍵字 class(物件導向的設計圖)
元素.find(" 想要找的元素品牌, ex:a, img", class_=" 一個職業名")

# 根據 class 來找, 以 find 為例, find_all 同理


# 這裡參數要特別加一個 _ 底線, 為的是避掉 Python 關鍵字 class(物件導向的設計圖)
元素.find(" 想要找的元素品牌, ex:a, img", id=" 身份證")

接著你要使用另外一個工具來幫你定位盒子,請見下圖
4 TABELOG 餐廳收集 51

圖: 定位的步驟

你會看到一個每個餐廳其實都是一個 li 盒子,li 其實是列表的一個一個『清單項目』的意思,不


過你其實不知道也沒關係!怎麼說呢?我們試試看下面的程式

[程式]: from urllib.request import urlopen


from bs4 import BeautifulSoup
import ssl
ssl._create_default_https_context = ssl._create_unverified_context

response = urlopen("https://tabelog.com/tw/tokyo/rstLst/?SrtT=rt")
html = BeautifulSoup(response)
# 使用 find_all, 因為要找多個
# 這裡我通常喜歡單用一個職業找, 比較不容易因為網頁的改變而必須重寫程式
restaurants = html.find_all("li", class_="list-rst")
# 我只印第一家餐廳給你看
print(restaurants[0])

<li class="list-rst js-list-item" data-id="13124391">


<div class="list-rst__header">
<p class="list-rst__name">
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1307/A130701/13124391/"
target="_blank">Matsukawa</a>
<small class="list-rst__name-ja"> 松川</small>
</p>
<ul class="list-rst__area-catg">
<li class="list-rst__area">
4 TABELOG 餐廳收集 52

六本木 乃木坂 西麻布 </li>


<li class="list-rst__catg"> 和式餐馆 / 小菜</li>
</ul>
</div>
<div class="list-rst__contents u-clearfix">
<div class="list-rst__info u-clearfix">
<ul class="list-rst__rate">
<li class="c-rating c-rating--val45 c-rating--lg"><i class="c-rating__star"></i><b
class="c-rating__val">4.95</b></li><li class="c-rating c-rating--val45 c-rating--
sm"><span class="c-rating__time c-rating__time--dinner">Dinner:</span><b
class="c-rating__val">4.90</b></li><li class="c-rating c-rating--val45 c-rating--
sm"><span class="c-rating__time c-rating__time--lunch">Lunch:</span><b
class="c-rating__val">4.88</b></li> <li class="list-rst__reviews">
<a class="list-rst__reviews-target"
href="https://tabelog.com/tw/tokyo/A1307/A130701/13124391/dtlrvwlst/" target="_blank">
<b>181</b>
評價 </a>
</li>
</ul>
<ul class="list-rst__price">
<li class="c-rating c-rating--sm">
<span class="c-rating__time c-rating__time--dinner">Dinner:</span>
<span class="c-rating__val">¥30,000~</span>
</li>
<li class="c-rating c-rating--sm">
<span class="c-rating__time c-rating__time--lunch">Lunch:</span>
<span class="c-rating__val">¥30,000~</span>
</li>
</ul>
<div class="list-rst__option">
</div>
</div>
<p class="list-rst__for-detail gly-b-sharing"> 查看詳情</p>
</div>
<p class="list-rst__img">
<a class="c-img-target" href="https://tabelog.com/tw/tokyo/A1307/A130701/13124391/"
target="_blank">
<img alt="150x150 square 96294254" class="c-img c-img--frame" height="150"
src="https://tblg.k-img.com/restaurant/images/Rvw/96294/150x150_square_96294254.jpg"
width="150"/> </a>
</p>
</li>
4 TABELOG 餐廳收集 53

4.3.4 再繼續找元素

找到你想要的元素以後,再繼續深入把你想要的元素萃取出來,不過這裡要注意一下,我們
find_all 得到的是一個列表,而不是元素,所以你必須用 for…in 走過一個一個元素,再繼續 find

圖: 找我們要的部分,我想要英文名字以及他連到的 blog

順帶一提,你可以對步驟 2 那邊的 class 快點兩下,即可直接用複製的!

[程式]: from urllib.request import urlopen


from bs4 import BeautifulSoup
import ssl
ssl._create_default_https_context = ssl._create_unverified_context

response = urlopen("https://tabelog.com/tw/tokyo/rstLst/?SrtT=rt")
html = BeautifulSoup(response)
restaurants = html.find_all("li", class_="list-rst")
# 走過每一家餐廳, 對那個餐廳盒子再繼續下去找
for r in restaurants:
# 只找第一個 match, 所以使用 find
en = r.find("a", class_="list-rst__name-main")
print(en)

<a class="list-rst__name-main js-detail-anchor"


href="https://tabelog.com/tw/tokyo/A1307/A130701/13124391/"
target="_blank">Matsukawa</a>
4 TABELOG 餐廳收集 54

<a class="list-rst__name-main js-detail-anchor"


href="https://tabelog.com/tw/tokyo/A1308/A130802/13015251/" target="_blank">Saito</a>
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1301/A130101/13200949/"
target="_blank">Shinohara</a>
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1302/A130204/13018162/" target="_blank">Sugita</a>
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1314/A131405/13159567/"
target="_blank">Quintessence</a>
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1301/A130103/13002887/" target="_blank">Kyoaji</a>
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1307/A130702/13121866/" target="_blank">Restaurant
l'equateur</a>
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1307/A130704/13182678/"
target="_blank">SUGALABO</a>
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1303/A130302/13072775/"
target="_blank">PELLEGRINO</a>
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1308/A130801/13134517/"
target="_blank">Hashiguchi</a>
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1306/A130602/13116356/"
target="_blank">L'Effervescence</a>
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1313/A131301/13176780/" target="_blank">Furuta</a>
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1301/A130101/13203796/" target="_blank">CHIUnE</a>
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1308/A130802/13005027/" target="_blank">Tomura</a>
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1301/A130103/13172920/" target="_blank">Choyo</a>
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1307/A130701/13001332/" target="_blank">Ajiman</a>
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1319/A131905/13159782/" target="_blank">SATO
Buriand</a>
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1309/A130905/13049130/" target="_blank">Kohaku</a>
<a class="list-rst__name-main js-detail-anchor"
href="https://tabelog.com/tw/tokyo/A1309/A130902/13042204/" target="_blank">Mitani</a>
<a class="list-rst__name-main js-detail-anchor"
4 TABELOG 餐廳收集 55

href="https://tabelog.com/tw/tokyo/A1316/A131601/13041029/"
target="_blank">Torishiki</a>

4.3.5 萃取目標

你會發現我們通常有兩個目標會想萃取,第一個是『紙條』(內容),對於上面的例子來說就是
英文店名,第二個是『特殊特徵』,對於上面的例子就是 href 特徵,語法分別如下

# 萃取紙條, 使用 text 把標籤全部去掉


元素.text

# 萃取特殊特徵, 跟字典一樣使用 []
元素 [特殊特徵]

[程式]: from urllib.request import urlopen


from bs4 import BeautifulSoup
import ssl
ssl._create_default_https_context = ssl._create_unverified_context

response = urlopen("https://tabelog.com/tw/tokyo/rstLst/?SrtT=rt")
html = BeautifulSoup(response)
restaurants = html.find_all("li", class_="list-rst")
for r in restaurants:
en = r.find("a", class_="list-rst__name-main")
# 萃取紙條和特殊特徵
print(en.text, en["href"])

Matsukawa https://tabelog.com/tw/tokyo/A1307/A130701/13124391/
Saito https://tabelog.com/tw/tokyo/A1308/A130802/13015251/
Shinohara https://tabelog.com/tw/tokyo/A1301/A130101/13200949/
Sugita https://tabelog.com/tw/tokyo/A1302/A130204/13018162/
Quintessence https://tabelog.com/tw/tokyo/A1314/A131405/13159567/
Kyoaji https://tabelog.com/tw/tokyo/A1301/A130103/13002887/
Restaurant l'equateur https://tabelog.com/tw/tokyo/A1307/A130702/13121866/
SUGALABO https://tabelog.com/tw/tokyo/A1307/A130704/13182678/
PELLEGRINO https://tabelog.com/tw/tokyo/A1303/A130302/13072775/
Hashiguchi https://tabelog.com/tw/tokyo/A1308/A130801/13134517/
L'Effervescence https://tabelog.com/tw/tokyo/A1306/A130602/13116356/
Furuta https://tabelog.com/tw/tokyo/A1313/A131301/13176780/
CHIUnE https://tabelog.com/tw/tokyo/A1301/A130101/13203796/
Tomura https://tabelog.com/tw/tokyo/A1308/A130802/13005027/
Choyo https://tabelog.com/tw/tokyo/A1301/A130103/13172920/
Ajiman https://tabelog.com/tw/tokyo/A1307/A130701/13001332/
SATO Buriand https://tabelog.com/tw/tokyo/A1319/A131905/13159782/
4 TABELOG 餐廳收集 56

Kohaku https://tabelog.com/tw/tokyo/A1309/A130905/13049130/
Mitani https://tabelog.com/tw/tokyo/A1309/A130902/13042204/
Torishiki https://tabelog.com/tw/tokyo/A1316/A131601/13041029/

4.3.6 再繼續找元素

在接下來的日文名字比較簡單,但是比較難的是評分的部分,評分難的是三個評分的元素長得
一模一樣

圖: 三個分數的盒子都長得一樣

如果你只是要找第一個,使用 find 即可,但如果你想要找日間或晚間的分數,那就必須使用


find_all,再利用 [座號] 來指出分數

[程式]: from urllib.request import urlopen


from bs4 import BeautifulSoup
import ssl
ssl._create_default_https_context = ssl._create_unverified_context

response = urlopen("https://tabelog.com/tw/tokyo/rstLst/?SrtT=rt")
html = BeautifulSoup(response)
restaurants = html.find_all("li", class_="list-rst")
for r in restaurants:
en = r.find("a", class_="list-rst__name-main")
# 抓取日本名, 這邊請讀者試一下去找元素
ja = r.find("small", class_="list-rst__name-ja")
# 抓取所有的分數先
scores = r.find_all("b", class_="c-rating__val")
# 利用 [0] 取得第一個 (綜合評分) [1] 取得第二個 (晚間評分) [2] 取得第三個 (午間評分)
print(scores[0].text, scores[1].text, scores[2].text, ja.text, en.text, en["href"])

4.95 4.90 4.88 松川 Matsukawa https://tabelog.com/tw/tokyo/A1307/A130701/13124391/


4.89 4.81 4.86 鮨 さいとう Saito https://tabelog.com/tw/tokyo/A1308/A130802/13015251/
4.86 4.85 4.36 銀座 しのはら Shinohara https://tabelog.com/tw/tokyo/A1301/A130101/13200949/
4 TABELOG 餐廳收集 57

4.84 4.79 4.80 日本橋蛎殻町 すぎた Sugita https://tabelog.com/tw/tokyo/A1302/A130204/13018162/


4.79 4.80 4.51 カンテサンス Quintessence
https://tabelog.com/tw/tokyo/A1314/A131405/13159567/
4.75 4.77 4.53 京味 Kyoaji https://tabelog.com/tw/tokyo/A1301/A130103/13002887/
4.74 4.74 4.62 エクアトゥール Restaurant l'equateur
https://tabelog.com/tw/tokyo/A1307/A130702/13121866/
4.71 4.75 3.54 SUGALABO SUGALABO https://tabelog.com/tw/tokyo/A1307/A130704/13182678/
4.66 4.66 - ペレグリーノ PELLEGRINO https://tabelog.com/tw/tokyo/A1303/A130302/13072775/
4.64 4.64 - 鮨 はしぐち Hashiguchi https://tabelog.com/tw/tokyo/A1308/A130801/13134517/
4.62 4.46 4.67 レフェルヴェソンス L'Effervescence
https://tabelog.com/tw/tokyo/A1306/A130602/13116356/
4.62 4.62 - フルタ Furuta https://tabelog.com/tw/tokyo/A1313/A131301/13176780/
4.61 4.55 4.33 CHIUnE CHIUnE https://tabelog.com/tw/tokyo/A1301/A130101/13203796/
4.61 4.63 3.09 と村 Tomura https://tabelog.com/tw/tokyo/A1308/A130802/13005027/
4.60 4.60 - 趙楊 Choyo https://tabelog.com/tw/tokyo/A1301/A130103/13172920/
4.60 4.60 - 味満ん Ajiman https://tabelog.com/tw/tokyo/A1307/A130701/13001332/
4.59 4.61 3.55 SATO ブリアン にごう SATO Buriand
https://tabelog.com/tw/tokyo/A1319/A131905/13159782/
4.58 4.58 - 虎白 Kohaku https://tabelog.com/tw/tokyo/A1309/A130905/13049130/
4.58 4.45 4.62 三谷 Mitani https://tabelog.com/tw/tokyo/A1309/A130902/13042204/
4.57 4.57 3.15 鳥しき Torishiki https://tabelog.com/tw/tokyo/A1316/A131601/13041029/

4.4 所有的頁面下載

我們在上面已經學會了一個頁面的下載,我們現在試試看整個頁面的下載

圖: 觀察網址的規律

你會發現每個頁面差別在中間的數字,而且有 1 和沒有 1 都是首頁,我們使用迴圈來做事,不


過由於我們不知道到底完整有幾頁,所以我們使用 while 來做迴圈,而且由於我們不知道停止條件,
我們先使用 while True 做到死再說,最後在把觀察到的停止條件利用 break 放進去!
4 TABELOG 餐廳收集 58

圖: 在跑到死的時候會出現的錯誤

這個 400 Error 是什麼呢?400 是指這是個『壞問題』


,對應到這裡的例子就是,明明最後一頁
是第 60 頁,你卻問第 61 頁!就會得到 400 這個錯誤!你可以按下面的網址看到這個錯誤
https://tabelog.com/tw/tokyo/rstLst/61/?SrtT=rt
那我們要怎麼讓這個程式正常結束呢?由於當你 urlopen 那一剎那,錯誤已經發生,沒辦法用
if-else 來偵測,所以我們改使用 try-except 這個『事後解決』的機制

[程式]: from urllib.request import urlopen


# 多 import 錯誤
from urllib.error import HTTPError
from bs4 import BeautifulSoup
import ssl
ssl._create_default_https_context = ssl._create_unverified_context

# 從第一頁開始
# page = 1
# 我因為講義篇幅關係,從 59 頁開始
page = 59
while True:
# 利用增加的數字製作網址
url = "https://tabelog.com/tw/tokyo/rstLst/" + str(page) + "/?SrtT=rt"
print("[正在處理]", url)
# 增加處理機制
try:
response = urlopen(url)
# 這裡你要看一下上面是發生什麼錯誤
except HTTPError:
print("[完成] 應該是來到最後一頁了")
break
html = BeautifulSoup(response)
restaurants = html.find_all("li", class_="list-rst")
for r in restaurants:
en = r.find("a", class_="list-rst__name-main")
ja = r.find("small", class_="list-rst__name-ja")
scores = r.find_all("b", class_="c-rating__val")
4 TABELOG 餐廳收集 59

print(scores[0].text, scores[1].text, scores[2].text, ja.text, en.text, en["href"])


# 增加 page
page = page + 1

[正在處理] https://tabelog.com/tw/tokyo/rstLst/59/?SrtT=rt
3.66 3.66 - 酒膳 蔵四季 Shuzenkurashiki
https://tabelog.com/tw/tokyo/A1321/A132102/13125348/
3.66 3.85 3.58 ラ ヴィータ LA VITA https://tabelog.com/tw/tokyo/A1309/A130903/13012197/
3.66 3.66 - そば工房 玉江 Sobakouboutamae
https://tabelog.com/tw/tokyo/A1323/A132301/13047621/
3.66 3.66 - 富士屋本店 日本橋浜町 Fujiyahontennihombashihamachou
https://tabelog.com/tw/tokyo/A1302/A130204/13193413/
3.66 3.66 - 鯛良 六本木店 Taira https://tabelog.com/tw/tokyo/A1307/A130701/13108409/
3.66 3.66 - BAR 五 Bago https://tabelog.com/tw/tokyo/A1303/A130302/13093230/
3.66 3.66 3.63 レストラン セン Restaurant Sen
https://tabelog.com/tw/tokyo/A1309/A130904/13117224/
3.66 3.66 - 楽食ふじた rakushokufujita https://tabelog.com/tw/tokyo/A1317/A131701/13166724/
3.66 3.65 3.63 レストラン 代官山小川軒 Resutorandaikanyamaogawaken
https://tabelog.com/tw/tokyo/A1303/A130303/13001763/
3.66 3.60 3.63 すずき Suzuki https://tabelog.com/tw/tokyo/A1316/A131604/13122982/
3.66 3.66 - カラペティ バトゥバ QUAND L'APPETIT VA TOUT VA
https://tabelog.com/tw/tokyo/A1307/A130702/13097828/
3.66 3.60 3.59 green glass guri-ngurasu
https://tabelog.com/tw/tokyo/A1321/A132104/13196314/
3.66 3.66 - はせ川 Hasegawa https://tabelog.com/tw/tokyo/A1312/A131201/13143482/
3.66 3.66 3.50 重よし Shigeyoshi https://tabelog.com/tw/tokyo/A1306/A130601/13001153/
3.66 3.66 - ロウホウトイ Rouhoutoi https://tabelog.com/tw/tokyo/A1307/A130703/13203446/
3.66 3.66 - すし龍尚 Sushishouryuu https://tabelog.com/tw/tokyo/A1316/A131602/13172896/
3.66 3.56 3.58 ソンブルイユ SOMBREUIL https://tabelog.com/tw/tokyo/A1309/A130905/13211031/
3.66 3.64 3.52 Libre 白金高輪店 Libre https://tabelog.com/tw/tokyo/A1316/A131602/13222761/
3.66 3.66 3.58 割烹 山路 Kappouyamaji https://tabelog.com/tw/tokyo/A1301/A130103/13195108/
3.66 3.66 3.08 きんとき Kintoki https://tabelog.com/tw/tokyo/A1320/A132002/13044590/
[正在處理] https://tabelog.com/tw/tokyo/rstLst/60/?SrtT=rt
3.66 3.64 3.52 の弥七 Noyashichi https://tabelog.com/tw/tokyo/A1309/A130903/13209784/
3.66 3.75 3.39 calme Calme https://tabelog.com/tw/tokyo/A1317/A131705/13197166/
3.66 3.66 3.46 ファイヤーホール 4000 麻布十番店 Faiyahoruyonsen
https://tabelog.com/tw/tokyo/A1307/A130702/13220650/
3.66 3.66 3.06 銀屋 Ginya https://tabelog.com/tw/tokyo/A1316/A131602/13153190/
3.66 3.66 - 和食 ひまわり Washokuhimawari
https://tabelog.com/tw/tokyo/A1301/A130103/13114242/
3.66 3.66 - 江戸前 鮨 服部 edomaesushihattori
https://tabelog.com/tw/tokyo/A1307/A130701/13098338/
3.66 3.66 - あらいかわ Araikawa https://tabelog.com/tw/tokyo/A1307/A130702/13218481/
3.66 3.66 - バーホシ イーリス Bahoshiirisu
https://tabelog.com/tw/tokyo/A1301/A130101/13200352/
3.66 3.66 3.06 くすのき Kusunoki https://tabelog.com/tw/tokyo/A1304/A130401/13223239/
4 TABELOG 餐廳收集 60

3.65 3.65 3.64 駒形どぜう 本店 Komagatadojou


https://tabelog.com/tw/tokyo/A1311/A131102/13003684/
3.65 3.61 3.66 つじ半 Tsujihan https://tabelog.com/tw/tokyo/A1302/A130202/13146590/
3.65 3.62 3.64 ラ ベットラ ダ オチアイ LA BETTOLA da Ochiai
https://tabelog.com/tw/tokyo/A1313/A131301/13002504/
3.65 3.64 3.63 VIRON viron https://tabelog.com/tw/tokyo/A1302/A130201/13043904/
3.65 3.59 3.65 ベーカーバウンス Bekabaunsu
https://tabelog.com/tw/tokyo/A1317/A131706/13004780/
3.65 3.52 3.63 カレーハウス チリチリ Curry House TIRI TIRI
https://tabelog.com/tw/tokyo/A1303/A130301/13002119/
3.65 3.53 3.63 パネッテリア アリエッタ 五反田本店 Panetteria ARIETTA
https://tabelog.com/tw/tokyo/A1316/A131603/13019888/
3.65 3.56 3.64 ショコラティエ イナムラショウゾウ Shokorathieinamurashouzou
https://tabelog.com/tw/tokyo/A1311/A131105/13047298/
3.65 3.61 3.66 伊勢廣 京橋本店 Isehiro https://tabelog.com/tw/tokyo/A1302/A130202/13000541/
3.65 3.64 3.54 本とさや Hontosaya https://tabelog.com/tw/tokyo/A1311/A131102/13003711/
3.65 3.59 3.64 中華そば ムタヒロ 1 号店 chuukasobamutahiro
https://tabelog.com/tw/tokyo/A1325/A132502/13131257/
[正在處理] https://tabelog.com/tw/tokyo/rstLst/61/?SrtT=rt
[完成] 應該是來到最後一頁了

4.5 加上儲存

我們抓完資料,勢必要把資料儲存,我們先儲存成最基本格式,CSV 格式!別忘了 CSV 是用


『逗號』分開每一格格子,那只要想到表格的儲存,一定要把 pandas 拿出來!大致的語法如下

# 先 import
import pandas as pd
# 準備空的 DataFrame, 先固定住 columns
df = pd.DataFrame(columns=["a", "b"])
# 準備一個 Series, index 的欄位要跟 columns 對到
s = pd.Series([資料 1, 資料 2], index=["a", "b"])
# 因為 Series 沒有橫列的標籤, 所以加進去的時候一定要 ignore_index=True
df = df.append(s, ignore_index=True)
# 儲存成 csv, 不過列編號的數字不用存, 所以我把 index=False
df.to_csv(" 檔名.csv", encoding="utf-8", index=False)

[程式]: from urllib.request import urlopen


from urllib.error import HTTPError
from bs4 import BeautifulSoup
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
4 TABELOG 餐廳收集 61

import warnings
warnings.filterwarnings('ignore')
# 先 import
import pandas as pd
# 準備空的 DataFrame
df = pd.DataFrame(columns=["綜合評分", "晚間評分", "午間評分", "日文店名", "英文店名", "Blog"])

page = 1
while True:
url = "https://tabelog.com/tw/tokyo/rstLst/" + str(page) + "/?SrtT=rt"
try:
response = urlopen(url)
except HTTPError:
break
html = BeautifulSoup(response)
restaurants = html.find_all("li", class_="list-rst")
for r in restaurants:
en = r.find("a", class_="list-rst__name-main")
ja = r.find("small", class_="list-rst__name-ja")
scores = r.find_all("b", class_="c-rating__val")
# 準備 Series 和 append 進 DataFrame
# 這邊由於會超過頁面, 所以我把 list 換行一下
s = pd.Series([scores[0].text, scores[1].text,
scores[2].text, ja.text, en.text, en["href"]],
index=["綜合評分", "晚間評分", "午間評分", "日文店名", "英文店名", "Blog"])
df = df.append(s, ignore_index=True)

page = page + 1
# 輸出 csv
df.to_csv("tabelog.csv", encoding="utf-8", index=False)

儲存的 CSV 長成這樣

圖: 儲存的 CSV

順帶一提,由於 Windows 的預設編碼問題,直接使用 Excel 開通常會有問題,會顯示亂碼,請


4 TABELOG 餐廳收集 62

按照下面的步驟做,不過記得在關掉的時候不要儲存回去,如果要再執行程式前也記得把開起來的
檔案關掉 (Excel 會把檔案鎖住)

圖: EXCEL 開啟步驟 (上)


4 TABELOG 餐廳收集 63

圖: EXCEL 開啟步驟 (下)

4.6 結語

恭喜你完成了第一個網頁的練習,我們後續還要再用各種例子來讓大家學習網頁的知識!
5 GOOGLE 小姐姐 64

5 Google 小姐姐

這次我們來試一個有趣一點的練習,我們來試試看 Google 小姐,這次一定幫你練習網路最重


要的一件事,『網路的一問一答』以及『網址的構成』

5.1 介紹

我們先來到 https://translate.google.com.tw/?hl=zh-TW Google 翻譯的地方

圖: Google 小姐住處

那別忘記了,網路是『動態』的,隨著你的動作,可能會有更多的『問題』(網址) 被送給伺服
器,所以請你把『更多工具』-『開發人員工具』- 『Network』一樣開起來,你會發現,隨著你按下
廣播的按鍵的時候,有一個問題悄悄被問出去
5 GOOGLE 小姐姐 65

圖: 按下廣播的時候被問出的問題

當你把這個問題放在旁邊的網頁送出的時候,應該會震驚的發現,你單獨得到了 mp3,而且下
載不過是一個按鍵的事

圖: 可以下載了

如果你更仔細一點,你還會發現如果是中文的 Google 小姐,第二次竟然會是慢速版,而且這


些資訊一定都藏在『網址』的『附加資訊上』
5 GOOGLE 小姐姐 66

圖: 第二次慢速版網址

這 是 第 一 次 的 網 址: https://translate.google.com.tw/translate_tts?ie=
UTF-8&q=%E4%BD%A0%E5%A5%BD&tl=zh-TW&total=1&idx=0&textlen=2&tk=612044.
1040128&client=t
這 是 第 二 次 的 網 址: https://translate.google.com.tw/translate_tts?ie=
UTF-8&q=%E4%BD%A0%E5%A5%BD&tl=zh-TW&total=1&idx=0&textlen=2&tk=612044.
1040128&client=t&ttsspeed=0.24
你可以清楚地看到 ttsspeed 就會是念的速度,那我們可不可以自己修改這速度呢?何不你自
己試試看下面的這個我修改過的網址呢!?
https://translate.google.com.tw/translate_tts?ie=UTF-8&q=%E4%BD%A0%
E5%A5%BD&tl=zh-TW&total=1&idx=0&textlen=2&tk=612044.1040128&client=t&
ttsspeed=0.1
是不是又更慢了!如果忘記的讀者請翻到 Doodles 的章節,好好的複習『網址構成』!學會網
址的構成讓你可以輕鬆自如地想要什麼,就拿什麼!

5.2 開始寫程式

5.2.1 選擇網址

由於上面的網址很多參數我們並不知道怎麼設定,這裡提供一個特別,老師私藏的網址當作我
們送出的網址
https://translate.google.com/translate_tts?ie=UTF-8&tl=en&client=
tw-ob&q=hello
其實就是上面網址的不同版本 (client 不同),這個網址基本上問號後面的每一個資料我們都知
道要填什麼
❤ ie: 編碼,固定式 utf-8
5 GOOGLE 小姐姐 67

❤ tl: 哪國 Google 小姐,常用的 en: 英文的 Google 小姐,zh-TW: 中文的 Google 小姐


❤ client: 這個簡短的網址一定要固定 tw-ob 這個伺服器
❤ q: 我們要 Google 小姐念的東西

5.2.2 先來試試看一小段文字

我們先從一小段文字的轉換和下載開始,這裡如果沒加 header 會返回 403 錯誤,上一節我們


介紹過 403 錯誤,再幫大家複習一下處理兩種方式

1. 使用內建的 urllib 並且加上 header

2. 使用第三方函式庫 requests,建議使用,會讓整個過程變得比較簡單

# 第一種方式, 內建函式庫, MAC 電腦同學記得多加入 ssl


import ssl
ssl._create_default_https_context = ssl._create_unverified_context
from urllib.request import urlopen, Request
r = Request(" 網址")
r.add_header("User-Agent", "Mozilla/5.0")
response = urlopen(r)
# 使用 read 把回應的部分用原始的方式讀取出來
audio = response.read()

第二種方式如下

# 第二種方式, 使用 requests 第三方函式庫, 會幫你加一些基本的東西


import requests
response = requests.get(" 網址")
# requests 如果需要不是純文字的答案, 你必須使用 content 來讀取
audio = response.content

儲存的方式一樣分兩種,這裡唯一要記得的是,以前我們的 open 裡的模式都是使用 “r”(讀取),


“w”(寫入) 再加上 encoding=“utf-8”,但我們現在儲存的是多媒體檔,而不是存文字檔,不會再有
encoding 這種東西了,所以你要改成 “rb”(原始讀取),“wb”(原始寫入)

1. 三部曲: open -> write -> close


2. with…as: 好處是會自動幫你關閉檔案,建議使用

# 第一種儲存方式
f = open(" 檔名", "wb")
f.write(audio)
f.close()
5 GOOGLE 小姐姐 68

with…as 會自動幫你處理檔案的關閉 (無論正常不正常)

# 第二種儲存方式
with open(" 檔名", "wb") as f:
f.write(audio)

[程式]: import requests


url = "https://translate.google.com/translate_tts?ie=UTF-8&tl=en&client=tw-ob&q=hello"
response = requests.get(url)
# 不要有解碼過的字串的話, 使用 content 來得到原始回應
audio = response.content
with open("test.mp3", "wb") as f:
f.write(audio)

你應該可以看到 mp3 已經被正確的儲存到同一層的資料夾了

圖: 儲存的 mp3

接下來我們試試看中文的 Google 小姐,順便加入檔案的讀取,我們準備一個 a.txt 放在同一層

圖: 我準備的文章

這裡要記得一下,由於 Google 的這個網址是有長度限制的,所以請先準備一個短一點的!補


充資料我們在來試試看長文章,純文字的文章記得要使用 “r” 或者 “w” 配合 encoding=“utf-8”

[程式]: import requests


# 開啟檔案, 記得要放在跟你的.py 或.ipynb 同一層, 使用"r" + encoding
with open("a.txt", "r", encoding="utf-8") as f:
article = f.read()
5 GOOGLE 小姐姐 69

# 組合成完整字串, article 是字串, 所有串連沒有問題


# 因為這裡會超過頁面篇幅, 所以我分成兩行書寫
base = "https://translate.google.com/translate_tts?ie=UTF-8&tl=zh-TW&client=tw-ob&q="
url = base + article
response = requests.get(url)
audio = response.content
with open("test.mp3", "wb") as f:
f.write(audio)

5.2.3 調整速度

對於中文的 Google 小姐,你可以透過 ttsspeed 來調整速度,我們來試試看

[程式]: import requests


with open("a.txt", "r", encoding="utf-8") as f:
article = f.read()

base = "https://translate.google.com/translate_tts?ie=UTF-8&tl=zh-TW&client=tw-ob&q="
# 我設定在外面方便調整, 當然讀者也可以做成個函數, 方便帶入不同數值
speed = 0.1
# 加入 ttsspeed 的網址參數, 由於 speed 是數字, 一定要轉換成字串才能串連
url = base + article + "&ttsspeed=" + str(speed)
response = requests.get(url)
audio = response.content
with open("test.mp3", "wb") as f:
f.write(audio)

5.3 大量文章轉換

如果要對大量文章轉換成 mp3 該如何做呢?這次我準備好一個資料夾,放入多篇文章,這裡


記得資料夾要事先創好,並且創在專案的同一層

圖: 我準備好的 input 資料夾以及兩篇文章


5 GOOGLE 小姐姐 70

要處理大量的檔案最方便的是內建的 glob.py,他裡面有個 glob 函式可以快速幫你找到符合格


式的檔案

import glob
# 會回傳一個所有符合格式的檔名 list
# 格式可以用 * 來表示任意字元
flist = glob.glob(" 檔名格式")

[程式]: import glob


# ./代表同一層
glob.glob("./input/*.txt")

[輸出]: ['./input/chou.txt', './input/lin.txt']

對於儲存的檔案,我會希望用原本檔名的部分來當輸出的 mp3 檔名,那儲存的資料夾位置,我


希望是一個叫做 output 資料夾的地方!這邊有兩個方法,第一個是親手把資料夾創好,第二個是使
用我們在 doodles 做的,先檢查存在,如果不存在的話,就創起資料夾!

# 創造資料夾
import os
# 在建立前必須先檢查資料夾有沒有存在
# 不存在才可以創建
if not os.path.exists(" 資料夾名"):
os.mkdir(" 資料夾名")

[程式]: import glob


import requests
import os

flist = glob.glob("./input/*.txt")
for fname in flist:
# 替換成 fname
with open(fname, "r", encoding="utf-8") as f:
print("[處理中]:", fname)
article = f.read()

base = "https://translate.google.com/translate_tts?ie=UTF-8&tl=zh-TW&client=tw-ob&q="
url = base + article
response = requests.get(url)
audio = response.content
# 準備存檔的名字, 把.txt 替換成.mp3 即可
savename = fname.replace(".txt", ".mp3")
# 這裡比較特別, 我把 input 替換成 output, 但只替換掉最左邊的 input, 帶入選用參數 (1)
5 GOOGLE 小姐姐 71

# 因為如果你全部替換有可能會把 input.txt 變成 output.mp3 而覆蓋掉其他 mp3


savename = savename.replace("input", "output", 1)
# 在建立前必須先檢查資料夾有沒有存在
# 不存在才可以創建
if not os.path.exists("./output"):
os.mkdir("./output")

with open(savename, "wb") as f:


print("[儲存中]:", savename)
f.write(audio)

[處理中]: ./input/chou.txt
[儲存中]: ./output/chou.mp3
[處理中]: ./input/lin.txt
[儲存中]: ./output/lin.mp3

圖: 儲存的結果

我們已經學會大量處理文章了,如果你發現你的 mp3 並沒有任何內容,0bytes,請你稍微減


縮一下文字量,因為他有限制每次最大的文字量,下面我會教你如何處理長篇文章!

5.4 進階 - 長文章

這裡最後補充一下如果太長的文章該如何做,如果你直接讀取長度太長的文章,會出現錯誤,
不過在 requests 模組,我們要多學一個功能,叫做 raise_for_status(),不像 urlopen 可以直接回傳
錯誤,你必須在使用後才會吐出錯誤,我準備了一個 b.txt,這裡就是更長的文章了
5 GOOGLE 小姐姐 72

圖: 我準備的長篇文章

你 可 以 試 試 看 下 面 的 程 式, 你 會 發 現 錯 誤 就 出 來 了, 那 你 當 然 也 可 以 以 使 用 re-
sponse.status_code 或者 try…except 去處理
這裡我們收到一個叫做 404 的錯誤,404 的錯誤的意思是 Not Found,
『資源不存在』的意思,
這裡 Google 回 404 的意思就是告訴你,這太長了,這麼長的資源我們無法翻譯,無法提供給你這
個資源!

[程式]: import requests


with open("b.txt", "r", encoding="utf-8") as f:
article = f.read()
# 因為篇幅關係, 我先把基本 url 放在這裡
baseurl = "https://translate.google.com/translate_tts?ie=UTF-8&tl=en&client=tw-ob&q="
url = baseurl + article
response = requests.get(url)
response.raise_for_status()
5 GOOGLE 小姐姐 73

5.4.1 NLTK 函式庫

這裡我的做法是把文章切成一句一句句子,然後把得到的 mp3 串接起來直接寫入檔案,之所


以這樣選擇是因為一段一段拿的時候,是因為每次拿 mp3 都會有前面的一段空白,空白在句子間
蠻適合的,但是如果在字和字之間會有點突兀,所以我不會選擇『每幾個字拿一次』這種做法!由
於英文的句子在分的時候其實沒那麼簡單,所以我選擇最知名的外文自然語言處理函式庫『nltk』幫
助我,請讀者也要安裝一下 nltk 函式庫,你可以看到下面的地方,我們是如何處理一句一句的句子
的!最後串連起來就成為完整的 mp3 了!

[程式]: import requests


# 讀取文章
with open("b.txt", "r", encoding="utf-8") as f:
article = f.read()
# import nltk 並且把句子分出來, 讀者可以印出 tok_article 來看句子 list
from nltk import tokenize
tok_article = tokenize.sent_tokenize(article)
# 因為我們最後才儲存檔案, 所以記得要準備一個箱子, 把一句一句 mp3 先放在箱子裡
# b 的意思是 bytes 的意思, b"" 可以幫你準備一個空的原始內容
allcontent = b""
# 因為篇幅關係, 我先把基本 url 放在這裡
baseurl = "https://translate.google.com/translate_tts?ie=UTF-8&tl=en&client=tw-ob&q="
# 走過 list 裡的一個一個 sentence
for sentence in tok_article:
print("[處理句子]:", sentence)
url = baseurl + sentence
response = requests.get(url)
# 為了怕有任何問題, 所以如果有 HTTP error 我會把它 raise 起來
response.raise_for_status()
# 把得到的 content 加在我們的容器後
audio = response.content
allcontent = allcontent + audio
# 把完整的 mp3 輸出, 注意縮排位址
with open("all.mp3", "wb") as f:
f.write(allcontent)

[處理句子]: Later that day, when the Princess was sitting at the table, something was
heard coming up the marble stairs.
[處理句子]: Splish, splosh, splish splosh!
[處理句子]: The sound came nearer and nearer, and a voice cried, "Let me in, youngest
daughter of the King."
[處理句子]: The Princess jumped up to see who had called her.
[處理句子]: Now when she caught sight of the frog, she turned very pale.
[處理句子]: "What does a frog want with you?"
[處理句子]: demanded the King, looking rather surprised.
[處理句子]: The Princess hung her head.
5 GOOGLE 小姐姐 74

[處理句子]: "When I was sitting by the fountain my golden ball fell into the water.
[處理句子]: This frog fetched it back for me, because I cried so much."
[處理句子]: The Princess started to cry again.
[處理句子]: "I promised to love him and let him eat from my golden plate, drink from my
golden cup, and sleep on my golden bed."
[處理句子]: The King looked at the frog and thought for a while before he spoke.
[處理句子]: "Then you must keep your promise, my daughter."
6 PTT 文章抓取 75

6 PTT 文章抓取

接下來的練習我們試試看 PTT 的抓取,之所以選擇 PTT 是因為 PTT 剛好跟我們上一個練習


是兩個極端,一個是很豪華的網頁,一個是較為簡陋的網頁,有些人會覺得:簡陋的網頁理應更簡
單啊,錯錯錯,其實簡陋的網頁會因為沒有良好的定位盒子方式而變得比較困難!

6.1 目標

先說明一下我們這個練習的目標
❤ 抓取 PTT Movie 版〸頁文章
❤ 把成果存成一個 CSV 檔案
❤ (進階) 抓取有年齡限定的 Gossiping 版

6.2 想要教給你的東西

❤ 403 的解決
❤ 較為簡陋網頁的抓取
❤ Cookie 的使用

6.3 網站分析

一樣開始網站 https://www.ptt.cc/bbs/movie/index.html 分析的動作


6 PTT 文章抓取 76

圖: 目標 (Movie 版首頁)

6.3.1 步驟 1: 打開原始碼

一樣點擊右鍵打開原始碼,這裡應該非常明顯的可以看到我們想要的東西就在原始碼裡!

圖: 原始碼

6.3.2 此網站的結論

一樣使用網址列當作我們 urlopen 的參數即可!

6.4 開始寫程式

但這時候我們發現,直接使用內建的 urlopen,會出現一個 HTTPError,叫做 403 錯誤


6 PTT 文章抓取 77

圖: 403 的 Error

6.4.1 HTTP 錯誤 - 403

那 403 是什麼呢?403 是我們在爬蟲的時候常常遇到的一種錯誤,對應的錯誤代碼是 Forbid-


den,我們說過,網路就如同一個服務台,一個問題一個答案!403 的意思就是服務台拒絕回答問
題。拒絕回答問題通常有兩種可能:
❤ 你一直問一直問,服務台就會把你列為拒絕往來戶,這個我們叫做 IP Ban
❤ 你看起來不像是正常瀏覽器,而像一隻爬蟲程式,這也是我們最常遇到的一種 403,通常對
面檢查你是否是爬蟲程式就是檢查你『網址』的『信封』上面有沒有填寫該寫的欄位 (也就是我們
下面要討論的案例)

6.4.2 Header

Header 就是所謂網址的信封,那該如何填寫這個信封呢?我們藉由下面的例子一起看一下!

[程式]: from urllib.request import urlopen


import ssl
ssl._create_default_https_context = ssl._create_unverified_context
url = "https://www.ptt.cc/bbs/movie/index.html"
# 讀者可以自己把這行註解拿掉, 就會看到上圖的 403
# urlopen(url)

6.4.3 確認 Header

點開 Chrome 的更多工具 -> 開發人員工具 -> Network -> 選到 index.html


6 PTT 文章抓取 78

圖: Network

事實上你會發現,大部分網站檢查的最基本就是『user-agent』這欄位有無填寫,而且最基本
的檢查就是『有寫就好』!

6.4.4 解決 403 Forbidden

先利用內建的 urllib.request 的 Request 設計圖,創造出一個 Request,並且填入 User-Agent


的部分
那我這裡先使用最簡單的『有填就好』,只填了我看到的整段的最前面『Mozilla/5.0』

[程式]: from urllib.request import urlopen, Request


from bs4 import BeautifulSoup
# BeautifulSoup 每次會跳出要你選擇分析器的 warning, 但我並不想固定住, 因為這樣放到別的作業系統會不能運作
# 而為了頁面的美化 不讓 warning 印出
import warnings
warnings.filterwarnings('ignore')

r = Request(url)
r.add_header("User-Agent", "Mozilla/5.0")
response = urlopen(r)
html = BeautifulSoup(response)
# 讀者可以把這行註解拿掉, 看看完整的 html
# html
6 PTT 文章抓取 79

6.4.5 requests 第三方函式庫

事實上,這裡你也可以考慮使用 requests 這個較為方便一點的第三方函式庫


他會自動幫你加上普通的 Header,讓你可以通過 80% 的檢查,但如果有時候你遇到檢查較為
嚴格的網站
還是要學會自己去修改 Header

[程式]: # 要先用 PyCharm 或者 pip 安裝過


import requests
# 這裡比較特別一點, 如果回應是網頁, 記得用.text 把 html 文字萃取出
response = requests.get(url).text
html = BeautifulSoup(response)
# 讀者可以把這行註解拿掉, 看看完整的 html
# html

6.4.6 抓取一篇文章

由簡單而難,我們先挑一篇文章試試看
我挑選的是 https://www.ptt.cc/bbs/movie/M.1540692666.A.E90.html
這裡最難的是,你會發現 main-content 混著『看板』,『時間』,『ID』導致我們很難抓取

圖: 混雜在一起導致難以抓取

所以我們要學會除了 find 以外的另外一個方法,extract(丟棄),當你使用 extract 的時候,你會


將某個『元素』直接『人間蒸發』

[程式]: import requests


from bs4 import BeautifulSoup
url = "https://www.ptt.cc/bbs/movie/M.1540692666.A.E90.html"
response = requests.get(url).text
html = BeautifulSoup(response)
# 找到文章內容部分
6 PTT 文章抓取 80

content = html.find("div", id="main-content")

# 丟掉前可以先保留起我們需要的資訊
values = content.find_all("span", class_="article-meta-value")
print("作者:", values[0].text)
print("看板:", values[1].text)
print("文章標題:", values[2].text)
print("發文時間:", values[3].text)

# 由於文章內容都是混在一起的, 所以我們不能直接用 find 去找, 只好用扣掉的


# extract: 把我們不要的部分從元素裡扣掉
# 扣掉 作者 標題 時間
meta = content.find_all("div", class_="article-metaline")
for m in meta:
m.extract()
# 扣掉 看板名稱
right_meta = content.find_all("div", class_="article-metaline-right")
for single_meta in right_meta:
single_meta.extract()

# 扣掉 推文 的部分
# 這裡我順便統計一下這篇文到底是幾分, 推: +1, 噓: -1
# 至於推文我就不留下了, 想留下的讀者可以自行試試看
pushes = content.find_all("div", class_="push")
# 準備總體推噓分數
score = 0
for single_push in pushes:
# 你仔細看的話, 會發現前面的推噓有個共同的職業 push-tag
pushtag = single_push.find("span", class_="push-tag").text
if "推" in pushtag:
score = score + 1
elif "噓" in pushtag:
score = score - 1
single_push.extract()

print("推噓分數:", score)
# 最後你的 main-content 就是一個乾淨的, 把 text 取出就可以了
print(content.text)

作者: inbow (石更 火暴)


看板: movie
文章標題: [討論] 看了會想吐的電影?
發文時間: Sun Oct 28 10:11:04 2018
6 PTT 文章抓取 81

推噓分數: 84

我本身小時候還滿容易暈車想吐

常常吐的到處都是

會想吐是因為汽油味很重

長大後雖然不會吐了

但只要聞到汽油味還是很不舒服

到現在都很討厭坐車

現在常常看到有人說看電影會暈會想吐

老實說我無法體會

因為從以前我以為會想吐都只是汽油味

很多人玩戰慄時空想吐

看一屍到底也想吐 看超狂亨利也想吐

到底還有哪些是會讓人想吐的電影?

我還不曾看電影看到吐過

很想體會一下

-----
Sent from JPTT on my Asus ASUS_Z00UD.

--
� 發信站: 批踢踢實業坊 (ptt.cc), 來自: 180.204.208.157
� 文章網址: https://www.ptt.cc/bbs/movie/M.1540692666.A.E90.html

恭喜你已經完成一篇文章的抓取了,我們下面再來進階一點,先教你一下如何爬過需要『超過
18 歲』認證的頁面和完整的看板抓取!
6 PTT 文章抓取 82

6.5 需要認證的版面

如果你來到一些譬如是 Gossiping 之類的版,一進去的時候會先被重新導向到一個新的網頁,


叫你按下認證,像這種的該如何爬取呢?我們先來看一下他是如何判斷重新導向的!

6.5.1 目標

我們以 Gossiping 的 https://www.ptt.cc/bbs/Gossiping/M.1542451587.A.A0A.


html 為例,請照著下面的圖操作一下

圖: 被重新導向到新的網頁
6 PTT 文章抓取 83

6.5.2 Cookie

什麼是 Cookie 呢?你應該還記得我們的比喻,在我們送出問題給服務台的時候,要附加額外


的資訊,服務台才有辦法回答你的問題!但是如果我們有固定要附加的資訊,那何不只要向同一個
服務台問問題的時候就自動幫我送出去呢?這就是 cookie 技術的由來!事實上,在像八卦版這種
有驗證年齡的版面都是靠著 Cookie 來判斷你有沒有點按過『已滿 18』按鈕的,如何知道是檢查什
麼 Cookie 呢?請照著下面的圖試試看

圖: 查看 cookie

你可以用下面的程式碼來為你的問題加上 cookie

# 使用 requests 模組較為方便
import requests
jar = requests.cookies.RequestsCookieJar()
# 你可以維護一個糖果罐, 把不同網頁的 cookie 設定進來
jar.set("over18", "1", domain="www.ptt.cc")
# 多帶入 cookies 參數
response = requests.get(" 網頁", cookies=jar)

[程式]: import requests


from bs4 import BeautifulSoup

url = "https://www.ptt.cc/bbs/Gossiping/M.1542451587.A.A0A.html"
6 PTT 文章抓取 84

jar = requests.cookies.RequestsCookieJar()
# 你可以維護一個糖果罐, 把不同網頁的 cookie 設定進來
jar.set("over18", "1", domain="www.ptt.cc")
# 多帶入 cookies 參數
response = requests.get(url, cookies=jar).text

# 接下來的就跟上面一樣!
html = BeautifulSoup(response)
content = html.find("div", id="main-content")

values = content.find_all("span", class_="article-meta-value")


print("作者:", values[0].text)
print("看板:", values[1].text)
print("文章標題:", values[2].text)
print("發文時間:", values[3].text)

meta = content.find_all("div", class_="article-metaline")


for m in meta:
m.extract()
right_meta = content.find_all("div", class_="article-metaline-right")
for single_meta in right_meta:
single_meta.extract()

pushes = content.find_all("div", class_="push")


score = 0
for single_push in pushes:
pushtag = single_push.find("span", class_="push-tag").text
if "推" in pushtag:
score = score + 1
elif "噓" in pushtag:
score = score - 1
single_push.extract()

print("推噓分數:", score)
print(content.text)

作者: kenbo (我愛黃恩賜)


看板: Gossiping
文章標題: [問卦] 認真問,競選晚會到底有什麼好玩的?
發文時間: Sat Nov 17 18:46:25 2018
推噓分數: 36

如題
6 PTT 文章抓取 85

現在幾乎每天都有競選晚會

然後每個候選人都在比自己的場有沒有爆場

問題來了

這種競選晚會

到底有什麼好玩的?????

要在現場待三四個小時

然後候選人來的時間可能不到二十分鐘

最後散場

搞得整個人很累

想問一下這種競選晚會

好玩在哪??
??
-----
Sent from JPTT on my iPhone

--
� 發信站: 批踢踢實業坊 (ptt.cc), 來自: 27.52.44.142
� 文章網址: https://www.ptt.cc/bbs/Gossiping/M.1542451587.A.A0A.html

6.6 完整程式

試試看抓取整個頁面吧!重點就是在首頁要把所有文章的 url 保存起來,這裡有點進階,讀者


想要先跳過此節也可以!這裡我順便將五頁抓取和儲存 CSV 一併加入!

6.6.1 定義單頁函式

由於頁面的抓取是每次都必須的,我會建議定義成一個獨立的函式,版面會變得較為整齊,而
且可以重複利用!但要注意的是,剛剛我們是直接印出來,這裡如果定義成函式就必須改成回傳值!

[程式]: def get_page_meta(url):


jar = requests.cookies.RequestsCookieJar()
jar.set("over18", "1", domain="www.ptt.cc")
6 PTT 文章抓取 86

# 先做最基礎的判斷, 非公告和版規我回傳答案
if not "公告" in title and not "版規" in title:
response = requests.get(article_url, cookies=jar).text
html = BeautifulSoup(response)
content = html.find("div", id="main-content")
# 準備我們要回傳的字典
result = {}
values = content.find_all("span", class_="article-meta-value")
# 先把文章資訊記錄在字典裡
result["author"] = values[0].text
result["board"] = values[1].text
result["title"] = values[2].text
result["time"] = values[3].text

meta = content.find_all("div", class_="article-metaline")


for m in meta:
m.extract()
right_meta = content.find_all("div", class_="article-metaline-right")
for single_meta in right_meta:
single_meta.extract()

pushes = content.find_all("div", class_="push")


score = 0
for single_push in pushes:
pushtag = single_push.find("span", class_="push-tag").text
if "推" in pushtag:
score = score + 1
elif "噓" in pushtag:
score = score - 1
single_push.extract()
# 分數和內容
result["score"] = score
result["content"] = content.text
return result
# 公告和版規我就直接回傳 None
else:
return None

接著把剛剛定義的函式加入到我們的每頁的抓取中!

[程式]: import requests


from bs4 import BeautifulSoup
import re
import pandas as pd
6 PTT 文章抓取 87

jar = requests.cookies.RequestsCookieJar()
jar.set("over18", "1", domain="www.ptt.cc")
# 從 movie 版首頁開始
url = "https://www.ptt.cc/bbs/movie/index.html"
# 準備要記錄的表格
df = pd.DataFrame(columns=["作者", "看板", "標題", "時間", "分數", "內容"])
# 走過五頁, range(5) 會幫你產生一個類似 [0, 1, 2, 3, 4] 的 list
for times in range(5):
response = requests.get(url, cookies=jar).text
html = BeautifulSoup(response)
# 得到每一篇文章的區域
articles = html.find_all("div", class_="r-ent")
# 走過每一篇文章
for single_article in articles:
# 得到 title 的超連結元素 <a>
title_area = single_article.find("div", class_="title").find("a")
# 如果有 title 才繼續 (被刪除的文章會沒有 title)
if title_area:
# 得到 title 的文字
title = title_area.contents[0]
# 得到 title 的超連結屬性 href
article_url = "https://www.ptt.cc" + title_area["href"]
# 使用我們剛剛定義的函式
result = get_page_meta(article_url)
# 檢查是不是回傳 None(公告和版規會回傳 None)
if result:
data = [result["author"], result["board"], result["title"],
result["time"], result["score"], result["content"]]
s = pd.Series(data, index=["作者", "看板", "標題", "時間", "分數", "內容"])
df = df.append(s, ignore_index=True)
# 往下一頁前進, string 參數可以找裡面文字符合我們帶入字串的元素
url = "https://www.ptt.cc" + html.find("a", text=re.compile(r"上頁"))["href"]
df.to_csv("ptt.csv", index=False, encoding="utf-8")

6.6.2 檢視成果

恭喜你,已經成功地完成了數頁的 PTT 抓取了,我們接下來還會用其他的例子讓你多多練習


一下!
6 PTT 文章抓取 88

圖: 我們抓取出來的成果
7 狂新聞下載 89

7 狂新聞下載

7.1 介紹

『POST』
這個主題除了要教你如何處理新聞,順便還要教你一個很重要的網路附帶資訊的方式: ,
最後還要教會你一點簡單的視覺化!

7.2 複習

還記得我們討論過網址的四個部分吧?
https://www.google.com.tw/search?q=apple&oq=apple&aqs=chrome.
.69i57j69i61j69i65l3j69i60.1283j0j4&sourceid=chrome&ie=UTF-8
上面這個網址是我在 Google 搜尋 Apple 這字眼得到的網址
❤ https: 使用語言
❤ www.google.com.tw: 服務台地址
❤ search: 問題
❤ q=apple&op=apple: 附加資訊
但你有沒有想過,會不會有一些資訊是不能放在檯面上的?譬如如果是一個登入的問題,我們
可以把我們的『帳號』和『密碼』放在網址上嗎?很明顯是不行的!但是也不能不傳,不然對面的
服務台也不知道你到底是不是一個正確的使用者!那怎麼辦呢?簡單,傳個『秘密紙條』過去就可
以了!

7.3 網址抓取兩步驟

首先我們先看看狂新聞的網址 https://crazy.ck101.com/category/1 我選的是上方的


9487 的項目
7 狂新聞下載 90

圖: 目標頁面

7.3.1 步驟 1: 查看原始碼

查看原始碼的時候,我們可以看到第一頁的條目都在上面,圖我稍微減少了一些額外的東西,
只留取新聞和更多的部分

圖: 右鍵 - 查看原始碼

但問題是第二頁以後呢?在你按下『更多』的按鈕的時候,你會發現,並沒有重整頁面,所以
我在步驟 1 得到的結論就是,我沒辦法通過『網址列』來拿到所有每一頁的新聞,我只能拿到第一
頁的新聞!

7.3.2 步驟 2: 找到真正的問題

一樣跟我們之前一樣打開 Network 並且把上面的標籤固定在 xhr,你會發現其實每當你點擊一


次『更多』,就會送出一次 more 的問題,而且回應恰恰巧就是更多的新聞
7 狂新聞下載 91

圖: 開發人員工具 - Network

但我們發現一個問題,為何每個 more 的網址都一樣,我們之前不是說一定要把必要的資訊給


服務台,服務台才知道要給你什麼嗎?請你拉到 header 的最下面

圖: Header 的最下面

沒錯,事實上他是有帶入的,只不過是隱藏在下面,利用 id(9487 固定 1) 和 page 來決定到底


是哪一頁,這種隱藏起來不讓別人看到附加資訊的方式我們叫做『POST』的夾帶方式,通常使用
在敏感資訊,但偶爾我們也會從普通網頁看到這種方式 (e.g. 我們的狂新聞)
7 狂新聞下載 92

7.3.3 步驟 3: GET & POST

沒錯,最後一個步驟,就是觀察附加資訊的方式:
❤ GET: 所有資訊都在網址上,最常出現,我們之前的例子都屬於 GET 例如 Google 查詢

圖: GET 的方式

❤ POST: 只要有任何隱藏的資訊,我們就稱為 POST 的附加方式,我們送出 POST 的 Python


語法如下

# 使用第三方的 requests 函式庫會比較方便一點


import requests
# 用一個字典來包裝所有要送出的額外資訊
post_data = {'id':'1', 'page':'第幾頁'}
# 指定 data 參數
r = requests.post('https://crazy.ck101.com/category/more', data=post_data)

[程式]: import requests


import json
post_data = {'id':'1', 'page':'2'}
r = requests.post('https://crazy.ck101.com/category/more', data=post_data)
# 這裡一樣用 text 得到內容, 讀者可以自行把註解去掉, 秀出內容
# 使用 json.loads, 因為我們如果用 requests 函式庫的話, 得到的是已經 read 過後的內容
json.loads(r.text)

[輸出]: [{'author_name': '專打腦殘',


'crazy_rating': 2,
'create_time': '2018-11-19',
'id': 13987,
'thumbnail': '//crazypic.ck101.com/c/f/cff72afeb46f06e1fea3c6bee4f5bf18.jpg',
'title': '選舉一路造假!直播辯論公然扯謊 遭大學院長回覆打臉',
'view': 513},
{'author_name': 'Bing-JunWu',
'crazy_rating': 3,
7 狂新聞下載 93

'create_time': '2018-11-19',
'id': 13986,
'thumbnail': '//crazypic.ck101.com/b/0/b053d749c173b97b161d161f93147461.jpg',
'title': '挑戰在台中的大學售出百罐蜂蜜檸檬!正妹的反應出乎意料?!',
'view': 319},
{'author_name': '專打腦殘',
'crazy_rating': 4,
'create_time': '2018-11-19',
'id': 13984,
'thumbnail': '//crazypic.ck101.com/e/c/ec7761a377831ba55554c72f8ad2335d.jpg',
'title': '選前奧步再現!寧願繳 50 萬罰金也要觸法提民調',
'view': 458},
{'author_name': '專打腦殘',
'crazy_rating': 5,
'create_time': '2018-11-19',
'id': 13983,
'thumbnail': '//crazypic.ck101.com/2/4/2434200761856884aca8d2e62477ed55.jpg',
'title': '生為中國人的悲哀!主場馬拉松被硬塞國旗失冠軍',
'view': 543},
{'author_name': '專打腦殘',
'crazy_rating': 3,
'create_time': '2018-11-17',
'id': 13979,
'thumbnail': '//crazypic.ck101.com/0/8/08fdb95f17edf7ec659000f28a3aa003.jpg',
'title': '國民黨輔選韓國瑜 惡意攻擊陳菊「肥滋滋大母豬」',
'view': 594},
{'author_name': 'ssaa794613',
'crazy_rating': -1,
'create_time': '2018-11-17',
'id': 13978,
'thumbnail': '//s3.imgs.cc/img/ZC6kdml.png',
'title': '批評別人的競選經費反遭自己被打臉???',
'view': 416},
{'author_name': 'ssaa794613',
'crazy_rating': 0,
'create_time': '2018-11-17',
'id': 13976,
'thumbnail': '//crazypic.ck101.com/8/7/8781cf156f83c05925aa85daa557772d.jpg',
'title': '候選人政見太狂 單身網友嗨爆',
'view': 603},
{'author_name': 'ssaa794613',
'crazy_rating': 0,
'create_time': '2018-11-17',
'id': 13975,
'thumbnail': '//crazypic.ck101.com/d/9/d942bfb0d6f05554903a976add123a24.jpg',
7 狂新聞下載 94

'title': '來亂 作戲 作秀的游藝參選候選人???',


'view': 481},
{'author_name': '專打腦殘',
'crazy_rating': 1,
'create_time': '2018-11-14',
'id': 13970,
'thumbnail': '//crazypic.ck101.com/b/0/b083336c49264f8b44b921963360b523.jpg',
'title': '不敢接受辯論的西瓜市!最浪費選民時間的政見發表會',
'view': 562},
{'author_name': '專打腦殘',
'crazy_rating': -1,
'create_time': '2018-11-13',
'id': 13966,
'thumbnail': '//crazypic.ck101.com/6/3/6322832f1bf24d987c126e8143a5e304.jpg',
'title': '國民黨又出新招!先告狀指稱「別人幫我賄選」',
'view': 434}]

7.3.4 得到新聞內容

回頭看一下 Network 的部分,我們發現回應是以 JSON 的方式存在,並且 id 其實就可以幫你


湊出新聞的網址

圖: JSON 內容

[程式]: import requests


import json
# 準備 pandas 來儲存表格
import pandas as pd
post_data = {'id':'1', 'page':'2'}
r = requests.post('https://crazy.ck101.com/category/more', data=post_data)
# 我準備儲存標題. 時間和內容三大項目
df = pd.DataFrame(columns=["標題", "時間", "新聞網址"])
7 狂新聞下載 95

for news in json.loads(r.text):


# news 是我們的單一則新聞, 以字典表示, 利用 [key] 拿到對應內容
url = "https://crazy.ck101.com/post/" + str(news["id"])
# 跟我們之前 tabelog 儲存表格一模一樣
s = pd.Series([news["title"], news["create_time"], url],
index=["標題", "時間", "新聞網址"])
df = df.append(s, ignore_index=True)
df.to_csv("news.csv", index=False, encoding="utf-8")

我們已經正確儲存成一個表格了,一起來看看這個表格

圖: 一頁的儲存

7.3.5 多頁儲存和文字雲

我們順便來做個多頁的儲存和一個有趣的資料視覺化:『文字雲』吧,文字雲就是把重要或者
多數的文字用比較大的字型凸顯,這也是現在人蠻喜歡使用的一種視覺方式,畢竟對於出現幾次之
類的計數,一般的時候我們並沒有那麼在意,我們比較在意的反而會是對於這個主題,哪些東西會
是主要的重點!
首先我們先把 jieba 函式庫安裝好,並且直接利用 urlretrieve 把他的較大字典讀取來完成比較
精準的分詞 (如果對於 jieba 不熟可以翻閱我的基本語法一章或者參閱 github 上說明文件)

[程式]: # 內建函式庫, 可以幫我們下載檔案


from urllib.request import urlretrieve
# MAC 電腦要特別加入這兩行
# 因為 MAC 電腦 +python 會有個 bug, 把對面的 ssl 憑證視為無效
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
url = "https://raw.githubusercontent.com/fxsjy/jieba/master/extra_dict/dict.txt.big"
# 下載到同一個資料夾的 bigdict.txt, 你可以在你的 Pycharm 看到檔案
urlretrieve(url, "bigdict.txt")
7 狂新聞下載 96

import jieba
# 讀入額外的字典
jieba.load_userdict("bigdict.txt")

Building prefix dict from the default dictionary …


Loading model from cache /var/folders/wv/6kw6f9sx5mqdrjl6820mgzfm0000gn/T/jieba.cache
Loading model cost 1.765 seconds.
Prefix dict has been built succesfully.

接下來我們除了儲存成 csv 以外,還順便將我們等等要用 wordcloud 函式庫處理的文字準備


好,通常如果我們要使用這些 Python 函式庫,我們必須將『中文處理得像是英文一樣』
,什麼是將
中文處理得像是英文呢?簡單來說就是

1. 把詞分開

2. 加上空白組合回去

以一個例子來說,『我喜歡你』是正常的中文,『我喜歡你』是把中文處理的像是英文

[程式]: import requests


import json
import pandas as pd
# 由於畫出文字雲需要把所有文字整理成一篇文章, 於是我們準備一個容器來儲存
fulltext = ""
for i in range(50):
# 多頁處理, 我們得到一下 50 頁的新聞
post_data = {'id':'1', 'page':str(i + 1)}
r = requests.post('https://crazy.ck101.com/category/more', data=post_data)
df = pd.DataFrame(columns=["標題", "時間", "新聞網址"])
for news in json.loads(r.text):
url = "https://crazy.ck101.com/post/" + str(news["id"])
s = pd.Series([news["title"], news["create_time"], url],
index=["標題", "時間", "新聞網址"])
df = df.append(s, ignore_index=True)
# 在使用 wordcloud 函式庫的時候, 必須將中文處理得像英文一樣
# 像英文 (詞與詞之間必須有空白) 的例子: 我 喜歡 你
# 兩篇文章接起來之間會少一個空白, 於是我們加了一個空白上去
fulltext = fulltext + " " + " ".join(jieba.cut(news["title"]))
df.to_csv("news.csv", index=False, encoding="utf-8")

請安裝 wordcloud 函式庫 (如果是 32-bit 的 Python 請使用 whl 安裝) 和 Pillow 函式庫 (直接
安裝即可),並且準備一個有輪廓的圖來做文字雲,我準備的圖在這裡 https://drive.google.
com/open?id=1VtHL7e5wv2FBxrkayodGZNn4ag2LTeem
7 狂新聞下載 97

圖: 我準備的文字雲圖

記得圖片要純白背景,有時候無法產生是因為背景並不是真正的純白!最後由於 Python
函式庫大部分沒有原生支援中文字型,所以請讀者到 https://noto-website-2.storage.
googleapis.com/pkgs/NotoSansCJKtc-hinted.zip 下載 Google 提供的中文字型,並且
選擇一個你喜歡的粗細放入同一層資料夾,我選擇的是 Bold 的粗細

[程式]: ##### from os import path


from PIL import Image
import numpy as np
from wordcloud import WordCloud, ImageColorGenerator
7 狂新聞下載 98

import jieba
import matplotlib.pyplot as plt
%matplotlib inline
mask_path = "map.jpg"
# 先把我們要做文字雲的圖片開起來
mask = np.array(Image.open(mask_path))
# 準備 WordCloud
# 由於原生不支援中文字型, 請下載後選擇粗細放入同一層資料夾
# https://noto-website-2.storage.googleapis.com/pkgs/NotoSansCJKtc-hinted.zip
# max_words: 構成雲的最大詞數, 順帶一提, wordcloud 處理中文的時候會只剩兩個字以上組成的詞
# mask: 剛剛準備的圖
# collocations: 要不要把兩個詞組合成一個有意義短語, 通常用於英文, 中文不需要
wc = WordCloud(font_path = "./NotoSansCJKtc-Bold.otf",
background_color="white", max_words=5000,
mask=mask, collocations=False)
wc.generate(fulltext)

# 剛才不足的地方 -> 色彩不對


# 我把原圖的色彩擷取出來, 而且用來上色 wc(文字雲)
color = ImageColorGenerator(mask)
wc.recolor(color_func = color)
# 儲存檔案, 你可在你的 project 看到這個檔案
wc.to_file("./result_cloud.png")
# (Jupyter Only) 你可以在 Notebook 上直接秀出
wc.to_image()

Out[10]:
7 狂新聞下載 99

你可以看到最近狂新聞最火紅的幾個單字,『台大』,『台灣』,『館長』等等,順便你也發現其
實有些詞語是切得不準確的,例如:
『柯文哲』這個詞沒有被切出來,如果你需要更精確的分詞,請
7 狂新聞下載 100

參考基礎語法篇或者 jieba 說明文件的『讀入自定義詞典』的部分

7.3.6 結語

在這個練習裡,我們學到了 POST 的處理方式,並且把我們的的結果儲存成為一個文字雲,請


讀者一定要好好練習我們上面如何找出『真正問題』的整個步驟流程!
8 YOUTUBE 下載 101

8 Youtube 下載

8.1 介紹

這章我們要教你的主要事情,不是影片下載,影片的下載只是次要的事,我們要教你的是如何
處理更複雜的 POST(e.g. 登入) 以及如何在爬蟲的時候維持著登入狀態!

8.2 目標

登入自己的 youtube 帳號情況下,把所有你創建的『播放清單』的影片下載下來

圖: 儲存到播放清單的方式

8.3 Youtube 登入方式

如果用我們之前使用的方式 (Network 來觀察真正問題),你會發現很難找到真正的問題,就算


找到了,整個過程也非常非常複雜,那這時候我們該怎麼辦呢?
8 YOUTUBE 下載 102

圖: Youtube 登入的導向

我們之所以把這個章節放在最後,是因為這招是『最後的手段』!如果依照之前的方式可以找
到真正的問題,我會推薦你使用之前的方式 (原始碼和 Network),但如果遇到下面兩個情況:

1. 對應問題是很複雜的 POST(有興趣的讀者可以在讀完章節試試看 Facebook 登入),由於很多


附加資訊都無法知道正確意思,所以我們不會希望自己填入!
2. 需要多複雜步驟的操作,就如我們現在 Youtube 登入一樣的情況

是的,讀者會發現,登入會是一個較為複雜的 POST 步驟,所以建議各位如果在處理登入問


題,如果沒有好的方法!我就會建議你使用最後一招,也是最原始的方式,『教電腦使用瀏覽器跟
你做一樣的操作』
!如果說我們之前都是很確切理解了『真正問題』
,對服務台遞出『真正問題』
,現
在這招就好像是:『讓下一個人 (電腦) 完全模仿上一個人 (我) 的所有操作』,暴力直接!
8 YOUTUBE 下載 103

8.4 Python 操縱瀏覽器

要讓 Python 操縱瀏覽器,你需要一些工具,我們先來看個完整工具圖

圖: 完整工具圖

要完成這個自動操縱,我們需要下面三個工具:

1. 瀏覽器:各位的電腦應該都有上網的瀏覽器!這裡推薦使用 Chrome 作為我們操作的工具!


2. 自動操縱介面:這介面是無關乎你使用的程式語言的,這介面的用途是讓你可以用『指令介面
(程式介面)』直接操縱瀏覽器,請到 http://chromedriver.chromium.org/downloads
下載最新版本即可
3. 程式語言操縱介面:最後我們還是要寫程式來操縱這個自動操縱介面,所以我們需要各個語
言的函式庫,這裡我們會選用 Selenium 函式庫 (請讀者利用 PyCharm 或者 pip 直接安裝),
各個程式語言都有自己的 Selenium 函式庫

所以整個流程就是:
『特定程式語言函式庫』操縱『通用的自動操縱介面』再去操縱『瀏覽器』

8.5 開始操縱

首 先 先 從 http://chromedriver.chromium.org/downloads 下 載 最 新 版
chromedriver,建議可以順便將 Chrome 更新 (右上角三個點 - 說明),並且解壓縮後放入專
案裡
8 YOUTUBE 下載 104

圖: 下載對應作業系統並且放入同一層資料夾

8.5.1 Selenium 操縱方式

操 縱 方 式 跟 BeautifulSoup 很 像, 但 find 被 find_element(單 數) 取 代,find_all 被


find_elements(複數),並且 Selenium 提供我們常用的篩選機制,我們可以使用 find_element_by_id
和 find_element_by_class_name 來直接對 id 或者 class 屬性進行篩選,請一樣開啟開發人員工具,
並且使用『選取元素工具』來選擇
8 YOUTUBE 下載 105

圖: 帳號的 HTML 元素表示

按下下一步的密碼頁面
8 YOUTUBE 下載 106

圖: 密碼的 HTML 元素表示

[程式]: # 等等要對接的是 Chrome driver, 如果是別的就要選擇別的


from selenium.webdriver import Chrome
import time
# 這裡路徑表示比較特別, ./這個同一層的表示一定要加上, 不然函式庫會去 $PATH 找
driver = Chrome("./chromedriver")
# youtube 登入後的 playlist 的網址
driver.get("https://www.youtube.com/view_all_playlists")
# send_keys 就是平常我們鍵盤輸入
driver.find_element_by_id("identifierId").send_keys("改成你的 Google 帳號")
8 YOUTUBE 下載 107

# click 就是點擊
driver.find_element_by_id("identifierNext").click()
# 這裡要注意一下, 因為事實上頁面不是馬上就能得到
# 我們要連等待這個動作都要模擬, 於是我通常會在這裡等待 3 秒, 讓頁面完整載入
# 要記得要 import time 這內建函式庫 (不用安裝)
time.sleep(3)

# 輸入密碼
driver.find_element_by_class_name("whsOnd").send_keys("改成你的 Google 密碼")
driver.find_element_by_id("passwordNext").click()
# 這裡我多等了一下, 等 5 秒登入並且 youtube 載入
time.sleep(5)
# 這裡我們拿到我們之前跟各位介紹過的 cookies, 要做什麼用呢? 稍微保密一下
# cookies: 瀏覽網站自動送出的附加資訊
cookies_list = driver.get_cookies()
# 順手把瀏覽器關掉
driver.close()

你應該可以看到以下的畫面

圖: 自動被喚起的 Chrome

你可以看到上面有『受到自動軟體控制』的畫面!那為何我們停止了呢?說好的抓播放清單呢?
8 YOUTUBE 下載 108

其實到這裡就可以了,事實上為何我們可以在每個頁面都保持著登入呢?我們並不是每看一個頁面
都會輸入登入訊息啊,為何同一個網址可以讓每個人保持著不同的登入狀態呢?沒錯,事實上,你
的登入訊息是有寄送給伺服器的,不過是記載在 cookie 裡,所以你只要把 cookie 記錄下來,在以
後使用網址的時候把 cookie 一併送出就可以了,如果忘記的同學可以回頭看一下 ptt 那章節的『八
,這裡之所以只使用 selenium 處理登入是因為後續如果還要靠點擊的來處理剩下的也太
卦版登入』
複雜了,所以我們改使用 requests 模組!

圖: 接下來的尋找

直接開始寫程式吧!

[程式]: # 為了版面整潔, 我把 BeautifulSoup 的紅 warning 去掉


import warnings
warnings.filterwarnings('ignore')
import requests
from bs4 import BeautifulSoup
# 這裡比較特別, 我們以前是加入一個 jar, 不過每次要加太煩了
# 所以我使用了 Session, Session 就是一次連線
# 使用了 Session 就跟你開著瀏覽器一樣, 在這次連線都會自動使用同樣的 cookie
s = requests.Session()
# cookies_list 是一個 list 包字典, 我們把 cookie 名字和值拿出來設定進去 Session
for cookie in cookies_list:
s.cookies.set(cookie['name'], cookie['value'])
# 這時候造訪就會自動帶入 Cookie
response = s.get("https://www.youtube.com/view_all_playlists")
html = BeautifulSoup(response.text)
# 依照上面的圖先找到 li
playlists = html.find_all("li", class_="playlist-item")
# 先把每個 playlist 網址存起來
urllist = []
8 YOUTUBE 下載 109

for l in playlists:
# 找到 a
title = l.find("a", "vm-video-title-text")
# 拿出 href 屬性 (超連結網址)
url = "https://www.youtube.com" + l.find("a", "vm-video-title-text")["href"]
# 把 title 和 url 準備成一個字典, 並且加入我們的 urllist 裡
urllist.append({"title":title.text, "url":url})
print(urllist)

[{'title': '五月天', 'url':


'https://www.youtube.com/playlist?list=PLcOWRIQvgym11nUFkiRKiGs8wEEDR7Q5o'}, {'title':
'林俊傑', 'url':
'https://www.youtube.com/playlist?list=PLcOWRIQvgym2lZ0gRV2KsHonC2bdPMbfg'}, {'title':
'林宥嘉', 'url':
'https://www.youtube.com/playlist?list=PLcOWRIQvgym17QjtFmFeGsqPBxFBFkdAK'}]

8.5.2 Playlist 下載

這裡我們使用 pytube 這函式庫來下載整個 Playlist,官方說明文件在此 https://github.


com/nficano/pytube,這裡要注意一下,如果直接使用 pip 或者 PyCharm 下載,下載到的 9.3.5
版會有問題,所以請按照一開始的附錄,直接去 github 下載最新版本

[程式]: # 如果你是 jupyter notebook 用戶, 可以直接使用此安裝


#!pip install git+https://github.com/nficano/pytube.git
# MAC 要加入下兩行, 不然 SSL 憑證會被視為無效
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
from pytube import *
import os
for url in urllist:
# 直接把 playlist 網址給出
# suppress_exception 不設定 True 會等於 False(預設值), 被刪除的影片會出現錯誤, 停下程式
pl = Playlist(url["url"], suppress_exception=True)
# 我會把 title 當成資料夾, 需要先創好 yt-dl 資料夾
path = "yt-dl/" + url["title"]
if not os.path.exists(path):
os.mkdir(path)
# 直接 download 所有, 會幫你選擇最高畫質
pl.download_all(path)
8 YOUTUBE 下載 110

圖: 我們下載的影片

你可以看到我們完美的下載完了

8.6 結語

這章節的重點是教你如何使用 Selenium 和把登入的 Cookies 留下來造訪網站,這樣就可以解


決大部分『需要登入的網站』和『複雜的網站』,如果某網站對於讀者太為複雜或者使用這種方式
較好拿到資訊,就請拿出最後這招:『使用 Selenium 暴力破解』!

You might also like