---
# System prepended metadata

title: Dcard網路爬蟲

---

<style>
.reveal, .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 {
	font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, "Microsoft JhengHei", Meiryo, "ＭＳ ゴシック", "MS Gothic", sans-serif;
}
h1, h2, h3, h4, h5, h6 {
	text-transform: none !important;
}
.color-yellow{
	color: yellow;
}
.alert {
    padding: 15px;
    margin-bottom: 20px;
    border: 1px solid transparent;
	border-radius: 4px;
	text-align: left;
	padding: 10px 0;
}
.alert-info {
    color: #31708f;
    background-color: #d9edf7;
    border-color: #bce8f1;
}
.alert-success {
    color: #3c763d;
    background-color: #dff0d8;
    border-color: #d6e9c6;
}
.alert-danger {
    color: #a94442;
    background-color: #f2dede;
    border-color: #ebccd1;
}
.reveal {
  font-size: 28px;
}
    
</style>

# Dcard網路爬蟲

---

## 網路爬蟲
#### 網路爬蟲，指的就是自動地在網際網路中進行資料資訊的採集與整理。
其實最笨的方法，我們可以用CTRL+C，CTRL+V的方式，把我們要的網頁資料一個個擷取下來。
<br>
### 如果只有幾十筆還好，但假如有上百筆、上千筆怎麼辦？

----

## 使用API來爬蟲
API定義了如何請求和接收數據的規則，並提供了一個結構化的方式來訪問數據。
因此可以使用程式語言向API發送HTTP請求，通常是GET或POST請求。這些請求通常包括你的API密鑰、API的URL、參數（例如範圍、搜索關鍵字等）。
最後以JSON格式接收並解析網頁返回的數據，以得到所需的信息。

#### 這種方法更加高效率且可控，因為API通常提供了結構化的數據，而不需要解析整個網頁的HTML。同时，使用API也通常更加合法，因為它是通過官方提供的接口來訪問數據。

----

## HTTP（Hypertext Transfer Protocol）<br>超文本傳輸協定
HTTP，可以分為客戶端 (Client) 和伺服器端 (Server)，而在這個傳輸協定裡頭，客戶端是主動的，他可以向伺服器發送請求 (Request)，相對的伺服器端是被動的，他會根據客戶端的請求來回傳不同的回應 (Response)
1. **GET：**
作用：伺服器請求資源（通常是網頁或數據）。
例子：當你在瀏覽器中輸入網址時，瀏覽器實際上是發送一個GET請求以獲取該網址對應的網頁。
2. **POST：**
作用：向伺服器提交數據。
例子：當你在一個註冊表單中填寫信息並點擊"提交"按鈕時，瀏覽器通常會發送一個POST請求將表單數據發送給伺服器。

**簡而言之，GET用於獲取資源，而POST用於提交數據。**

----

## Http response (網頁回應)
**在伺服器端接收並處理完一個請求後他就會回傳一個response**

- 200: success，請求成功
- 302: found，轉址、重新導向
- 400: bad request，伺服器看不懂請求
- 404: not found，找不到網頁
- 500: internal server error，伺服器內部問題
- 505: http version not support，http 版本不支援

當請求成功時，回應的內部就有相應的資料，可能是 html、json 檔、一串文字等

----

## Dcard爬蟲的問題

過去，我們可以使用 https://www.dcard.tw/service/api/v2/posts 取得不分類全部文章，等同於抓取 https://www.dcard.tw/f 頁面內文章資訊。

