# 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. 解析網頁

4. 階層標籤

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),項目分類清單中的"學生線上服務"

* 程式碼:
```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)
```
* 結果:
## GET POST
在上一個LAB提到GET與POST的差別
| GET | POST |
| ---- | ---- |
| 發送requests,Server 回傳資料。 | 發送requests,並附帶資料,Server 回傳資料。 |
| requests 網址會隨著不同的網頁要求而改變。 | requests 網址不會改變,但是附帶的資料,隨著使用者不同的網頁要求而改變。 |
* 那使用GET方法去爬蟲,爬到POST會發生什麼事
https://pythonscraping.com/files/form.html

* 怎麼用POST方法爬蟲呢?
爬蟲前先看看網頁怎麼傳輸資料的
* 傳送firstname, lastname 資料給server
* 改用post方法
* 再使用Beautiful Soup解析網頁

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


瀏覽狀態儲存在cookies

使用cookies 維持登入狀態

在網頁爬蟲時,使用session紀錄cookies

Session vs. Cookie
| 特性 | Session | Cookie |
| ---- | ------- | ------ |
| 定義 | 一種伺服器端的狀態管理技術,用於存儲用戶會話數據。 | 一種用戶端的狀態管理技術,用於存儲小型數據。 |
| 存儲位置 | 數據存儲在伺服器端,由伺服器生成 Session ID。 |數據存儲在用戶端瀏覽器中。 |
## 動態網頁爬蟲
在前面介紹的方法都只能用來爬取網頁內的靜態內容
http://pythonscraping.com/pages/javascript/ajaxDemo.html
想爬的內容

卻爬到

>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()
```

* 範例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/)

* 傳送按鍵給瀏覽器
```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"

## 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%)

請截圖三個不同的股票(需要選擇一隻當日上漲與當日下跌的股票),
確定是否都能正常抓到。
## 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應該會保持不變 |
作業截圖呈現
* 進入首頁

* 輸入資料後,terminal的顯示
* 輸入資料後,current data 顯示現在後端已存放之資料

* 在瀏覽器輸入 " http://ip:3000/reset/y " 發送要求,讓後端清除已存放之資料,前端會顯示 Reset以及回首頁的超連結

伺服器端顯示空的

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