---
# System prepended metadata

title: LAB 12 網頁爬蟲
tags: [計算機概論]

---

# LAB 12 網頁爬蟲
助教:威仁
## 什麼是爬蟲
網絡爬蟲（Web Crawler），又稱為蜘蛛（Spider）或機器人（Bot），是一種用來自動化瀏覽和收集網路資訊的程式。爬蟲會依據設定好的規則，自動地從一個網站抓取資料，並進行儲存或分析。其運作原理類似於搜尋引擎的索引過程：爬蟲會訪問一個網站的網頁，擷取其中的內容，並追蹤該頁面的連結以訪問更多的頁面。

* 爬蟲的運作方式
    1. 起始網址（種子 URL）：爬蟲從一個或多個初始網頁開始，下載這些網頁的內容。
    1. 解析網頁：爬蟲解析下載的網頁，從中提取有用的資訊，如文本、圖像、連結等。
    1. 追蹤連結：爬蟲識別並追蹤網頁中的連結，將這些連結加入待抓取的清單，並繼續抓取。
    1. 重複過程：爬蟲重複上述步驟，直到抓取到足夠的資料或達到預定的條件。
* 應用範圍
    * 搜索引擎：構建和更新搜尋引擎的索引庫。
    * 資料挖掘與分析：收集大規模資料進行市場分析、競爭對手分析等。
    * 價格比較與監控：監控網站上的價格變動，實現自動化比較和提醒。
* 法律與道德考量
在編寫和使用爬蟲時，需遵守相關法律和道德規範。例如，遵循網站的 robots.txt 規範，避免對網站造成過大的負擔或違反其服務條款。

## 套件
* Requests
    * 處理 HTTP 請求
* Beautiful Soup
    * 解析和提取 HTML/CSS 內容
```python=
!pip install request beautifulSoup4
!pip imstall selenium
```
## 爬蟲步驟
1. 抓網頁
    ```python=
    import requests

    # 發送HTTP GET請求到指定的URL
    r = requests.get("http://www.es.ncku.edu.tw/esncku/zh/")
    # 自動檢測並設置正確的編碼
    r.encoding = r.apparent_encoding
    # 輸出HTTP狀態碼
    print(r.status_code) #200
    # 檢查請求是否成功（狀態碼為2XX時返回True）
    print(r.ok) #True
    # 輸出回應標頭中的Content-Type
    print(r.headers['content-type']) #text/html
    # 輸出請求的編碼方式
    print(r.encoding) #utf-8
    # 輸出回應的二進制內容
    print(r.content)
    # 輸出回應的文字內容
    print(r.text)
    ```

