<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 ゴシック", "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卻被鎖住了,讓我們沒有辦法方便的使用

----

Cloudflare是一個提供網站安全性提升的服務提供商,它提供了多種方法來識別並阻止機器人(包括惡意機器人)訪問你的網站。
- 使用 CAPTCHA 驗證:要求用戶在訪問頁面時要進行人類驗證。這可以幫助阻止自動化的機器人。
- 設置 IP 阻止規則:封鎖特定機器人的 IP 地址。

---
#### 可如果今天網站沒有提供api怎麼辦?
# 那我們只能土法煉鋼,<br>找到網頁中,我們要找的目標資料,複製並儲存下來。
----
## 瀏覽網頁的過程
- 使用者在瀏覽器中輸入路徑,向伺服器發送請求HTML文件
- 伺服器會回傳對應的HTML文件給瀏覽器
- 瀏覽器解析HTML並產生內容、解析CSS產生排版與樣式、解析JavaScript並產生網頁元素與使用者的互動機制…等等
- 最終,生成我們看到的網頁畫面
----
## 網頁由HTML、CSS、JavaScript所組成
- HTML(HyperText Markup Language,超文本標記語言):用來建構網頁的標記式語言,而非程式。
- CSS (Cascading Stylesheets,階層樣式表):一種風格頁面語言,負責建構網頁外觀。
- JavaScript:可以讓網頁中實現複雜的功能,經常用在呈現網頁動態效果。負責描述網頁如何與用戶互動。
----
## 什麼是HTML?
HTML 是一種標記語言(markup language),而非一般熟知的程式設計語言。它主要用來告訴瀏覽器該如何呈現你的網頁。
HTML 由一個個元素所組成,而元素中包含了標籤(tag)與內容。

- 起始標籤:角括弧,也就是符號「`< >`」,裡面需放入元素名稱,如上面的例子「`<p>`」。起始標籤代表這個元素從這裡開始。
- 結束標籤:與起始標籤一樣,只是在元素名稱前面多了個前置斜線「`/`」。內容的最後加上結束標籤,代表這個元素的尾端。
- 內容:這個元素的內容,以上面的例子來說,內容就是這句文字。
- 元素:由起始標籤、結束標籤、內容所組成。
----
1. 元素中可以添加「屬性(Attribute)」

屬性能提供更多的資訊,可以利用屬性設定這個元素的色彩、對齊方式、圖表的格線。
<br>
2. 元素裡面還可以放進其他元素

這個句子:「我的貓脾氣很暴躁」,若你想強調「very」,我們就可以把「very」用粗體的元素顯示。
<p> My cat is <strong> very </strong> grumpy.</p>
----
## HTML文件基本結構

----
## 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>而這些物件最終會形成一個樹狀結構。

幫助訪問、查找和修改網頁中的元素,因而可以透過程式語言來改變網頁的內容。
----
## 節點
如果我們將一個網頁比喻為一棵樹,那麼每個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」或「檢視原始碼」。

----
## 補充: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,然後將其發送給瀏覽器。
- 例子:社交網站、在線應用程式等。通常是動態網頁,因為它們需要根據用戶的操作和數據來實時更新內容。
----
## 靜態網頁

----
## 動態網頁

---
# 網頁元素定位
## 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 外觀

- 與 HTML 不同,屬性與值在兩者是不一樣的描述參數:

----
#### 了解CSS與HTML的關係,要修改目標的外觀,我們要能「定位」目標
2. CSS Selector 介紹
可以簡單理解為:element name(大範圍)、class name(複數範圍)、id name(單一範圍),
- `Element Type Selector`:也就是指定常見的標籤作為定位對象
- 例如:`<h1>、<p>、<div>`等
- `Class Selector`:對標籤加上 `class` ,讓特定標籤形成一個群組。而在CSS編譯器裡往往用`「.」`來指向`class`
- 例如:
----
2. CSS Selector 介紹
- ID Selector:對指定標籤加上 `id` ,`class`可以提供給很多 HTML 標籤使用,但`id`只能有一個。而在CSS編譯器裡往往用`「#」`來指向`id`
- 例如:
----
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通常語法較為簡潔,運行速度更快些

---
## Selenium
對於爬蟲來說,如果爬取動態網頁內容,只會得到一堆JavaScript程式碼,因為網頁內容需要透過執行JavaScript程式碼後產生。
**Selenium**可以說是自動化控制網路瀏覽器工具,用來**模擬出使用者在瀏覽器的所有操作行為** (點擊按鈕、輸入帳號密碼、捲動捲軸...等),因此除了爬蟲的應用,也常作為「自動化測試」使用的工具,在網站開發完成後,透過自動化的腳本測試所有功能是否正常。

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

---
# 使用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輔助執行

----
### 4. 確認並更新Chrome
在Chrome網址中輸入`chrome://version` 進行確認
點選右上角設定 > 說明 > 關於 Google Chrome ,進行更新

----
### 5. 下載ChromeDriver
https://googlechromelabs.github.io/chrome-for-testing/
選擇stable的版本來下載

**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壓縮檔,放到這裡,並且把名字改成剛剛下載的版本

### 這個步驟非常重要↓
將改好的資料夾(118.0.5993.70,請用最新的版本名稱)中的LICENSE.chromedriver**刪除**

---
## 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時很重要的是,需要了解「如何在頁面上查找元素」。也就是如何定位出需要的資料位置。
#### 開發者模式,可以幫助我們查找元素

----
## 定位到網頁上某個物件-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()
```
{"slideOptions":"{\"theme\":\"white\",\"spotlight\":{\"enabled\":false},\"transition\":\"slide\",\"slideNumber\":true}","description":"自動化爬取dcard網站資料","title":"Dcard網路爬蟲","contributors":"[{\"id\":\"23b124fd-e00e-4e8a-b739-5d9c10ed8241\",\"add\":60461,\"del\":34731}]"}