# 社群網路爬蟲實作課
---
#### 講者:資工三 江尚軒
---
## What is 爬蟲?
![](https://i.imgur.com/9LFv0kB.png =40%x) ![](https://i.imgur.com/IMGKK6l.jpg =25%x)
> 爬蟲(web crawler) - 是一種自動化瀏覽網頁的機器人
---
## Why 爬蟲?
![](https://i.imgur.com/CNk6LGS.png =45%x)
<!-- 有時資料量可是幾百萬筆起跳的,手動抓會抓到發瘋 -->
---
## Who 爬蟲?
* 訓練機器學習之人員
* 股票分析人員
* 市場分析人員
---
## How 爬蟲?
---
## 1. 首先,我們還要知道網頁長什麼樣子。
---
#### HTML簡介
----
### HTML(Hypertext Markup Language)
HTML 就像網頁的骨架,決定網頁的組織架構以及呈現內容。
HTML 包含了一系列的元素(elements),而元素包含了標籤(tags)與內容(content)。
----
### 元素組成
![](https://i.imgur.com/x7X1TRF.png)
1. Opening tag: 起始標籤,<>內放入標籤名稱。
2. Closing tag: 結尾標籤,<>內放入和起始標籤一致的標籤名稱,而且要在名稱前加上/符號。
3. Content: 標籤內容。
----
### 屬性
![](https://i.imgur.com/9IBJo4j.png)
屬性可以為元素提供更多資訊,ex: 字型、顏色等等...。
格式為: `屬性名稱 = 屬性值`
而class是將標籤分類(就想成放在同一個箱子裡),之後爬蟲經常會用到。
----
### HTML架構
```htmlmixed=
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>my first html</title>
</head>
<body>
<p>Hello, HTML!</p>
</body>
</html>
```
1. `<head>`: 標頭,包含網頁的基本設置,不會顯示在畫面上,ex: 網頁標題、字元實體集...。
2. `<body>`: 包含了所有會顯示於網頁瀏覽者眼前的內容,ex: 段落、圖片...。
----
### 看起來...
![](https://i.imgur.com/W5JKock.jpg)
----
### 基本元素介紹
#### `<h1>~<h6>`
標題,一共有六層,字體從大到小,通常用於文章標題與副標題。
```htmlmixed=
<h1>This is heading1</h1>
<h2>This is heading2</h2>
<h3>This is heading3</h3>
<h4>This is heading4</h4>
<h5>This is heading5</h5>
<h6>This is heading6</h6>
```
----
<h1>This is heading1</h1>
<h2>This is heading2</h2>
<h3>This is heading3</h3>
<h4>This is heading4</h4>
<h5>This is heading5</h5>
<h6>This is heading6</h6>
----
#### `<p>`
段落,通常用於文字內容。
```htmlmixed=
<p>
超文本標記語言(英語:HyperText Markup Language,
簡稱:HTML)是一種用於建立網頁的標準標記語言。
HTML是一種基礎技術,常與CSS、JavaScript一起被眾多網站
用於設計網頁、網頁應用程式以及行動應用程式的使用者介面[3]。
網頁瀏覽器可以讀取HTML檔案,並將其彩現成視覺化網頁。
HTML描述了一個網站的結構語意隨著線索的呈現,
使之成為一種標記語言而非程式語言。
</p>
```
----
<p>
超文本標記語言(英語:HyperText Markup Language,
簡稱:HTML)是一種用於建立網頁的標準標記語言。
HTML是一種基礎技術,常與CSS、JavaScript一起被眾多網站
用於設計網頁、網頁應用程式以及行動應用程式的使用者介面[3]。
網頁瀏覽器可以讀取HTML檔案,並將其彩現成視覺化網頁。
HTML描述了一個網站的結構語意隨著線索的呈現,
使之成為一種標記語言而非程式語言。
</p>
----
#### `<a>`
超連結,用於連結其他網址,href屬性加上連結網址。
```htmlmixed=
<a href="https://www.nchu.edu.tw/index1.php">中興大學首頁</a>
```
----
<a href="https://www.nchu.edu.tw/index1.php">中興大學首頁</a>
----
#### `<div>`
區塊,就是一個空的容器,可以在區塊中加入其他元素。
```htmlembedded=
<div style="background-color:blue; color:white; width:400px; height:200px;">
<p>inside the div</p>
</div>
```
----
<div style="background-color:blue; color:white; width:400px; height:200px;">
<p>inside the div</p>
</div>
----
#### `<span>`
文字的區塊,包住文字並提供更多資訊。
```htmlembedded=
<p>this color is <span style="color: yellow;">yellow</span></p>
```
----
<p>this color is <span style="color: yellow;">yellow</span></p>
<!-- last page of ch1 -->
---
## 2. 再來,我們得先知道網頁的運作方式。
---
#### request和response的機制
----
![](https://i.imgur.com/KbjaM94.png =80%x)
我們就是用戶端(Client),當我們點一個網頁時,就等於向存放該網頁的伺服器(Server)送出請求(Request),當伺服器接收到我們的請求後,就會回傳網頁的內容回來,這就是回應(Response)。
----
### 感覺很複雜嗎?
其實網路運作方式就跟去餐廳點餐的流程非常相似,我自己當作用戶端(Client),餐廳老闆當作伺服器(Server)。
![](https://i.imgur.com/jsvqtfX.png)
----
當我們進到一間餐廳看著菜單,就像在瀏覽器上瀏覽眾多的網頁。
接著,到櫃檯向老闆點餐,就像是看到喜歡的網頁,發出請求(Request)給伺服端(Server)想看的網頁。
最後,等待老闆烹飪完成送到餐桌上,像是伺服端(Server)端將回應(Response)回傳給用戶端(Client)。
<!-- last page of ch2 -->
---
## 3. OK,那可以開始爬蟲了吧?
---
#### requests和BeautifulSoup套件介紹
----
#### 本文以[PTT八卦版](https://www.ptt.cc/bbs/Gossiping/index.html)為例,教會你如何爬蟲。
----
### Python - requests
此次爬蟲所使用的程式語言為Python,是因為Python提供了許多好用的爬蟲套件,requests就是其中之一。
----
### requests是蝦米??
requests是一個幫助你抓下網頁的HTML原始碼的套件,抓下原始碼後才能對網頁進行分析。
----
### 下載requests
```
// CMD
pip install requests
```
### 引用requests
```python=
import requests
```
----
### requests抓取網頁HTML原始碼
抓取網站使用到 requests.get() 函數,參數為我們想要爬蟲的網址(URL)
```python=
response = requests.get("https://www.ptt.cc/bbs/Gossiping/index.html")
```
----
### 你以為抓下來就可以直接用了嗎,並沒有!
抓下來的還只是最原始的HTML,需要透過BeautifulSoup解析。
----
### BeautifulSoup又是蝦米??
BeautifulSoup是一個解析HTML原始碼並且提供搜尋節點功能的套件。
![](https://i.imgur.com/eNL70dh.png =30%x)
----
### 下載BeautifulSoup
```
// CMD
pip install beautifulsoup4
```
### 引用BeautifulSoup
```python=
from bs4 import BeautifulSoup
```
----
### BeautifulSoup解析網頁HTML原始碼
```python=
response = requests.get("https://www.ptt.cc/bbs/Gossiping/index.html")
soup = BeautifulSoup(response.text, "html.parser")
print(soup.prettify())
```
----
### 結果...
![](https://i.imgur.com/ZhUlHqz.png)
<!-- last page of ch3 -->
---
## 4. 你騙我! 你不是說這樣就可以抓到HTML了嗎?
---
#### cookie介紹
----
### 我們來找找看原因
第一次進入PPT八卦板時,會先看到這個畫面:
![](https://i.imgur.com/8HOiGFG.png)
點 "我同意" 之後才會看到文章列表。
但第二次再進去時,就不會再詢問一次是否滿18歲了,這是為什麼呢?
----
我們觀察 **F12>>Application>>Cookies**
![](https://i.imgur.com/LiVKv1b.jpg)
----
然後當我按下 "我同意" 時,cookie會多出一個 over18=1,這就是PTT存在瀏覽器中,記錄我是否點過滿18的餅乾。
![](https://i.imgur.com/Dz53JA8.jpg)
----
再來觀察 **Network>>index.html>>Headers>>cookie**,會發現PTT就是把cookie包裝在Headers中發送出去的。
![](https://i.imgur.com/Fx7fEhw.jpg =90%x)
----
也就是說,我們只要讓程式模擬使用者,發送一個一樣的cookie,就能順利抓到文章列表了!
```python=
my_headers = {"cookie": "over18=1"}
url = "https://www.ptt.cc/bbs/Gossiping/index.html"
response = requests.get(url, headers=my_headers)
soup = BeautifulSoup(response.text, "html.parser")
print(soup.prettify())
```
----
### 結果...
![](https://i.imgur.com/b3rZimq.png)
![](https://i.imgur.com/9elgmgr.png)
![](https://i.imgur.com/dtS8Mfr.png)
![](https://i.imgur.com/mHBXwxx.png)
----
其實可以將cookie想成暫存在用戶端(client)這邊的便利貼,上面記錄了你之前的所做過的事情,之後在進相同的網頁時,就會連著cookie帶給伺服器,好讓伺服器知道你做過什麼事。
<!-- last page of ch4 -->
---
## 5. 看起來不錯,但好像少了些什麼?
---
#### session介紹
----
繼續做下去你可能會發現,當每次向伺服器送出請求(Request)時,都必須補上cookie,伺服器才會正確的傳回網頁,這是為什麼呢?
----
在網頁的世界裡,它是一個「無狀態」的協議。什麼意思呢?那就是每一個 request 都是一個「獨立的」request,彼此之間不會有任何關聯。所以 Server 那邊也不會保存任何狀態。每一個 request 都是一個新的 request。
----
你可以把伺服器想成是一個喪失記憶能力的人,每一次你去找他的時候,他都當作是第一次見到你,完全忘記你以前有去找他了。
就會發生...
----
![](https://i.imgur.com/LUhdhfA.png =40%x)![](https://i.imgur.com/4FGfF0U.png =40%x)
----
### 我懂了,但每次都要傳cookie我覺得好麻煩,有沒有更好的做法?
----
### 有的,那就是session~
----
### session是什麼?
session的英文意思是 *持續一段時間的狀態*,基本上和它的運作方式並沒有差太多,此機制確保了伺服器可以知道使用者的狀態。
----
想像伺服器留下一張便利貼,上面留者你的一切所作所為,並給你一個密碼,那當你下次拿著這個密碼來找伺服器時,伺服器就會對應到那張便條紙,就可以知道你之前做了哪些事了。
簡單來說,你可以把session當作是存在伺服器端(server)的cookie。
----
### session該怎麼用?
requests有內建session()函數,會暫存使用者的資料。
```python=
rs = requests.session()
```
----
### 跟cookie有點不一樣
前面我們是讓程式將cookie夾帶在headers中傳送給伺服器,但缺點是每次request都必須補上cookie,相當麻煩。
所以我們希望==程式可以模擬的像是真人==,回想一下,我們剛剛進八卦版做了哪些事情?
----
1. 先進入 "詢問你是否滿18歲" 的畫面
2. 然後你按了 "我同意"
3. 瀏覽器存下 "over18的cookie"
4. 進入文章列表
----
### 那麼,我們怎麼讓程式做一樣的事呢?
```python=
payload = {
'from': '/bbs/Gossiping/index.html',
'yes': 'yes'
}
# 用session紀錄此次使用的cookie
rs = requests.session()
response = rs.post("https://www.ptt.cc/ask/over18", data=payload)
```
----
### post vs. get
* get: 將資料全部寫在header上,就像你寫明信片一樣,傳遞上較不安全。
* post: 將資料寫在內部,就像你寫信然後裝進信封袋一樣,傳遞上比較安全且傳遞的資訊可以比較多。
----
### post 使用 data 參數傳遞資料
查看 **Network>>over18>>Headers>>Form Data**
我們看到了這兩個參數,不難猜測代表的意思。
* from: 代表我們進入的是八卦版。
* yes: 代表按了同意。
![](https://i.imgur.com/WhaxWRR.png =100%x)
----
### 看看是不是成功了
```python=
response = rs.get("https://www.ptt.cc/bbs/Gossiping/index.html")
soup = BeautifulSoup(response.text, "html.parser")
print(soup.prettify())
```
結果會和上一個一樣,不過你會發現如果想要再進入一次八卦版的話,就不需要再傳cookie了。
----
### 好session,不用嗎?
![](https://i.imgur.com/EnQ9D9k.png =60%x)
----
### 爬蟲重要觀念
### ==讓程式模擬真人的行為==
因為爬蟲會造成伺服器的負擔,所以大部分網站都不歡迎別人來爬他們的網站,因此多數網站都會設下許多障礙阻止爬蟲。
想當然,魔高一尺道高一丈,身為一個爬蟲工程師,就要想盡辦法==讓你的程式看起來像是真人==,以騙過伺服器的眼睛。
<!-- last page of ch5 -->
---
## 6. 抓到了沒錯,可是,我們要的東西在哪裡?
---
#### BeautifulSoup分析HTML+搜尋節點
----
### 先別急,我們先來觀察一下八卦版
----
文章標題按右鍵>>檢查
![](https://i.imgur.com/bXeeuK6.png)
----
會出現文章標題在HTML上的位置
![](https://i.imgur.com/9ezbUoD.png)
----
多抓幾篇會發現,所有的標題都放在一個
==class="title" 的 div 中==
![](https://i.imgur.com/5em1ZQ5.png)
![](https://i.imgur.com/n11aRUl.png)
----
### BeautifulSoup搜尋節點
* find_all("標籤名稱", 屬性="值"):
```python=
links = soup.find_all("div", class_="title")
for link in links:
print(link)
```
找到所有 class 名稱為 "title" 的 "div" ,
回傳一個列表。
----
### 結果...
![](https://i.imgur.com/YpT2ak2.png)
<!-- last page of ch6 -->
---
## 7. 我們要什麼? 我們要連結!
---
#### 抓出每篇文章的連結
----
因為我們想要文章的詳細資料,因此我們必須進到文章內才能抓到。
**那麼,要怎麼抓出文章聯結呢?**
----
### 我們先看一下剛剛抓到的東西
```
<div class="title">
<a href="/bbs/Gossiping/M.1615705592.A.DEE.html">[問卦] 高雄最美的景點是什麼?</a>
</div>
```
會發現我們要的連結是在 ==`<a>`中的"href"屬性==。
```python=
links = soup.find_all("div", class_="title")
for link in links:
print(link.a["href"])
```
----
### 結果
![](https://i.imgur.com/OzRqsWs.png =80%x)
----
但這並不是完整的網址,所以我們要在前面加上:
"https://www.ptt.cc/"
----
### 結果
![](https://i.imgur.com/RgHtTQ3.png =80%x)
----
還記得requests的get函數嗎?
接下來就用此函數進到文章內頁。
```python=
page_url = "https://www.ptt.cc/"+link.a["href"]
# 進入文章頁面
response = rs.get(page_url)
result = BeautifulSoup(response.text, "html.parser")
print(result.prettify())
```
----
### 結果
![](https://i.imgur.com/cHBl3rd.png =45%x)
<!-- last page of ch7 -->
---
## 8. 一切都準備好啦!
---
#### 抓出文章的作者、標題、發文時間
----
檢查文章作者、標題、發文時間的位置
**右鍵>>檢查**
![](https://i.imgur.com/6Hw1wii.png)
----
發現都會在
==class="main-content" 的 div 中==
![](https://i.imgur.com/l5oB6is.png)
----
再往下找到
==class="article-meta-value" 的 span 中==
```python=
# 找出作者、標題、時間、留言
main_content = result.find("div", id="main-content")
article_info = main_content.find_all("span",
class_="article-meta-value")
```
----
因為會用list的方式裝下來,就用索引值取我們要的資料吧~
```python=
if len(article_info) != 0:
author = article_info[0].string # 作者
title = article_info[2].string # 標題
time = article_info[3].string # 時間
else:
author = "無" # 作者
title = "無" # 標題
time = "無" # 時間
```
很小機率會出現沒有 "article-metaline" 等資訊還是要特殊處理 QAQ
---
## 9. 都是一樣抓東西,應該很簡單吧~(Flag
---
#### 抓出文章內容
----
### 到文章內容>>右鍵>>檢查
----
結果內容不存在任何類別內。只是單純的丟到 "main-content" 而已
![](https://i.imgur.com/h6LvRUJ.png)
**那麼到底要怎麼把內容抓下來OAO?**
----
### 也只能再仔細觀察文章內容在HTML內的形式
----
檢查過每篇不同文章,就會發現文章都是以 "- -" 來做結尾的
![](https://i.imgur.com/FaDdQcB.png)
----
把先前抓的 "main_content" 文字的部分(text),再用 `split()` 函數丟掉後面不需要的。
又因為文章內可能也含有 "- -" 符號而不小心被切割,再用 `join()` 函數恢復原樣。
```python=
# 將整段文字內容抓出來
all_text = main_content.text
# 以--切割,抓最後一個--前的所有內容
pre_texts = all_text.split("--")[:-1]
# 將前面的所有內容合併成一個
one_text = "--".join(pre_texts)
```
**這樣子文章後面的部分搞定啦**
----
試著先把目前抓到的print出來檢查
![](https://i.imgur.com/Oljgk4L.png)
發現文章是從第一個換行後開始,所以就用 "\n" 來做分割~
----
跟上面一樣的方法,只是改成 "\n"。
```python=
# 以\n切割,第一行標題不要
texts = one_text.split("\n")[1:]
# 將每一行合併
content = "\n".join(texts)
```
**文章內容全部搞定w**
---
## 10. 蛤?管你是誰發的,我全都要!
---
#### 抓出留言並分類
----
### 到流言處>>右鍵>>檢查
----
所有留言都是放在==class="push" 的 div 內==
然後再細分出 "push-tag" "push-userid" "push-content" "push-ipdatetime"
![](https://i.imgur.com/BHUvTxe.png)
----
跟前面一樣使用BeautifulSoup。
```python=
# 抓出所有留言
comments = main_content.find_all("div", class_="push")
for comment in comments:
push_tag = comment.find(
"span", class_="push-tag").string # 分類標籤
push_userid = comment.find(
"span", class_="push-userid").string # 使用者ID
push_content = comment.find(
"span", class_="push-content").string # 留言內容
push_time = comment.find(
"span", class_="push-ipdatetime").string # 留言時間
```
----
為了分類,我建立三個list,分別存放"推"、"→"、"噓"三種標籤。
```python=
push_dic = []
arrow_dic = []
shu_dic = []
```
----
以字典型式將所有資料塞好塞滿,然後再依照標籤分類囉~
```python=
dict1 = {"push_userid": push_userid, "push_content": push_content, "push_time": push_time}
if push_tag == "推 ":
push_dic.append(dict1)
if push_tag == "→ ":
arrow_dic.append(dict1)
if push_tag == "噓 ":
shu_dic.append(dict1)
```
---
## 11. 抓一頁不夠,那就抓兩頁阿!
---
#### 自動換頁
----
#### 現在只能取得一頁的文章,但如果想要取得其他頁的文章該怎麼做?
----
#### 我們想要看上一頁時,會做什麼事?
----
對上方導覽列的 "上頁" 按鈕,使用**右鍵>>檢查**。
我們要的連結在==string="‹ 上頁" 的 `<a>` 中==
![](https://i.imgur.com/5cyNOug.png)
----
把新的連結給url,再來就用for迴圈做到想要的頁數吧~
```python=
# 找到上頁的網址,並更新url
url = "https://www.ptt.cc/"+
soup.find("a", string="‹ 上頁")["href"]
```
---
## 12. JSON是什麼挖糕?
---
#### JSON格式介紹
----
以易於讓人閱讀的文字為基礎,用來傳輸由屬性值或者序列性的值組成的資料交換語言
-- 維基百科
![](https://i.imgur.com/o4ALd1u.png =300x)
----
python 與 json 的差別
![](https://i.imgur.com/TBjpKm9.png)
----
#### 簡單來說,json就是單純由list、dict型態所組成的複合資料型態。
----
#### 所以,我們要做的前置作業就是把資料整理好,以利將其轉成json檔。
----
首先,宣告一個list,名稱為data。
```python=
data = [] # 全部文章的資料
```
再來,宣告一個dict,名稱為article_data。
```python=
article_data = {} # 單篇文章的資料
```
以及另一個dict,名稱為comment_dic。
```python=
comment_dic = {} # 所有留言
```
dict的格式:`{"key": "value"}`
----
所有資料包起來
```python=
article_data["author"] = author
article_data["title"] = title
article_data["time"] = time
article_data["content"] = content
comment_dic["推"] = push_dic
comment_dic["→"] = arrow_dic
comment_dic["噓"] = shu_dic
article_data["comment"] = comment_dic
data.append(article_data)
```
----
#### 最後轉成json格式後,如果只是單純顯示在terminal上會很難辨識,也很難讓其他程式使用。
----
#### 那就乾脆寫檔匯出吧!
Python內建有json套件,利用 *dump()* 將其轉成json然後匯出囉!
```python=
import json
```
```python=
# 輸出JSON檔案
with open('data.json', 'w', encoding='utf-8') as f:
json.dump(data, f)
```
----
可以到網站[json editor](https://jsoneditoronline.org/),可以很方便的確認最後匯出的json檔喔~
![](https://i.imgur.com/yTNhBCq.png)
----
#### 有了這些資料後,就可以用於其他的應用了~
---
## 總結
---
這堂課先從網頁的架構HTML開始,接著以PPT八卦版為例,代入cookie和session概念,以及使用Python的requests和BeautifulSoup套件篩選出我們要的資料,到最後匯出成JSON格式。
---
## Q & A
----
Q1: 所以我們這麼辛苦抓下了這些資料要做什麼?
A1: 在機器學習上,資料量決定了訓練出來的精準度,因此這些資料可是非常重要的喔!
----
Q2: 我不是上面講的那些人員,我也可以爬蟲嗎?
A2: 當然可以! 只要你是要從網路上取得大量資料,而且想要電腦幫你完成這件事的人,就可以利用爬蟲幫你省下大量時間!
----
Q3: 我想爬爬看別的網站,但我不知道從何開始下手?
A3: 一開始可以先從PTT的其他版開始,因為PTT應該算是最好爬的網站了,等到比較熟悉之後,就可以開始爬其他網站了,爬蟲的重點就是要**多觀察**,多利用**右鍵>>檢查**來了解原始碼的樣子,或到**F12>>網路**檢查傳了什麼request給伺服器,都能幫助你更了解這個網站的運作方式,並找出破綻來取得資料。
----
Q4: 我用這堂課教的方法在爬某個網站時,發現無論如何都抓不到資料,怎麼會這樣?
A4: BeautifulSoup和requests並不是萬用的,爬蟲還有很多的技巧可以用,不同的技巧對應到不同的情況,但這部分就要由你們自己去探索囉!
---
[完整程式碼GitHub網址](https://github.com/AndyChiangSH/NCHU_topic_homework/tree/master/hw01_PTT%20crawler)
---
## Homework
---
爬[PTT政黑板](https://www.ptt.cc/bbs/HatePolitics/index.html),跟我做一樣的事情。
完成後將程式碼寄到我的信箱
**chiang1051009@gmail.com**。
信件標題格式為 **學號-系級-姓名**。
程式碼需要註解說明每一段的功用。
---
## END
![](https://i.imgur.com/rTUnT06.png =50%x)
{"metaMigratedAt":"2023-06-16T07:16:09.857Z","metaMigratedFrom":"YAML","title":"This is heading1","breaks":true,"slideOptions":"{\"transition\":\"slide\"}","contributors":"[{\"id\":\"368f247e-5311-4476-bb9f-5b2db977e314\",\"add\":15625,\"del\":1329}]"}