3. 解析網頁
    ![image](https://hackmd.io/_uploads/S1cba9ZhR.png)

4. 階層標籤
    ![image](https://hackmd.io/_uploads/rykQCcbhC.png)
    
6. 搜尋標籤find() and find_all()
    * ==find(tag, attributes, recursive, string, keywords)==
    find() 回傳第一個找到的區塊
    * ==find_all(tag, attributes, recursive, string, limit, keywords)==
    find_all() 會回傳一個 list，包含所有符合條件的區塊

## 範例
* 爬蟲目標:[成功大學註冊組](https://reg-acad.ncku.edu.tw/?Lang=zh-tw)，項目分類清單中的"學生線上服務"
![image](https://hackmd.io/_uploads/r1MPcoW2R.png)
* 程式碼:
    ```python=
    import requests
    from bs4 import BeautifulSoup as bs

    # 發送 GET 請求到指定網址
    r = requests.get("https://reg-acad.ncku.edu.tw/?Lang=zh-tw")
    # 解析 HTML 內容，將其轉換為 BeautifulSoup 對象
    soup = bs(r.text,"html.parser")
    # 找到外層的 <ul> 元素
    ul_outer = soup.find('ul',{'class':'cgmenu list-group dropmenu-right'})

    #將Title"學生線上服務"輸出
    li_outer = ul_outer.find('li',{'id':'Cg_7569'})
    title = li_outer.find('a')
    print(title.text)

    #將項目內的元素抓出並輸出
    ul_inner = ul_outer.find('ul',{'id':'Cgl_7569'})
    li_inner = ul_inner.find_all('li')
    for row in li_inner:
        print(row.text)
        link = row.find('a', href = True).get('href')
        print(link)
    ```
* 結果:![image](https://hackmd.io/_uploads/Bk6g2oZhC.png)

## GET POST
在上一個LAB提到GET與POST的差別

|  GET    |  POST  |
| ---- | ---- |
|   發送requests，Server 回傳資料。   |  發送requests，並附帶資料，Server 回傳資料。  |
| requests 網址會隨著不同的網頁要求而改變。 | requests 網址不會改變，但是附帶的資料，隨著使用者不同的網頁要求而改變。 |

* 那使用GET方法去爬蟲，爬到POST會發生什麼事
https://pythonscraping.com/files/form.html
![image](https://hackmd.io/_uploads/ByK9lhWnC.png)


* 怎麼用POST方法爬蟲呢?
    爬蟲前先看看網頁怎麼傳輸資料的![image](https://hackmd.io/_uploads/B1q1b2-3C.png)
    * 傳送firstname, lastname 資料給server

* 改用post方法![image](https://hackmd.io/_uploads/Sy0-bnW3A.png)
* 再使用Beautiful Soup解析網頁
    ![image](https://hackmd.io/_uploads/rkJV-hW2R.png)

## Login Cookie
網頁的 Cookie 是什麼？
Cookie 是一小段由伺服器生成並儲存在用戶瀏覽器中的文本檔案，用於存儲關於用戶的狀態資訊。當用戶再次訪問相同的網站時，瀏覽器會將 Cookie 中的數據發送回伺服器，以實現某些功能，例如記住用戶偏好、追蹤用戶行為等。
http://pythonscraping.com/pages/cookies/login.html
![image](https://hackmd.io/_uploads/BJgZ7hb30.png)
![image](https://hackmd.io/_uploads/Sk5bmhWhA.png)

瀏覽狀態儲存在cookies
![image](https://hackmd.io/_uploads/HJ2Bw3-2A.png)

使用cookies 維持登入狀態
![image](https://hackmd.io/_uploads/rkekcnb2C.png)

在網頁爬蟲時，使用session紀錄cookies
![image](https://hackmd.io/_uploads/H1FHY3b3C.png)

Session vs. Cookie
| 特性 | Session | Cookie |
| ---- | ------- | ------ |
|   定義   |   一種伺服器端的狀態管理技術，用於存儲用戶會話數據。      |   一種用戶端的狀態管理技術，用於存儲小型數據。     |
| 存儲位置 | 數據存儲在伺服器端，由伺服器生成 Session ID。  |數據存儲在用戶端瀏覽器中。  |

## 動態網頁爬蟲
在前面介紹的方法都只能用來爬取網頁內的靜態內容
http://pythonscraping.com/pages/javascript/ajaxDemo.html
想爬的內容
![image](https://hackmd.io/_uploads/rJpZjAWhA.png)
卻爬到
![image](https://hackmd.io/_uploads/rksgiCW2A.png)

>requests 無法執行 JavaScript指令 讓頁面產生變化

[小工具](https://chromewebstore.google.com/detail/quick-javascript-switcher/geddoclleiomckbhadiaipdggiiccfje?pli=1)
透過此擴充功能可以檢視關掉JavaScripts的網頁內容

那要如何爬取動態的網頁呢?
* 使用Selenium
    * 工具簡介
    • Selenium 最初是網頁自動測試工具，把瀏覽器自動化，讓瀏覽器載入頁面，以便取得相關資料。
    • Selenium 經常被拿來處理動態網頁爬蟲、擷取畫面、執行動作
    • 因為是模擬操作瀏覽器，速度上會比靜態網頁慢
* 安裝程式庫
    ```python=
    pip install selenium
    ```
* 程式碼
    * 範例1
        ```python=
        from bs4 import BeautifulSoup as bs
        from selenium import webdriver
        import time

        #透過指定的瀏覽器 driver 打開 chrome
        driver = webdriver.Chrome()

        #將視窗最大化
        driver.maximize_window()

        #透過瀏覽器取得網頁
        driver.get('https://pythonscraping.com/pages/javascript/ajaxDemo.html')

        #等待3秒
        time.sleep(3)

        #page_source取得網頁內容
        bsobj = bs(driver.page_source,'html.parser')
        print(bsobj.div.text)

        #將瀏覽器關閉
        driver.close()
        ```
        ![image](https://hackmd.io/_uploads/SJtJy1z3C.png)
    * 範例2(不出現視窗)
        ```python=
        from bs4 import BeautifulSoup as bs
        from selenium import webdriver
        from selenium.webdriver.chrome.options import Options
        import time

        # 不希望出現視窗
        chrome_options = Options()
        chrome_options.add_argument("--headless")
        driver = webdriver.Chrome(options=chrome_options)

        driver.get('https://pythonscraping.com/pages/javascript/ajaxDemo.html')
        time.sleep(3)

        bsobj = bs(driver.page_source,'html.parser')
        print(bsobj.div.text)

        driver.close()
        ```
    * 等待WAIT
        * 影響網頁讀取時間
            * 網頁伺服器負載
            * 網路速度
            
        ```python=
        #不停地等待，直到按鈕出現
        from selenium import webdriver
        from selenium.webdriver.chrome.options import Options
        from selenium.webdriver.common.by import By
        from selenium.webdriver.support.ui import WebDriverWait
        from selenium.webdriver.support import expected_conditions as EC 

        chrome_options = Options()
        chrome_options.add_argument("--headless")
        driver = webdriver.Chrome(options=chrome_options)

        driver.get('https://pythonscraping.com/pages/javascript/ajaxDemo.html')

        try:
            element = WebDriverWait(driver,10).until(EC.presence_of_element_located((By.ID,'loadedButton')))
        finally:
            print(driver.find_element(By.ID,'loadedButton').text)
            driver.close()
        ```
        * WebDriverWait(driver, 10)
         • 在拋出TimeoutException異常之前，將等待10秒或者在10秒內發現了要查找的元素
        * Expected_conditions
            [可預期條件](https://www.selenium.dev/selenium/docs/api/py/webdriver_support/selenium.webdriver.support.expected_conditions.html)
        * By
            [定位器](https://www.selenium.dev/documentation/webdriver/elements/locators/)
            ![image](https://hackmd.io/_uploads/rkePB1MnA.png)

* 傳送按鍵給瀏覽器
    ```python=
    #send_key(),click()
    import time
    from IPython.display import display, HTML
    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
    from selenium.webdriver.common.keys import Keys

    chrome_options = Options()
    #chrome_options.add_argument("--headless")
    driver = webdriver.Chrome(options=chrome_options)

    driver.get('https://www.facebook.com/dcard.tw/')

    #將跳出的LOGIN提示點掉
    try:
        # 等待元素加載並定位
        close_button = WebDriverWait(driver, 10).until(
            EC.element_to_be_clickable((By.XPATH, '//div[@aria-label="關閉" and @role="button"]'))
        )

        # 點擊該元素
        close_button.click()

    except Exception as e:
        print("元素未找到或無法點擊: ", e)

    element = driver.find_element(By.CSS_SELECTOR,'body')

    #滑到最底
    element.send_keys(Keys.END)
    time.sleep(1)
    ```


## 防爬蟲 headers
取得headers "User-Agent"
Chrome 按下F12打開開發者工具
找到Network的資訊頁面
點一個找到"User-Agent"
![image](https://hackmd.io/_uploads/BJyZxFn4kl.png)


## Lab12
作業繳交注意事項：
1. 基礎題與加分題直接用一個資料夾上傳至github，資料夾名: 「學號_Lab12」。
2. 程式碼需要註解，server需要在樹梅派上面執行。
3. 將程式碼上傳且需要包含html檔案，
    基礎題py檔取名`Lab12.py`而加分題為`Lab12_plus.py`，
    html檔一樣放在`templates`中，加分題為`Lab12_plus.html`。
5. 本次作業截圖較多，請將截圖都放在PDF中，取名為`Lab12.pdf`和`Lab12_plus.pdf`，存放在學號_Lab12資料夾中，並標明題號，若未標明將會扣分，能夠讓人看得懂就可以（每題扣10分）。
6. 作業繳交時間：12/30 12:00 不開放補交請在時限內繳交作業。



## Lab12 基礎題
註 (給分方式) : 依配分扣除，小錯誤*0.9
* 對 https://tw.stock.yahoo.com/ 等多種股票網頁，透過爬蟲獲取當時該股票價格
1. 有一個輸入股票號碼的地方，透過該股票號碼找到該股票頁面 (30%)
3. 顯示該股票頁面URL (20%)
4. 顯示該股票名稱與該股票的價格 (50%)
![image](https://hackmd.io/_uploads/S1aaMFnVkl.png)
請截圖三個不同的股票(需要選擇一隻當日上漲與當日下跌的股票)，
確定是否都能正常抓到。

## Lab12 加分題
註 (給分方式):總分低於45分>>>不加分 超過45分>>>+1分 90分以上>>>+2分 ，每個錯誤扣5分。
將 Lab12 基礎題透過Flask，架設在自己的網頁上透過瀏覽器進行爬蟲獲取資訊。
Server一樣要架設在樹梅派上!


| 配分    | API名稱 | 功能                                                                                                                                                                                                                                                          |
| --- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|  10%   | /       | 接收request後， 回傳 Lab12_plus.html 至前端呈現首頁畫面，Lab12_plus.html 會包含文字與表單，title為 "Lab12_plus"，建立表單，表單為Stock Data                                                                                                                   |
|  40%   | /stock  | 依照上Lab12_plus.html，在表單輸入股票號碼，按下送出按鈕後，切換到/stock路由，在Server端的terminal顯示使用者輸入的股票號碼與搜尋到的股票名稱和價格，並回傳股票名稱和價格用json格式給前端                                                                       |
|  50%   | /reset  | 使用動態路由的方式來要求清空後端的字典，輸入'y'時，後端會執行清空字典的動作，並顯示在terminal，並回傳reset.html至前端呈現畫面，畫面內需包含文字以及回首頁的超連結，其中文字為 " Reset ! " ，若輸入'n'或其他則會依然顯示首頁畫面，且current data應該會保持不變 |

作業截圖呈現
* 進入首頁
    ![image](https://hackmd.io/_uploads/SJ1EtKnV1g.png)
* 輸入資料後，terminal的顯示![image](https://hackmd.io/_uploads/S1oFYt3EJe.png)
* 輸入資料後，current data 顯示現在後端已存放之資料
![image](https://hackmd.io/_uploads/ryS9FKnNkl.png)

* 在瀏覽器輸入 " http://ip:3000/reset/y " 發送要求，讓後端清除已存放之資料，前端會顯示 Reset以及回首頁的超連結
![image](https://hackmd.io/_uploads/S1ex5FhEyl.png)
伺服器端顯示空的
![image](https://hackmd.io/_uploads/H1U-5Y2Nke.png)



* Hint
如何回傳後端字典資料，請參考以下資料 :
data_dict_string = json.dumps(data_dict,ensure_ascii = False) #將字典資料轉換成 json 格式的字串