More Related Content Similar to Python crawling tutorial (20) Python crawling tutorial7. 爬蟲目的
● 做深度學習的時候,缺 training data …
● 做文字探勘的時候,缺文本 data …
● 做輿情分析的時候,缺輿論 data …
● 做第三方平台要比價的時候,缺即時價格資訊 …
在做各種實驗或是分析的時候,總是會遇到缺少資料的時候
不論是否要即時性,或是要大量資料,都有爬蟲的需求
9. 基本爬蟲流程
1. 取得網站的 HTML
○ 透過 requests 送出請求取得 HTML
2. 解析資料以取得目標資訊
○ 透過瀏覽器的開發者工具,觀察目標資訊位置
○ 透過 BeautifulSoup 解析 HTML
3. 重複以上過程
12. HTML (HyterText Markup Language)
HTML 又稱做超文件標記語言,是由一堆預定義好的元素組成階層式架構的文件
元素的組成包含了
● 標籤
● 屬性
● 內容
<標籤 屬性>
內容
</標籤>
13. HTML 結構
前面提到 HTML 是元素組成階層式架構的文件,
而元素以這種方式組合的樹狀結構,我們又稱為 DOM (Document Object Model)
html
head body
<meta charset="utf-8" />
<title>Page Title<title/>
<h1 id="title">Header<h1/>
14. 元素組成
我們說 HTML 相當於網頁的骨幹,代表網頁的組成架構
而這就是由 DOM 樹決定架構,加上元素的標籤來決定段落用途
元素的組成包含了
● 標籤:通常成對出現,說明元素定義
● 屬性:標籤可以有多種屬性,說明元素性質
● 內容:通常是顯示的文字,說明元素的值
<h1 id="title"> Header <h1/>
還會有各自屬性的值,
有可能代表行為或是外觀
19. 開始寫爬蟲之前
● Github
○ 許多爬蟲程式在 Github 上都有,有時候可以不用全部都重頭開始自己寫
○ e.g. PTT Crawler
● API
○ 許多公司提供 API 讓使用者可以在遵循公司規定的情況下拿到整理後的資料
○ e.g. Facebook Graph API,Google Places API
● 道德規範
○ 爬蟲是一個不斷送請求的過程,而頻繁大量的請求會對網站伺服器造成負擔
○ 雖然非強制性,但請大家練習的時候請遵守規範
○ robots.txt 規範通常會放在網站的根目錄 (e.g. https://www.facebook.com/robots.txt )
○ 詳細的規範可以參考 wiki 與 google 文件
21. GET 請求
GET 請求會把資料放在 header 傳送,就像寄明信片一樣
資料很容易被看見,所以其實會有安全性的問題
一般操作會使用 GET,但是帳號密碼等隱私性高的資料一般不會用這種方式實作
我們要傳送到對方伺服器的資料
原網址:https://www.mywebsite.com/
請求後網址:https://www.mywebsite.com/form?name=afun
22. POST 請求
POST 允許在 body 裡放資料,就像是放在信封裡的信件
比起 GET 相對安全,可以傳送的資料也更多
原網址:https://www.mywebsite.com/
請求後網址:https://www.mywebsite.com/ 網址不會改變
補充說明:GET 與 POST 底層都是以 TCP 實作,所以其實這兩者基本上差不多,只
是透過不同的標籤 (HTTP method) 決定實作細節
24. 透過比較高階的套件 requests,
可以很簡單的實作 HTTP method (GET/ POST)
import requests
url = 'http://research.sinica.edu.tw/'
response = requests.get(url) # GET 請求
response.encoding = 'utf-8' # 解決中文問題
print(response.text) # HTML 架構
程式 - 發送 GET 請求
25. 程式 - 發送 GET 請求
如果成功收到回應,透過回應的 text 可以取得 HTML 檔案的字串
原始檔案很亂,需要透過解析器 (parser) 幫我們找到有用的資訊
27. 程式 - 解析網頁
BeautifulSoup 背後的解析器有多種選擇,因為速度快跟容錯能力高的優點,
我們這邊選擇使用的是 lxml
from bs4 import BeautifulSoup
# 假設已經送出請求拿到回應 resp
soup = BeautifulSoup(resp.text, 'lxml')
28. 程式 - 解析網頁
當我們要找尋目標元素時,通常會根據標籤或是屬性來定位元素 (官方文件)
soup.p # 尋找網頁中第一個 p tag
soup.find('p') # 尋找網頁中第一個 p tag
soup.find_all('p') # 尋找網頁中所有 p tag
# 尋找網頁中所有 id 是 main 的 p tag
soup.find_all('p', {'id': 'main'})
soup.p.img # 尋找網頁中第一個 p tag 底下的 img tag
soup.p['style'] # 取得 p tag 中 style 屬性的值
soup.p.text # 取得 p tag 中的文字
36. 程式 - 發送 POST 請求
與 GET 請求的過程大同小異,只要記得在送出請求的時候附上相關資訊
以高鐵時刻表查詢為例,我們現在知道要附上的欄位資訊
但是像 StartStation 是一連串很像亂碼的片段,
我們必須去觀察 HTML 架構找出相關資訊的位置
37. 程式 - 發送 POST 請求
查找方式可以透過開發者工具的 Inspect 功能找出元素在 HTML 內的位置
然後透過觀察就會發現疑似亂碼的片段其實在元素的屬性裏面就有
38. 程式 - 發送 POST 請求
與 GET 請求的過程大同小異,只要記得在送出請求的時候附上相關資訊
最後在把回傳的 HTML 傳入 parser 定位找出查詢結果的資訊
import requests
url = 'https://www.thsrc.com.tw/tw/TimeTable/SearchResult'
form_data = {
'StartStation': '...', # 填上相關站別的值
'EndStation': '...', # 填上相關站別的值
… # 填上其他資訊
}
response = requests.post(url, data=form_data) # POST 請求
48. 程式 - 下載圖片
下載圖片有很多種方式,我這邊以 requests 套件當作範例
# 假設我們已經有圖片網址 image_url
resp = requests.get(image_url, stream=True)
with open('logo.png', 'wb') as f:
# receive 10240 bytes per chunk
for chunk in resp.iter_content(chunk_size=10240):
f.write(chunk)
程式執行結束後,就會把圖片下載下來並存成 logo.png
55. 程式 - 圖片格式資訊
為了要用正確的副檔名存檔,我們必須下載下來之後先判斷圖片格式
這邊可以藉由 PIL.Image 來判斷格式
from PIL import Image
resp = requests.get(image_url, stream=True)
image = Image.open(resp.raw)
print(image.format) # e.g. JPEG
# 假設我們重新組合圖片檔名與副檔名 logo.jpeg 之後
image.save('logo.jpeg') # 儲存圖片
60. 定位策略
我們知道 BeautifulSoup 可以在 tag 裏面再次搜尋,
一般的策略都是透過定位目標 tag 的上一層,藉此縮小搜尋範圍再搜尋
可是還是會有跟目標 tag 同一層同時存在其他不需要的超連結
這種情況通常就需要額外花時間去解析結構
檔案敘述 1
檔案敘述 2
檔案敘述 3
檔案敘述 4
檔案敘述 5
e.g. 目標僅有 PDF 檔案
61. 程式 - 定位策略
由於 HTML5 的架構類似家族樹的概念,<a> tag 可以視作 <img> tag 的 parent
因此除了由外而內縮小範圍尋找,我們也可以透過定位 PDF icon 由內往外找
images = soup.find_all('img', {
'src': re.compile('application-pdf.png')
}
for image in images:
# 透過 parent method 尋訪 img tag 的上一層 tag
print(image.parent['href'])
62. 檔案的絕對路徑與相對路徑
前面提到圖片的時候我們知道圖片路徑在 <img> tag 的 src 屬性
而現在我們知道超連結檔案的檔案路徑在 <a> tag 的 href 屬性
上述兩者皆代表了檔案位置,同樣都有以下這兩種表達方式
● 絕對路徑,e.g. http://exam.lib.ntu.edu.tw/graduate
● 相對路徑,e.g. /exam/sites/all/themes/ntu/logo.png
66. 轉換路徑 - 相對路徑轉為絕對路徑
● 相對路徑:/exam/sites/all/modules/pubdlcnt/pubdlcnt.php
● 絕對路徑:http://exam.lib.ntu.edu.tw/graduate
組合後的路徑
:http://exam.lib.ntu.edu.tw/exam/sites/all/modules/pubdlcnt/pubdlcnt.php
68. 解析 URL
透過原始碼可以得知 urljoin 內部是透過 urllib.parse.urlparse 實作
將 URL 拆解成數個有意義的片段再去組合
<scheme>://<netloc>/<path>;<params>?<query>#<fragment>
70. 基本反爬蟲 - 身份識別 User-Agent
當我們對網站送出請求時,其實會送出很多其他的資訊,包含身份識別
透過程式送的話,一般來說不會包含身份識別
沒有身份
拒絕回傳
請求查看
html
請求查看
html
83. 遍歷網站 - 概念 (1)
第一個非常直覺的想法是透過 loop
將所有網址的超連結都送出 request
history
root
index2
index3
從 root 開始搜尋
超連結
搜尋到新網頁
搜尋到新網頁
84. 遍歷網站 - 問題 (1)
但這樣無法發現其他網頁的超連結
history
root
index2
index3
從 root 開始搜尋
超連結
搜尋到新網頁
搜尋到新網頁
hidden 僅能從 index3
連結
85. 遍歷網站 - 概念 (2)
為了解決遍歷其他網頁裡的超連結,有兩個很直覺的作法
● recursive 遍歷
● 建立清單紀錄待尋訪的網址,再對待尋訪清單的網址做 loop
86. 遍歷網站 - 問題 (2)
但是現代網站通常都會有「導覽列」的設計,而這很容易會造成無窮迴圈
87. 遍歷網站 - 概念 (3)
為了預防無窮迴圈的問題,通常還會紀錄我們尋訪過的網址
設想多一點的會連網頁上次更改的時間 Last Modified 或是 ETag 等資訊都紀錄
相關細節可以參考循序漸進理解 HTTP Cache 機制這篇部落格文章
91. 網址以外的合法超連結
超連結 <a> tag 的定義並非是一串網址,所以實際爬蟲時
有可能會有不適合送出請求的超連結
除了前面提到的相對路徑以外,我們更應該要注意 <a> tag 中的 href 屬性,
而且 urljoin 的回傳結果並不會過濾
92. 合法超連結
參考 w3schools 的文件
href value 敘述 範例
absolute URL 絕對路徑 https://gushi.tw
relative URL 相對路徑 /ex1/index1.html
anchor 其他 tag #top
other protocols 其他協定 mailto://example@gmail.com
JavaScript 程式碼 javascript:console.log("Hello")
94. 合法超連結 - Other Protocol
Protocol 稱為協定,在網路上各種不同的傳輸都有不同的協定
一般熟知的 http 與 https 就是其中一種,而其他協定也有可以是超連結目標
● 信箱 mailto://
● 文件 ftp://
● …
95. 合法超連結 - JavaScript
JavaScript 是現代網站很常使用的程式語言之一,
雖然不常在網頁中看到這種寫法,但有可能會是反爬蟲手段之一
將 <a> tag 透過 CSS 隱藏,然後在 href 屬性呼叫 JS function,
因為一般使用者看不到不會去點擊,
但如果透過程式對該超連結送出請求,即可判定為爬蟲程式將之拒絕
96. 程式 - 過濾超連結
我們這邊可以很簡單的透過 regular expression 跟 urlparse 來過濾
練習的話可以參考線上工具做測試
# anchor
check1 = re.match('.*#.*', href)
# protocol
check2 = re.match('[^http|https].*', urlparse(href).scheme)
# JS
check3 = re.match('javascript.*', href)
100. 過濾合法網址
我們前面有提到 URL 有其意義,可以使用 urlparse 解析並判斷
<scheme>://<netloc>/<path>;<params>?<query>#<fragment>
目標網站 http://foundation.datasci.tw/
目標網站粉絲專頁 http://www.facebook.com/twdsconf
上述情況可以透過 urlparse 直接判斷 <netloc> 決定是否爬蟲
114. Selenium 定位 Tag
我們透過 Selenium 拿到 render 好的 HTML 後有兩種選擇
● 跟之前一樣把 HTML 傳入 Beautifulsoup 定位
● 直接用 Selenium 定位
這邊會介紹 Selenium 的定位方式提供參考,
因為其中有 Beautifulsoup 沒有的定位方式
115. Selenium 定位 Tag
Selenium 的定位方式跟 Beautifulsoup 大同小異,
基本上只是 function name 與回傳物件不太相同而已
● find_element_by_id
● find_element_by_tag_name
● find_element_by_class_name
tag = driver.find_element_by_id('first')
print(tag)
117. XPath 與 Beautifulsoup 的定位
比較 XPath 的寫法與前面使用 Beautifulsoup 定位的差異
# Beautifulsoup
soup.find_all('div')[2].find_all('div')[0]
# Selenium XPath
driver.get_elements_by_xpath(
'/html/body/div[2]/div[0]'
)
118. 程式 - XPath
透過 Selenium 的 By 可以更簡單的更換定位方式
from selenium.webdriver.common.by import By
# 尋找所有 p tag
driver.find_elements(By.XPATH, '//p')
# 尋找任意 id 為 first 的 tag
driver.find_elements(By.XPATH, '//*[@id="first"]')
# 尋找任意 id 為 second 或 third 的 h2 tag
driver.find_elements(By.XPATH,
'//h2[@id="second"] | //h2[@id="third"]'
)
119. 取得 XPath
● 開發者工具
○ tag 按右鍵 > Copy > Copy XPath
○ Chrome 與 Firefox 都支援
透過這種方式你會取得只針對該 tag 的 XPath 寫法
如果你是希望根據條件取得所有 tag
可以考慮透過這種方式取得之後再修改
121. Responsive Web Design (RWD)
RWD 是響應式網站設計的縮寫,
設計目的是為了讓網站在不同大小的裝置都可以有很好的使用體驗
因此,在不同大小的裝置中,會呈現不一樣的 HTML
122. Responsive Web Design (RWD)
為了所見即所得,建議用 selenium 模擬瀏覽器時可以做視窗最大化
from selenium import webdriver
driver = webdriver.Chrome(path_to_webdriver)
driver.maximize_window()
123. 程式 - Implicit 請求等待
Selenium 的最常使用的等待有 Implicit 與 Explicit 兩種
Implicit Wait 通常適用於整個 Selenium 程式的預設等待,
當網頁元件暫時找不到時會進行等待,直到 timeout
● 程式執行期間,每次執行尋找指令時都會進行 n 秒等待
● Implicit wait 在瀏覽器開啟期間都會運作
driver = webdriver.Chrome(path_to_webdriver)
driver.implicity_wait(10) # 程式最多等 10 秒
124. 程式 - Implicit 請求等待
driver = webdriver.Chrome(path_to_webdriver)
driver.implicity_wait(10) # 最多等 10 秒
最多等 10 秒就
要回傳 HTML
2 秒就好了,馬
上回傳
請求查看
首頁
請求查看
首頁
首頁 首頁
125. 程式 - Implicit 請求等待
driver = webdriver.Chrome(path_to_webdriver)
driver.implicity_wait(10) # 最多等 10 秒
最多等 10 秒就
要回傳 HTML
10 秒到了,不
管怎樣先回傳
請求查看
首頁
請求查看
首頁
首頁 首頁
126. 程式 - Explicit 請求等待
另外一種等待是更明確的 Explicit Wait,宣告一個等待物件之後給予條件
Selenium 本身提供很多種條件供調用,但如果有需求也可以自定義條件
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
wait = WebDriverWait(driver, 10)
el = wait.until(EC.presense_of_element_located(
By.XPATH, '//div[@class="count" and text()]'
))
127. 程式 - Implicit 請求等待
Explicit wait 主要針對特殊元件,或是需要等待某種屬性的狀態等,在精確的等待與
判斷時較適用 e.g. 是否可以被點擊,是否可見
符合條件的 tag 好了
就回傳,最多等十秒
10 秒到了,不
管怎樣先回傳
請求查看
首頁
請求查看
首頁
首頁 首頁
128. Implicit 與 Explicit 的比較
● Implicit 實作較簡單,而且只要宣告一次
● Implicit wait 只作用在 find elements,無法檢查屬性
● Implicit wait 無法客製化,非預期的顯示時間可能會被忽略
● Implicit wait 沒有實際定義行為
● 推荐使用 Explicit wait,雖然實作上會比較複雜
● 不建議混用 Implicit 與 Explicit wait
參考來源:Implicit Wait 與 Explicit wait 的區別,Selenium with Python
131. 練習 (進階)
模擬 google search
● https://www.google.com.tw/
● 搜尋「人工智慧」
● 紀錄前兩頁搜尋結果的連結
Reference: Selenium Action
132. 練習 (進階)
簡易下載 reCAPTCHA v1 圖片
● https://www.google.com/recapt
cha/demo/recaptcha
● 使用 Selenium
● 使用 Implicit wait + time.sleep
● 檢查圖片格式
● 下載五張圖片
135. Graph API - 不用每次都硬爬一發
為了讓資料有更多的應用,部份公司會提供 API 給予整理好的資料
開發者可以很方便的調用資料,並且可以不用花太多時間擔心反爬蟲策略
Facebook 提供的 API 名稱為 Graph API
代表你的身份,提供給 Facebook 驗證用 搜尋條件
搜尋欄位 搜尋結果
136. 身份驗證 Access Token
1. Facebook 中的 Access Token 可以代表用戶,粉絲專頁,或是應用程式的身份
2. 我們在送出搜尋的請求時必須附上 Access Token 才會被視為是有效的請求
3. 根據身份的不同,能搜尋到的資料也不同 (權限管理)
一般來說這都會是短期 Token
但可以申請延長到 60 天
若是粉絲專頁甚至可以取得永久 token
137. 身份驗證 Access Token
Access Token 種類有好幾種,使用情境與機制等細節可以參考官方文件
一般來說要爬單一粉絲專頁的內容,使用短期 Access Token 應該就夠了
如果有使用永久粉絲專頁 Access Token 的需求可以參考這個部落格文章
139. Graph API 版本
API 有多種版本使用,幾乎所有的 API 請求都會往 graph.facebook.com 傳遞,除了
上傳影片的請求以外
● 每個版本至少在 2 年內都可以使用,並且不會修改
● 平台變更紀錄,版本詳細資訊
140. HTTP GET 請求
我們只需要對 Graph API 發出 HTTP GET 請求就可以讀取節點跟關係連線,
通常還要附上 Access Token 讓 Graph API 判斷權限,回傳相關結果
Graph API HTTP GET 所需資訊
● Graph API 版本 X.Y,e.g. 2.10
● 節點或邊緣編號
● 搜尋條件
graph.facebook.com/vX.Y/{id}?{query-request}
147. 取得留言內容 - 透過 Graph API 檢視
這邊我們先簡化題目為「爬取文章的所有留言,不包含留言回覆」
假設我們要爬的文章是
https://www.facebook.com/DoctorKoWJ/videos/1213927345375910/
先透過 Graph API 給定文章 id 搜尋 comment 欄位檢視
148. 取得留言內容 - HTTP GET request
根據前面提到的 HTTP GET 方式,我們可以知道請求的格式與所需資訊
version = 'v2.11'
id = '1213927345375910'
query = 'fields=comments&access_token={}'.format(access_token)
url = https://graph.facebook.com/{}/{}?{}.format(
version, id, query
)
149. 取得留言內容 - HTTP GET request
當我們對 graph.facebook.com 發送合法請求時,
對方會把分頁結果以 json 字串格式回傳
# method 1: 透過 response 物件的 json method 轉成 dict
resp = requests.get(url)
data = resp.json()
# method 2: 透過 json module 把 json 字串轉成 dict
import json
data = json.loads(resp.text)
150. 取得留言內容 - 游標型分頁
當我們對文章節點發出 API 請求時,
有可能會因為結果有上千筆,所以系統會做分頁效果回傳給你部份資訊
游標型分頁是有效的分頁方法,但你不應該儲存游標而是每次透過程式決定
游標會指向一個隨機字串,
用來代表清單中的特定項目
判斷是否有下個分頁來決定是否繼續送出請求