但現今的Dcard api卻被鎖住了，讓我們沒有辦法方便的使用
![](https://hackmd.io/_uploads/S1r43EpgT.png)

----

![](https://hackmd.io/_uploads/rJrCKrM-T.png =200x)
Cloudflare是一個提供網站安全性提升的服務提供商，它提供了多種方法來識別並阻止機器人（包括惡意機器人）訪問你的網站。
- 使用 CAPTCHA 驗證：要求用戶在訪問頁面時要進行人類驗證。這可以幫助阻止自動化的機器人。
- 設置 IP 阻止規則：封鎖特定機器人的 IP 地址。
![](https://hackmd.io/_uploads/BJuhtBz-T.png =700x)

---

#### 可如果今天網站沒有提供api怎麼辦？
# 那我們只能土法煉鋼，<br>找到網頁中，我們要找的目標資料，複製並儲存下來。

----

## 瀏覽網頁的過程
- 使用者在瀏覽器中輸入路徑，向伺服器發送請求HTML文件
- 伺服器會回傳對應的HTML文件給瀏覽器
- 瀏覽器解析HTML並產生內容、解析CSS產生排版與樣式、解析JavaScript並產生網頁元素與使用者的互動機制…等等
- 最終，生成我們看到的網頁畫面

----

## 網頁由HTML、CSS、JavaScript所組成

- HTML（HyperText Markup Language，超文本標記語言）：用來建構網頁的標記式語言，而非程式。
- CSS (Cascading Stylesheets，階層樣式表）：一種風格頁面語言，負責建構網頁外觀。
- JavaScript：可以讓網頁中實現複雜的功能，經常用在呈現網頁動態效果。負責描述網頁如何與用戶互動。

----

## 什麼是HTML？
HTML 是一種標記語言（markup language），而非一般熟知的程式設計語言。它主要用來告訴瀏覽器該如何呈現你的網頁。

HTML 由一個個元素所組成，而元素中包含了標籤（tag）與內容。
![](https://hackmd.io/_uploads/BJyYmSXWa.png =500x)
- 起始標籤：角括弧，也就是符號「`< >`」，裡面需放入元素名稱，如上面的例子「`<p>`」。起始標籤代表這個元素從這裡開始。
- 結束標籤：與起始標籤一樣，只是在元素名稱前面多了個前置斜線「`/`」。內容的最後加上結束標籤，代表這個元素的尾端。
- 內容：這個元素的內容，以上面的例子來說，內容就是這句文字。
- 元素：由起始標籤、結束標籤、內容所組成。


----

1. 元素中可以添加「屬性（Attribute）」
![](https://hackmd.io/_uploads/rkd9mB7bT.png)
屬性能提供更多的資訊，可以利用屬性設定這個元素的色彩、對齊方式、圖表的格線。
<br>

2. 元素裡面還可以放進其他元素
![image](https://hackmd.io/_uploads/ryZJMFdy1l.png)
這個句子：「我的貓脾氣很暴躁」，若你想強調「very」，我們就可以把「very」用粗體的元素顯示。
<p> My cat is <strong> very </strong> grumpy.</p>

----

## HTML文件基本結構
![](https://hackmd.io/_uploads/HkYGybMZa.png)

----

## HTML標籤
- `<!DOCTYPE html>`：表明文件類型為HTML檔
- `<html>`：用來包裹住整個網頁的內容與設定
- `<head>`：網頁的標頭，經常含有網站名稱、導覽列的訊息等
    - `<title>`：頁面標題
- `<body>`：整個網頁的內容資訊
    - `<h>`：內容的標題。`<h1>`~`<h6>`的順序決定標題的權重
    - `<p>`：用來包裹純文字的段落
    - `<a>`：標籤中屬性，例如`<a href="目標網址" target="_blank"(開啟新分頁)>`等。
    - `<img>`：圖片。`<img src="圖片網址路徑">`
    - `<div>`：division，把關聯的物件聚合起來。

----

### DOM（Document Object Model，文件物件模型）
把HTML文件內的各個標籤，包括文字、圖片等等都定義成物件，<br>而這些物件最終會形成一個樹狀結構。
![](https://hackmd.io/_uploads/Hyc0gzfWa.png)
幫助訪問、查找和修改網頁中的元素，因而可以透過程式語言來改變網頁的內容。

----

## 節點
如果我們將一個網頁比喻為一棵樹，那麼每個HTML標籤就像是樹上的節點
1. 元素節點（Element Node）
最常見的節點類型，對應於 HTML 中的標籤
例如：`<div>、<p>、<span>`
2. 屬性節點（Attribute Node）
用於提供關於元素的額外資訊，對應於元素的屬性
例如：`class="XXXClass"、id="XXXId"`
3. 文字節點（Text Node）
文字節點表示元素內的文字內容，不包含任何 HTML 標籤
例如，在 `<p>Hello World</p>` 中，Hello World 是一個文字節點
4. 註釋節點（Comment Node）
註釋節點包含檔案中的註釋
例如，HTML 中的 `<!– This is a comment –>`

----

## 哪裡可以看到HTML
在瀏覽器按下F12，使用開發者模式。或右鍵找到「Inspect」或「檢查」。
想直接完整的看到HTML，也可以直接在網頁中按下右鍵，找到「View page source」或「檢視原始碼」。
![](https://hackmd.io/_uploads/rJMR6H7-a.png)

----

## 補充：XML V.S. HTML
XML 和 HTML、JSON都同屬於被稱為「**標記語言**」的程式設計語言
兩者同樣用來定義資料的結構、類型、屬性、部件之間的關係

1. HTML 具有預定義的標籤，每個人都必須使用這些標籤（`<header>`、`<p>`），無法建立自己的標籤，相比之下，XML就可使用出於文件用途定義的自訂標籤。
2. HTML 的主要用途是在瀏覽器中以圖形形式顯示以程式語言構建好的文件，所以我們常使用 HTML 來**建立網頁和用戶端 Web 應用程式的**，XML則是允許不同類型的應用程式 (如資料庫) 理解和使用相同的資料及其結構，所以用來在兩個**系統之間交換資料**。
3. XML 可以內嵌在 HTML 中，並用 JavaScript 等語言進行解析以建立動態的網頁。當然，HTML 也可以在必要時內嵌在 XML 中，不過多使用在純文字的資料中。

---

## 靜態網頁v.s.動態網頁
1. 靜態網頁：
    - 生成方式：靜態網頁的內容通常是提前生成好，所以每個網頁都是一個單獨的HTML文件。
    - 呈現特點：訪問靜態網頁時，伺服器直接將預生成的HTML文件發送給瀏覽器，瀏覽器僅負責解釋和呈現HTML、CSS和JavaScript，無需伺服器端的處理。
    - 例子：公司網站介紹、個人履歷等。
2. 動態網頁：
    - 生成方式：根據用戶的請求和數據庫的內容來動態生成HTML。
    - 呈現特點：當用戶訪問動態網頁時，伺服器會根據用戶的請求動態生成HTML，然後將其發送給瀏覽器。
    - 例子：社交網站、在線應用程式等。通常是動態網頁，因為它們需要根據用戶的操作和數據來實時更新內容。

----

## 靜態網頁
![](https://hackmd.io/_uploads/rysRYUQbp.png)

----

## 動態網頁
![](https://hackmd.io/_uploads/BkLZq87WT.png)

---

# 網頁元素定位

## XPath 與 CSS Selector 定位方式

----

## XPath
1. 什麼是XPath?

    - XPath，全稱為 XML Path Language，是一種用於在 XML 檔案中查詢資訊的語言
    - 現代的解析器可以將 HTML 轉換為類似於 XML 的 DOM 結構，因此可以使用 XPath 進行查詢，這使得它成為提取網頁數據的強大工具
    - XPath透過路徑表達式（也就是前面提到的分層結構），用於定位 XML 或 HTML 檔案中的節點來選取文件中的「節點」或「節點集」。這些節點可以是元素、屬性、文字等

----

2. 常見的 XPath 表達式：
    - `nodename`：挑選所有名為 `<nodename>` 的節點
    - `/` ：被稱為絕對路徑定位
    當 `/` 用在表達式開頭時，表示查詢從該節點開始
    當 `/` 用在兩個節點之間，表示查詢從該節點下的節點開始 
        * 例如：`/title`：挑選 `<title>` 節點下的所有內容
        * 例如：`/html/body`：挑選 `<html>` 節點下 `<body>` 的所有內
    - `//`：被稱為相對路徑定位
    在整個檔案中進行全面搜索，尋找符合條件的節點，不用擔心它具體在哪一層
        * 例如：`//p`：挑選檔案中所有的 `<p>` 節點。
    - `[]`：被稱為索引定位
    挑選節點中的某個特定的條件，可以是節點的順序或擁有的特定屬性、包含的文字內容
        * 例如：`//p[2]`：挑選檔案中第二個 `<p>` 節點
        * 例如：`//div[span]`：挑選所有包含特定子元素 `<span>` 的 `<div>` 節點

----

### 補充：`//div[span]` V.S. `//div/span`
* `//div[span]`：尋找所有包含至少一個 `<span>` 子元素的` <div> `元素
* `//div/span`：所有位於 `<div>` 元素內部的 `<span>` 元素

假如我們有以下的 HTML 檔案結構：
```
<html>
  <body>
    <div>
      <span>第一個 div 的 span</span>
    </div>
    <div>
      <p>第二個 div 的 p 元素，內有 <span>span</span></p>
    </div>
  </body>
</html>
```
`//div[span]` 匹配結果：
```
<div>
  <span>第一個 div 的 span</span>
</div>
```
`/div/span` 匹配結果：
```
<span>第一個 div 的 span</span>
```

----

2. 常見的 XPath 表達式：
    - `@`：可以用於挑選元素的特定屬性，也可以用於篩選特定條件
        * 例如：`//box/@class`：挑選所有有 `class` 屬性的 `<box>` 元素 
        * 例如：`//box[@class=’red’]`：`[@class=’red’]` 是一個條件，意思是「挑選那些其 `class` 屬性等於 "red" 的 `<box>` 元素」
    - `.`：代表「當前正在查看的節點」
    - `「..」`在 XPath 表達式中用來挑選任何節點的「父節點」，也就是說，它允許你從當前節點向上移動到其直接上層的節點。
        * 當我們有HTML結構，假設當前節點為`/html/body/div`
            ``` 
            <html>
              <body>
                <div>
                  <p>第一段落</p>
              </body>
            </html>
            ```
        * 例如：`./p`：會挑選這個 `<div>`元素內的所有` <p>` 元素
        * 例如：`../..`：會挑選`<div> `元素上層再上層的`<html>`元素

----

## CSS Selector

1. 了解CSS Selector前，先了解CSS
    - CSS 可以說是針對 HTML 標籤在具現化後，進行外觀調整，所以CSS 需要額外編寫並宣告與 HTML 的選擇關係，這樣瀏覽器才會知道哪個標籤該用怎樣的 CSS 外觀
![image](https://hackmd.io/_uploads/rJq2W3OJyg.png =400x)
    - 與 HTML 不同，屬性與值在兩者是不一樣的描述參數：
![image](https://hackmd.io/_uploads/BJknk3Ok1e.png =400x)

----

#### 了解CSS與HTML的關係，要修改目標的外觀，我們要能「定位」目標
2. CSS Selector 介紹
可以簡單理解為：element name（大範圍）、class name（複數範圍）、id name（單一範圍），
    - `Element Type Selector`：也就是指定常見的標籤作為定位對象
        - 例如：`<h1>、<p>、<div>`等
    - `Class Selector`：對標籤加上 `class` ，讓特定標籤形成一個群組。而在CSS編譯器裡往往用`「.」`來指向`class`
        - 例如：![image](https://hackmd.io/_uploads/rJPS6hOJ1l.png =400x)

----

2. CSS Selector 介紹

    - ID Selector：對指定標籤加上 `id` ，`class`可以提供給很多 HTML 標籤使用，但`id`只能有一個。而在CSS編譯器裡往往用`「#」`來指向`id`
        - 例如：![image](https://hackmd.io/_uploads/rJKeRnOJJx.png)

----

3. CSS Selector定位

    - `.class、#id、element`：選擇特定標籤及屬性下的所有節點
        - 例如：`.introl`為選擇`class="intro"`的所有元素
        - 例如：`#firstname`為選擇 `id="firstname"` 的所有元素
        - 例如：`div > p`為選擇上層為`<div>`元素的所有 `<p>`元素
        - 例如：`div + p`為選擇`<div>`元素下層的所有元素
    - `[]`：同樣是挑選某個特定的條件、屬性的節點
        - 例如：`div[name]`為選擇帶有`name`属性的`<div>`元素
        - 例如：`div[name=_keywords]`為選擇`name="_keywords"` 的`<div>`元素
    - `nth-child(n)`：特定序列子元素下的所有節點，也就是透過上下層關係來挑選特定節點
        - 例如：`p:nth-child(2)`為第二子元素下的所有`<p>`元素

----

## XPath 定位 V.S. CSS Selector 定位
總的來說，XPath對於文本的處理上，功能要更全面、更強大；CSS Selector通常語法較為簡潔，運行速度更快些
![image](https://hackmd.io/_uploads/HyU_DTuJ1l.png)

---

## Selenium

對於爬蟲來說，如果爬取動態網頁內容，只會得到一堆JavaScript程式碼，因為網頁內容需要透過執行JavaScript程式碼後產生。

**Selenium**可以說是自動化控制網路瀏覽器工具，用來**模擬出使用者在瀏覽器的所有操作行為** (點擊按鈕、輸入帳號密碼、捲動捲軸...等)，因此除了爬蟲的應用，也常作為「自動化測試」使用的工具，在網站開發完成後，透過自動化的腳本測試所有功能是否正常。
![](https://hackmd.io/_uploads/B1HIxP7bp.png =500x)

----

## WebDriver
WebDriver可以用來執行並操作瀏覽器的API介面。每一個瀏覽器都會有各自對應的驅動程式(driver)，Selenium就可以透過WebDriver來直接對瀏覽器進行操作，對其所支援的瀏覽器進行自動化作業，就如同真的使用者在操作。

![](https://hackmd.io/_uploads/rkp9gwQ-T.png =800x)


---

# 使用R版本的Selenium & WebDriver

----

### 1. 安裝 RSelenium 套件
``` r=
install.packages("RSelenium")
```
或是從 GitHub 上面下載最新的版本來安裝：
```r=
# 從 GitHub 安裝 RSelenium
install.packages("remotes")
remotes::install_github("ropensci/RSelenium")
```
<br>

### 2. 安裝wdman套件
提供許多Webdriver和Selenium相關的專案
``` r=
install.packages("wdman")
```

----

### 3. 安裝Java
https://www.java.com/zh-TW/download/manual.jsp
Selenium需要Java輔助執行
![](https://hackmd.io/_uploads/SylHE8fZa.png =500x)

----

### 4. 確認並更新Chrome
在Chrome網址中輸入`chrome://version` 進行確認
點選右上角設定 > 說明 > 關於 Google Chrome ，進行更新
![](https://hackmd.io/_uploads/Hy9qCLzZ6.png)

----

### 5. 下載ChromeDriver
https://googlechromelabs.github.io/chrome-for-testing/
選擇stable的版本來下載
![](https://hackmd.io/_uploads/HJxr1vGbp.png)
**ChromeDriver網站**
https://chromedriver.chromium.org/home

----

### 6.將ChromeDriver放進專案中
```r=
# install.packages("wdman")
# install.packages("RSelenium")
library(wdman)
library(RSelenium)
# 啟用一次selenium()，安裝WebDriver相關專案
# 並且後續要把剛下載的WebDriver，放進專案裡面
selenium() # 這裡會需要跑一段時間來下載專案
# 確認專案位置
chromeCommand <- chrome(retcommand = T, verbose = F, check = F)
chromeCommand

rm(chromeCommand)
gc() #負責回收垃圾的函數，可以釋放不再使用的內存
```

----

### 6.將ChromeDriver放進專案中

找到"C:\\Users\\323be\\AppData\\Local\\binman\\binman_chromedriver\\win32"
大家請記得把"C:\\Users\\{323be}\\..."中的{323be}改成自己的user名字

將ChromeDriver壓縮檔，放到這裡，並且把名字改成剛剛下載的版本
![](https://hackmd.io/_uploads/r1zECr7Z6.png =300x)


### 這個步驟非常重要↓
將改好的資料夾(118.0.5993.70，請用最新的版本名稱)中的LICENSE.chromedriver**刪除**
![](https://hackmd.io/_uploads/H15wGvzb6.png =300x)

---

## R的實戰篇
```r=
# 確認版本
binman::list_versions("chromedriver")

rD <- rsDriver(browser = "chrome",
               chromever = "118.0.5993.70",
               verbose = F)

remDr <- rD[["client"]]
# 打開瀏覽器
remDr$open()
# 關閉瀏覽器
remDr$quit()  # 直接退出
remDr$close()  # close用於關閉當前會話，也可以用作關閉瀏覽器
rD[["server"]]$stop() # 關掉selenium的server
rm(rD) # 删除瀏覽器
rm(remDr)
gc() #負責回收垃圾的函數，可以釋放不再使用的內存

# 開啟特定網頁
remDr$navigate("http://www.google.com/ncr")
# 取得該網頁url
remDr$getCurrentUrl()
```

----

## 網頁定位方法
使用WebDriver時很重要的是，需要了解「如何在頁面上查找元素」。也就是如何定位出需要的資料位置。

#### 開發者模式，可以幫助我們查找元素

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


----

## 定位到網頁上某個物件－findElements函數

這個函數我們主要使用兩個參數，`using`和`value`。
其中`using`：用來指定查找元素用的定位方法。
而`value`則是我們根據`using`方法所搜尋的目標
:::success
findElements(using = c("xpath", "css selector", "id", "name", "tag name", "class name", "link text", "partial link text"), value)
:::

----

1. name：通過元素的 NAME 屬性來查找元素。
2. class name：通過元素的類別名來查找元素。不支持複合類別名。
3. id：通過元素的 ID 屬性來查找元素。
``` r=
# 使用name屬性定位，找到搜尋框
webElem <- remDr$findElement(using = "name", value = "q")
webElem$getElementAttribute("name")
## [[1]]
## [1] "q"
webElem$getElementAttribute("class")
## [[1]]
## [1] "gLFyf"
webElem$getElementAttribute("id")
## [[1]]
## [1] "APjFqb"
 
# 使用class name定位，找到搜尋框
webElem <- remDr$findElement(using = "class name", value = "gLFyf")
# 使用id定位，找到搜尋框
webElem <- remDr$findElement(using = "id", value = "APjFqb")
```

----

4. link text：通過連結文字來查找元素。
5. partial link text：通過部分連結文字來查找元素。
``` r=
# 使用sendKeysToElements發送關鍵字
webElem$sendKeysToElement(list("網路爬蟲", key = "enter"))
# 到網路爬蟲的搜尋頁面
# 使用partial link text，找到有連結的文字元素
webElems <- remDr$findElements(using = "partial link text", value = "網路爬蟲")
sapply(webElems, function(x) x$getElementText())
## [[1]]
## [1] "網路爬蟲- 維基百科，自由的百科全書\n維基百科\nhttps://zh.wikipedia.org › zh-tw"

## [[2]]
## [1] "認識網路爬蟲：爬蟲的應用與原理\nwebscrapingpro.tw\nhttps://www.webscrapingpro.tw › w..."
```

----

6. xpath：通過 XPath 運算式來查找元素
7. css selector：通過 CSS 選擇器來查找元素。
``` r= 
webElems <- remDr$findElements(using = "css selector", value = "h3.LC20lb.MBeuO.DKV0Md")
resHeaders <- unlist(lapply(webElems, function(x) {x$getElementText()}))
resHeaders
## [1] "網路爬蟲- 維基百科，自由的百科全書"
## [2] "認識網路爬蟲：爬蟲的應用與原理"

webElems <- remDr$findElements(using = "xpath", value = "//*[@id='rso']/div[1]/div/div/div[1]/div/div/span/a/h3")
resHeaders <- unlist(lapply(webElems, function(x) {x$getElementText()}))
resHeaders
[1] "網路爬蟲- 維基百科，自由的百科全書"
```

----
8. tag name：通過元素的標籤名來查找元素。
``` r=
remDr$navigate("https://CRAN.r-project.org/")
webElems <- remDr$findElements(using = "tag name", "frame")
XML::htmlParse(remDr$getPageSource()[[1]])
## <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
## <html>
## <head>
## <title>The Comprehensive R Archive Network</title>
## <meta http-equiv="content-type" content="text/html; charset=utf-8">
## <link rel="icon" href="favicon.ico" type="image/x-icon">
##<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
## <link rel="stylesheet" type="text/css" href="R.css">
## </head>
## <frameset cols="1*, 4*" style="border: none;">
## <frameset rows="120, 1*">
## <frame src="logo.html" name="logo" frameborder="0">
## <frame src="navbar.html" name="contents" frameborder="0">
## </frameset>
## <frame src="banner.shtml" name="banner" frameborder="0">
## <noframes>
## <body>
## <h1>The Comprehensive R Archive Network</h1>
## Your browser seems not to support frames,
## here is the <a href="navbar.html">contents page</a> of CRAN.
## </body>
## </noframes>
## </frameset>
## </html>

webElems <- remDr$findElements(using = "tag name", "frame")
# webElems <- remDr$findElements(using = "xpath", value = "//frame") # using xpath
# webElems <- remDr$findElements(using = "css", value = "frame") # using css
sapply(webElems, function(x){x$getElementAttribute("src")})
## [[1]]
## [1] "https://cran.r-project.org/logo.html"
## 
## [[2]]
## [1] "https://cran.r-project.org/navbar.html"
## 
## [[3]]
## [1] "https://cran.r-project.org/banner.shtml"
```

----

:::success
* getElementText()：獲取元素的內部文字（innerText）
* sendKeysToElement(sendKeys)：這個功能可以用於模擬在網頁上的文字輸入。主要是用來模擬按鍵輸入結果並發送到元素中，
* getElementAttribute(attrName)：獲取元素的屬性值。
* getPageSource()：取得整個網頁的原始程式碼。
:::
``` r= 
remDr$navigate("http://www.google.com/ncr")
# 使用name屬性定位，找到搜尋框
webElem <- remDr$findElement(using = "name", value = "q")
webElem$getElementAttribute("name")
## [[1]]
## [1] "q"
# 使用sendKeysToElements發送關鍵字
webElem$sendKeysToElement(list("網路爬蟲", key = "enter"))
webElems <- remDr$findElements(using = "partial link text", value = "網路爬蟲")
resHeaders <- sapply(webElems, function(x) x$getElementText())
resHeaders
## [[1]]
## [1] "網路爬蟲- 維基百科，自由的百科全書\n維基百科\nhttps://zh.wikipedia.org › zh-tw"
remDr$navigate("https://zh.wikipedia.org")     
WebCrawler <- remDr$getPageSource()[[1]]

rvest::read_html(WebCrawler)
write(WebCrawler, file = "WebCrawler_wiki.html")
```

---

# 進入到python爬蟲

----

## undetected-chromedriver

這是別人幫我們寫好的套件，模擬我們使用chromedriver像是人類，所以可以通過人機測驗
https://github.com/ultrafunkamsterdam/undetected-chromedriver

``` python=
# !pip install undetected-chromedriver

# 該網站的CAPTCHA驗證測試
import undetected_chromedriver as uc
driver = uc.Chrome(headless=True,use_subprocess=False)
driver.get('https://nowsecure.nl')
driver.save_screenshot('nowsecure.png')
```

----

## 程式碼
### 先來運行套件
``` python=
# !pip install selenium
# !pip install webdriver_manager
# !pip install undetected_chromedriver 
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
import time
from time import sleep
import undetected_chromedriver as uc
import json
import random
import pandas as pd
from bs4 import BeautifulSoup
```

----

### 開啟瀏覽器
``` python=  
#%%
# 設定別人提供的 user agent，可以避免被記錄
user_agents = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36"
]
options = uc.ChromeOptions()
options.headless = False # 這部分是讓其可以有視窗的呈現
random_user_agent = random.choice(user_agents)
options.add_argument(f"--user-agent={random_user_agent}") #隨機抓取一個設定好的 user agent
driver = uc.Chrome(options=options, version_main = 129)

#%%
url = "https://www.dcard.tw/f"
driver.get(url)
```


----

### 先抓好url
``` python=       
#%%
tmp = []

def extract_links():
    try:
        Mult_links = driver.find_elements(
                        By.CSS_SELECTOR, 'div.d_b1_1w.wdl7s0r')
        for ClassLink in Mult_links:
            elementSoup = BeautifulSoup(ClassLink.get_attribute('innerHTML'), 'html.parser')
            links = elementSoup.find_all('a', class_='d_d8_1hcvtr6 d_cn_2h d_gk_10yn01e d_7v_gdpa86 d_1938jqx_2k d_2zt8x3_1y d_grwvqw_gknzbh d_1ymp90q_1s d_89ifzh_1s d_1hh4tvs_1r d_1054lsl_1r t1gihpsa')
            for each_link in links:
                print(each_link['href'])
                tmp.append(each_link['href'])
    except Exception as e:
        print(f"An error occurred: {str(e)}")
        tmp.append('None')

def scroll_and_extract():
    innerHeight = 0
    offset = 0
    count = 0
    limit = 1

    # 持續捲動直到沒有元素動態產生
    while count <= limit:
        sleep(2)
        extract_links()
        offset += 800
        driver.execute_script(f'''
            window.scrollTo({{
                top: {offset}, 
                behavior: 'smooth' 
            }});''')
        # 獲取捲動後的當前總高度
        innerHeight = driver.execute_script(
            'return window.document.documentElement.scrollHeight;'
        )
        if offset >= innerHeight:
            count += 1

#%%
# 運行
scroll_and_extract()

#%%
# 存取成txt
tmp = list(set(tmp)) #讓資料不會重複

#存取抓到的網址
with open("C:/Users/323be/Documents/just_for_try/dcard_link.txt" , "w")as f:
    for element in tmp:
        f.write(str(element) + "\n")
```

----

### 存取文字資料（json檔）
``` python=
#%%
def extract_post():
    
    # 文章標題
    title = driver.find_element(
        By.CSS_SELECTOR, 'div > h1').get_attribute('innerText')
    
    # 發文內容
    try:
        txt = driver.find_element(By.CSS_SELECTOR, 'div.d_ma_o2urrm.c1golu5u > div > div').get_attribute(
            'innerText').replace('\n', '')
    except:
        # 這邊沒改好，還沒找到可能的例外
        txt= driver.find_element(By.CSS_SELECTOR,'#__next > div > div.f1og407v > div > div > div > div > article > div.d_ma_o2urrm.c1golu5u > div > a > div > div.m1vig5f1 > div >div').get_attribute('innerText').replace('\n', '')
    
    # 發文時間
    date = driver.find_element(
        By.CSS_SELECTOR, 'div.d_d8_2s.d_cn_1v.d_gk_l52nlx.d_7v_7.d_xa_2b.d_lzn0r3_af0gpl.d_du7p7_3j.d_1qszg8z_2m.d_1ti0u92_7 time').text
    
    # 提取作者名稱和性別
    author_name = ""
    author_gender = ""
    author_identifier = ""

    # 找到 <script type="application/ld+json"> 並提取作者信息
    scripts = driver.find_elements(By.XPATH, '//script[@type="application/ld+json"]')
    for script in scripts:
        script_text = script.get_attribute('innerText')
        data = json.loads(script_text)
        
        if isinstance(data, list):
            for item in data:
                if item.get('@type') == 'SocialMediaPosting' or item.get('@type') == 'DiscussionForumPosting':
                    author_info = item.get('author', {})
                    author_name = author_info.get('name', '')
                    author_gender = author_info.get('gender', '')
                    author_identifier = author_info.get('identifier', '')
                    break  # 找到後即可跳出循環
        # 將 "Female" 和 "Male" 轉換為 "女生" 和 "男生"
        if author_gender == 'Female':
            author_gender = '女生'
        elif author_gender == 'Male':
            author_gender = '男生'
    
    # 愛心&留言數量
    try:
        HC_total = driver.find_element(
            By.XPATH, '//*[@id="__next"]/div[2]/div[2]/div/div/div/div/div[4]/div/div/div[1]').get_attribute('innerText')
        HC_totals = HC_total.replace('\n', ' ').split(' ')
    except: 
        HC_total=driver.find_element(
            By.XPATH,'//*[@id="__next"]/div[2]/div[2]/div/div/div/div/div[5]/div/div/div[1]').get_attribute('innerText')
        HC_totals = HC_total.replace('\n', ' ').split(' ')
    
    # 文章標籤
    try:
        tickets = driver.find_element(
            By.CSS_SELECTOR, 'meta[name="keywords"]').get_attribute('content')
        ticket = tickets.replace('\n', ',')
    except:
        ticket = 'None'
        
    
    # 存取資料到 result 裡面
    result = {
        '文章標題': title,
        '作者名稱': author_name,
        '作者id': author_identifier,
        '作者性別': author_gender,
        '發文時間': date,
        '發文內容': txt,
        '文章標籤': ticket,
        '愛心數量': HC_totals[0],
        '留言數量': HC_totals[-1],
        '內文留言': ''
    }
    results.append(result)

def extract_comments():
    try:
        # 定位留言元素
        comment_locator = (By.CSS_SELECTOR, 'div[data-key^="comment-"] > div > div > div > div > div > div > span')
            # 供搜索用：
            # div.d_b1_1w.d_lc_1u.c19xyhzv
            # div.d_7v_5.d_cn_1t.d_gk_31.c201erw
        WebDriverWait(driver, 1).until(EC.presence_of_element_located(comment_locator))
        
        # 找到留言元素
        comments = driver.find_elements(*comment_locator)

        # 提取留言文字內容並添加到集合中
        for comment in comments:
            commentss = comment.get_attribute('innerText')
            commentsss = commentss.replace('\n', '')
            tmpcomment.add(commentsss)
        
    except Exception as e:
#         print(f"An error occurred: {e}") # 這邊是可以幫助你檢查發生了什麼錯誤
        commentssss = 'None'
        tmpcomment.add(commentssss)
        
def expand_comments():
    while True:
        # 定位所有「查看其他留言」按鈕
        btn_locator = (By.CSS_SELECTOR, 'div[data-key^="subCommentToggle-"] > div > div > div > button')
        btns = driver.find_elements(*btn_locator)

        has_more_buttons = False  # 用來檢查是否還有更多的按鈕
        for btn in btns:
            str_text = str(btn.text)

            # 只點擊包含「查看」的按鈕，跳過「隱藏留言」的按鈕
            if "查看" in str_text:
                # 滾動到按鈕可見位置
                driver.execute_script("arguments[0].scrollIntoView(true);", btn)
                sleep(1)  # 等待頁面更新

                # 嘗試使用 WebDriverWait 等待按鈕可以被點擊
                WebDriverWait(driver, 10).until(EC.element_to_be_clickable(btn))

                try:
                    # 嘗試正常點擊
                    btn.click()
                except:
                    # 使用 JavaScript 點擊按鈕，避免被其他元素遮擋
                    driver.execute_script("arguments[0].click();", btn)

                sleep(1)  # 等待留言加載
                has_more_buttons = True  # 標記還有更多按鈕需要點擊

        # 如果這次迴圈中沒有點擊到新的按鈕，跳出迴圈
        if not has_more_buttons:
            break


# 視窗滑到最後時，幫助你再滑動視窗
def scroll():
    innerHeight = driver.execute_script(
        'return window.document.documentElement.scrollHeight;'
    )
    innerHeight += 500
    driver.execute_script(f'''
        window.scrollTo({{
            top: {innerHeight}, 
            behavior: 'smooth' 
        }});
    ''')

# 每滑動一次，就獲取一次所有頁面資料
def scroll_and_extract_data():
    innerHeight = 0
    offset = 0
    count = 0
    limit = 1

    while count <= limit:
        sleep(1)
        expand_comments()
        extract_comments()
        offset += 500
        
        # 使用JavaScript執行滑動操作，將頁面滑動
        driver.execute_script(f'''
            window.scrollTo({{
                top: {offset}, 
                behavior: 'smooth' 
            }});
        ''')
        innerHeight = driver.execute_script(
            'return window.document.documentElement.scrollHeight;'
        )
        # 如果滑動的位移超過或等於頁面的高度，則增加計數
        if offset >= innerHeight:
            count += 1

# 存取資料成 json檔
def savejson(filename):
    link_name = filename.replace("/", "_")
    with open(f'{folderPath_w}{link_name}.json', "w", encoding='utf-8') as file:
        file.write(json.dumps(
            results, ensure_ascii=False, indent=4))
    print("檔案", filename, "存好了")

#%%
tmpcomment = set()
results = []
current_height = 0
folderPath_w = 'C:/Users/323be/Documents/just_for_try/'

#%%
all_links = open('C:/Users/323be/Documents/just_for_try/dcard_link.txt',encoding="UTF-8").readlines()

#%%
for link in all_links:
    try:
        link = link.strip()
        url = "https://www.dcard.tw"
        driver.get("{}{}".format(url,link))
    #     print("{}{}".format(url,link))
        sleep(random.randint(1, 3))
        extract_post()
        scroll_and_extract_data()
        scroll()
        scroll_and_extract_data()
        for commentt in tmpcomment:
            commentt.replace('', 'None')
            results[0]['內文留言'] = results[0]['內文留言'] + commentt
        tmpcomment = set()
        savejson(link)
        
        # 清空 tmpcomment 和 results
        tmpcomment.clear()
        results.clear()
        tmpcomment = set() # 將 tmpcomment 還原成空的 set
    except:
        print(link,"失敗")
        continue
        
#%%
driver.quit()
```