# 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 格式的字串