---
# System prepended metadata

title: 爬蟲實作筆記
tags: [作品]

---

# 爬蟲實作筆記
爬蟲-讓我們省去瀏覽網站的時間，自動化抓取網站上的特定內容，另外也可搭配不同功能將資料儲存成特定資料型態，方便檢視及觀看。

<div class="name">中央資工-朱禹澄著</div>
<style>
    .name{
        text-align:right;
    }
</style>  

:::success
我們需要的模組(Module):
- requests 下載網頁
- beautifulsoup4 解析網頁、獲得資料
- json 把到的資料轉換成JSON格式
- openpyxl 將df寫入excel表格中
(若有遇到任何安裝模組問題可直接詢問ChatGPT)

:::

![image](https://hackmd.io/_uploads/H1ri7rG_A.png)
<div class="name">圖片來源:CodeShiba程式柴</div>
<style>
    .name{
        text-align:right;
    }
</style>  

---
:::spoiler 完整程式碼(存成excel版本)
```python=
import requests
from bs4 import BeautifulSoup
import json
import pandas as pd

url = "https://www.ptt.cc/bbs/NBA/index.html"
response = requests.get(url)

soup = BeautifulSoup(response.text, "html.parser")
articles = soup.find_all("div", class_="r-ent")

data_list = []
for a in articles:
    data = {}

    title = a.find("div", class_="title")
    if title and title.a:
        title = title.a.text
    else:
        title = "沒有標題"
    data["標題"] = title

    popularity = a.find("div", class_="nrec")
    if popularity and popularity.span:
        popularity = popularity.span.text
    else:
        popularity = "N/A"
    data["人氣值"] = popularity

    date = a.find("div", class_="date")
    if date:
        date = date.text
    else:
        date = "N/A"
    data["日期"] = date

    data_list.append(data)


df = pd.DataFrame(data_list)
df.to_excel("ptt_nbaa.xlsx", index=False, engine="openpyxl")
print(df)
print("資料已成功儲存在: ptt_nbaa.xlsx 中")
```
:::
---
## 第壹部-讀取網頁

:::info
#### 1-1 黃金六行
:::

```python=
import requests
headers = {'User-Agent':"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15" }
url = "https://www.ptt.cc/bbs/NBA/index.html"
response = requests.get(url, headers=headers)
with open('output.html', 'w', encoding='utf-8') as f:
    f.write(response.text)
```

:::spoiler 逐行解釋
- 一開始我們引入requests模組(需在終端機先pip install requests)
- 模仿正常人，加上request的header避免被反爬蟲機制阻擋
- 定義url這個變數為想要爬取的網站網址
- 定義response這個變數為使用request get方法爬到url資料的值
- 將response.text 的內容以 utf-8 編碼寫入到名為 output.html 的文件中。
![useragent](https://hackmd.io/_uploads/SkB9Frf_A.png)
:::


---
## 第貳步-觀察網頁

:::info
#### 2-1 右鍵->檢查->按下elements左邊兩格的圖案->觀察要爬取的資料在哪一欄裡
:::

:::spoiler 圖示步驟
![image](https://hackmd.io/_uploads/ry2DSHzuR.png)
![image](https://hackmd.io/_uploads/Hyjcrrz_R.png)
![image](https://hackmd.io/_uploads/S1_seeAOR.png)
![image](https://hackmd.io/_uploads/Hk_0BSGd0.png)
![image](https://hackmd.io/_uploads/SJsw8BzuC.png)
:::

---
## 第參步-解析網頁

:::info
#### 3-1 變數soup-把剛剛得到的response(爬到的網站資料)丟到beautifulSoup()裡面解析
:::

```python=
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.text, "html.parser") 
# "html.parser"用html解析器 解析response.text產生的html
articles = soup.find_all("div", class_='r-ent')
#find_all會把所有找到符合的加在articles這個list裡面
#print(articles[0]) 在終端機測試看看有沒有輸出r-ent第一大條的html
```

---
:::info
#### 3-2 一張圖解釋一切
:::

```python=
soup = BeautifulSoup(response.text, "html.parser") 
# "html-parser"用html解析器 解析response.text產生的html
articles = soup.find_all("div", class_='r-ent') #find_all會把所有找到符合的加在articles這個list裡面
print(articles[0])
```

![image](https://hackmd.io/_uploads/Sy15vgCO0.png)


:::spoiler ### **如果要更詳細的得到某一項，那我們就要去找到該項對應到的html碼**

**(例如標題 -> div, 'title', a, "文章標題文字")**
**(一種attribute的感覺，eg:title.a)**

![image](https://hackmd.io/_uploads/BkUCnBG_A.png)
---
**可以觀察到整個標題條是包含在"r-ent"下(包含"nrec"對應到的人氣及"title對應到的標題**
![image](https://hackmd.io/_uploads/SJirGe0OC.png)
:::

---
:::info
#### 3-3 如果我們想要萃取出所有的標題，我們可以搭配for迴圈迭代整個articles列表
:::

:::danger
- **迴圈的a跟html的a是不一樣的東西，請不要搞混**
- **變數的值可以持續不斷的被覆蓋更新**
:::
![image](https://hackmd.io/_uploads/B1VsixCuC.png)

:::　　spoiler 三個解釋點
- 用for迴圈迭代每一個articles列表find到的元素
- 利用.text使得輸出的是純文字而不是html程式碼
- 發現找到的其中一項(articles中找到的其中一個 class="r-ent"中是沒有東西的 -> 這是因為該文章已被刪除，故所有裡面的內容皆不見了，只留下一個空殼。   
:::

---
**問題:有些文章可能被刪除，只剩下空殼，無法將None轉換
解決方法:判斷該"r-ent"是否被刪除，是的話需要另做處理，避免發生錯誤**

```python=
for a in articles: #每一個文章就是一個a
    title = a.find("div", class_="title")
    if title and title.a: #有title且有title下面的a
        title = title.a.text #如果有標題的話，我們讓title這個變數名稱的值為該標題
    else:
        title = "沒有標題"
    print(title) #此處的a非上面的a，此為html title下面的連結a(a href=......的a)
```
::: spoiler 輸出結果
![image](https://hackmd.io/_uploads/H1GRMbRu0.png)
:::
::: spoiler **如果我們還想得到發佈日期、人氣值等等資訊，我們可以這麼做...**
**step1:我們先找到人氣值與日期的對應html區塊**
![image](https://hackmd.io/_uploads/B1-nrWCOC.png)

**step2:對症下藥**
```python=
for a in articles: #每一個文章就是一個a
    title = a.find("div", class_="title")
    if title and title.a: #有title且有title下面的a
        title = title.a.text #如果有標題的話，我們讓title這個變數名稱的值為該標題
    else:
        title = "沒有標題"
    # print(title) #此處的a非上面的a，此為html title下面的連結a(a href=......的a)

    popularity = a.find("div", class_="nrec")
    if popularity and popularity.span:
        popularity = popularity.span.text # 輸出nrec下面span區塊裡面的純文字
    else:
        popularity = "N/A"

    date = a.find("div", class_="date")
    if date:  #date區塊內就直接是我們要的內容了
        date = date.text
    else:
        date = "N/A"

    print(f"標題:{title} 人氣值:{popularity} 日期:{date}")
```
![image](https://hackmd.io/_uploads/Sy-iubAdR.png)

:::


---
## 第肆部-儲存內容
#### 儲存以便爾後重複使用

:::info
#### 4-1結構化資料-先把資料以一一對應的dictionary元素加入list中(需要想一下結構長怎麼樣)
:::

data_list內每一個元素都是一大條r-ent內的數據(包含該條的標題，人氣值及時間) 

:::danger
- 字典的key重複時，value會被新進來的給覆蓋取代
:::

```python=
data_list = [] #儲存所有artcles內("r-ent"區塊內)資料的容器
for a in articles:
    data = {} #儲存單一"r-ent"區塊內的標題、人氣值及日期元素
    title = a.find("div", class_="title")
    ...
    data["標題"] = title # key:"標題", value:title
    
    popularity = a.find("div", class_="nrec")
    ...
    data["人氣"] = popularity
    
    date = a.find("div", class_="date")
    data["時間"] = date
    data_list.append(data) 
# [ {k11:val11, k12:val12... }, {k21, val21, k22:val22...}, ... ]
print(data_list) #測試
print(data_list[0]) #測試
```

:::spoiler 輸出結果
![image](https://hackmd.io/_uploads/S1JwyUCuC.png)

:::

:::info
#### 4-2將結構化資料儲存成json格式的檔案
:::
:::success
想想看:json格式有甚麼功用? 獲取伺服器資料時所獲得的格式? 還有嗎?
:::
```python=
import json
with open("ptt_nba_data.json", "w", encoding="utf-8") as file:
    json.dump(data_list, file, ensure_ascii=False, indent=4)
print("資料已經成功儲存為: ptt_nba_data.json")
```

:::spoiler 輸出結果
![image](https://hackmd.io/_uploads/SJN0yLA_C.png)
![image](https://hackmd.io/_uploads/S1qQlUC_C.png)
:::

:::info
#### 4-3將結構化資料儲存成Excel格式的檔案-使用pandas將列表data_list轉換成dataframe
:::

:::danger
- 檔名命名時盡量使用小寫英文字母及底線，不要使用大寫、空格或特殊符號
:::

```python=
df = pd.DataFrame(data_list)
df.to_excel("ptt_nba.xlsx", index=False, engine="openpyxl")
```

:::spoiler 詳細解說
![image](https://hackmd.io/_uploads/rJ3I9tR_0.png)
- DataFrame是一個pandas中的一種資料結構，類似excel的表格或SQL表中的一個表格
- df.to_excel -> 將 DataFrame df 輸出到一個 Excel 文件中
- "ptt_nba.xlsx" -> 保存 Excel 文件的名稱
- index=False -> 表示不要將 DataFrame 的索引寫入到 Excel 文件中。如果設置為 True，索引將作為 Excel 文件中的第一列。
- engine="openpyxl" -> 指定使用 openpyxl 作為寫入 Excel 文件的引擎。openpyxl 是一個 Python 庫，用於讀取和寫入 Excel 2010 xlsx/xlsm/xltx/xltm 文件。
![image](https://hackmd.io/_uploads/BJZP9tAOR.png)
:::

:::spoiler 成果
![image](https://hackmd.io/_uploads/ryq9YtC_A.png)
![image](https://hackmd.io/_uploads/By_pdt0dR.png)
:::

---
:::spoiler 完整程式碼(存成excel版本)
```python=
import requests
from bs4 import BeautifulSoup
import json
import pandas as pd

url = "https://www.ptt.cc/bbs/NBA/index.html"
response = requests.get(url)

soup = BeautifulSoup(response.text, "html.parser")
articles = soup.find_all("div", class_="r-ent")

data_list = []
for a in articles:
    data = {}

    title = a.find("div", class_="title")
    if title and title.a:
        title = title.a.text
    else:
        title = "沒有標題"
    data["標題"] = title

    popularity = a.find("div", class_="nrec")
    if popularity and popularity.span:
        popularity = popularity.span.text
    else:
        popularity = "N/A"
    data["人氣值"] = popularity

    date = a.find("div", class_="date")
    if date:
        date = date.text
    else:
        date = "N/A"
    data["日期"] = date

    data_list.append(data)


df = pd.DataFrame(data_list)
df.to_excel("ptt_nbaa.xlsx", index=False, engine="openpyxl")
print(df)
print("資料已成功儲存在: ptt_nbaa.xlsx 中")
```
:::