Python程式設計

Lesson 14: 網路爬蟲

Last Updated: 2022/12/27

網路爬蟲

簡介

三步驟完成網頁大數據分析

網頁爬蟲

web spider?

web crawler?

web scraper?

關於網路爬蟲(1/3)

爬取網頁(連結)  ➠ 製作網頁索引

Web crawling

應用:搜尋引擎

Spider程式爬取網站內容

為爬取的網頁建立索引

使用者搜尋關鍵字

接收結果

關於網路爬蟲(2/3)

爬取網頁內容  ➠ 轉成結構化資料

Web scraping

目的:Make smarter decisions

價格追蹤

商品評價

潛在客戶

2.1 遍訪連結
建立待訪堆疊

1. 從一個網頁開始

2.2 擷取內容

3. 輸出結果(至檔案)

取得HTML回應碼

解析超連結

解析其他HTML元素

資料輸出

資料清理

關於網路爬蟲(3/3)

實作爬蟲程式

實作Web Scraper?(1/4)

能力1: 取得HTML回應碼?了解HTTP協定

網址(http request)

瀏覽器

Web 伺服器

HTML回應碼(http response)

實作Web Scraper?(2/4)

能力2: 解析超連結、其他HTML元素、網頁內容  ➠

HTML Parser(解析器)

符號化

建立DOM樹

關於HTML元素(1/3)

以p元素為例

元素

內容

開始標籤

結束標籤

屬性名稱

屬性值

找出超連結元素A,屬性href的值 ➠ 待訪網址

關於HTML元素(2/3)

【任務】建立待訪網址串列:

​        1.找到所有超連結元素A,讀取屬性href的值
        2.讀取該A元素的內容(顯示於網頁的文字)

<div id="mw-pages">
    <h2>類別”台灣球員“中的頁面</h2>
    <p>這個分類中有以下的 200 個頁面,共有 15,436 個頁面。</p>(先前200) (
    <a href="/wiki/index.php?title=%E5%88%86%E9%A1%9E:%E5%8F%B0%E7%81%A3%E7%90%83%E5%93%A1"
        title="分類:台灣球員">之後200</a>)
    <div lang="zh-tw" dir="ltr" class="mw-content-ltr">
        <table style="width: 100%;">
            <tr style="vertical-align: top;">
                <td style="width: 33.3%;">
                    <h3>丁</h3>
                    <ul>
                        <li><a href="/wiki/index.php/%E4%B8%81%E4%B8%96%E5%81%89" title="丁世偉">丁世偉</a>
                        </li>
                        <li><a href="/wiki/index.php/%E4%B8%81%E4%B8%96%E6%9D%B0" title="丁世杰">丁世杰</a>
                        </li>
                    </ul>
                    <h3>丘</h3>
                    <ul>
                        <li><a href="/wiki/index.php/%E4%B8%98%E6%98%8C%E6%A6%AE" title="丘昌榮">丘昌榮</a>
                        </li>
                    </ul>
                    <h3>中</h3>
                </td>
            </tr>
        </table>
    </div>
</div>

關於HTML元素(3/3)

內文:之後200 網址:http://twbsball.dils.tku.edu.tw/wiki/index.php/%E5%88%86%E9%A1%9E:....
球員:丁世偉 網址:http://twbsball.dils.tku.edu.tw/wiki/index.php/%E4%B8%81%E4%B8%96%E5%81%89
球員:丁世杰 網址:http://twbsball.dils.tku.edu.tw/wiki/index.php/%E4%B8%81%E4%B8%96%E6%9D%B0
球員:丘昌榮 網址:http://twbsball.dils.tku.edu.tw/wiki/index.php/%E4%B8%98%E6%98%8C%E6%A6%AE

【任務】建立待訪網址串列:

​        1.找到所有超連結元素A,讀取屬性href的值
        2.讀取該A元素的內容(顯示於網頁的文字)

...
<a href="/wiki/index.php/%E4%B8%98%E6%98%8C%E6%A6%AE" title="丘昌榮">丘昌榮</a>
...
球員:丘昌榮 網址:http://twbsball.dils.tku.edu.tw/wiki/index.php/%E4%B8%98%E6%98%8C%E6%A6%AE

【問題】
         1. 超連結內文:語意不同

         2. href有「絕對」、「相對」路徑之分

相對路徑, 不完整

補成絕對路徑

實作Web Scraper?(3/4)

能力2: 解析超連結、其他HTML元素、網頁內容

 Parser(解析器)

能力1: 取得HTML回應碼?了解HTTP協定

http連線功能

實作Web Scraper?(4/4)

能力3: 資料清理、資料輸出  ➠

資料處理工具

2.1 遍訪連結
建立待訪堆疊

1. 從一個網頁開始

2.2 擷取內容

3. 輸出結果(至檔案)

取得HTML回應碼

解析超連結

解析其他HTML元素

資料輸出

資料清理

網路爬蟲套件: Selenium

能力2: 解析網頁內容(Parser)

能力1: HTTP連線功能

安裝Selenium

pip install selenium

在主控台安裝selenium:

pip3 install selenium

安裝Selenium

❷ 下載瀏覽器驅動程式

確認你的Chrome版本

安裝Selenium

❸ 設定Path環境變數

輸入d:\drivers 或其他資料夾

安裝Selenium

❹ 將驅動程式解壓縮至d:\drivers

安裝Selenium

❺ 測試環境

from selenium import webdriver

從selenium模組引入webdriver功能

# 1. 引入webdriver功能
from selenium import webdriver

# 2. 設定驅動程式路徑
dirverPath = 'D:\drivers\chromedriver.exe'

# 3. 建立webdriver物件
browser = webdriver.Chrome(dirverPath)

print(type(browser))

建立webdriver物件, 變數名為browser

selenium功能:擷取網頁

get()函式

# 1. 引入webdriver功能
from selenium import webdriver

# 2. 設定驅動程式路徑
dirverPath = 'D:\drivers\chromedriver.exe'

# 3. 建立webdriver物件
browser = webdriver.Chrome(dirverPath)

# 4. 設定網址
url = 'http://www.au.edu.tw'

# 5. 取得網頁(HTTP連線功能)
browser.get(url)        # 網頁下載至瀏覽器

呼叫get()函式,可連上網頁

尋找HTML元素

find_element_by_id(id):傳回第一個相符id的元素。
find_elements_by_id(id):傳回所有相符的id的元素,傳回串列
find_element_by_class_name(name):傳回第一個相符Class的元素。
find_elements_by_class_name(name):傳回所有相符的Class的元素,傳回串列
find_element_by_name(name):傳回第一個相符name屬性的元素。
find_elements_by_name(name):傳回所有相符的name屬性的元素,傳回串列
find_element_by_css_selector(selector):傳回第一個相符CSS selector的元素。
find_elements_by_css_selector(selector):傳回所有相符的CSS selector的元素,傳回串列
find_element_by_partial_link_text(text):傳回第一個內含有text的<a>元素。
find_elements_by_ partial_link_text(text):傳回所有內含相符text的<a>元素,傳回串列
find_element_by_link_text(text):傳回第一個完全相同text的<a>元素。
find_elements_by_link_text(text):傳回所有完全相同text的<a>元素,以串列方式傳回。
find_element_by_tag_name(name):不區分大小寫,傳回第一個相符name的元素。
find_elements_by_tag_name(name):不區分大小寫,傳回所有相符的name的元素,傳回串列

尋找HTML文件中元素的方法(Parser功能)

找不到特定元素:NoSuchElementException

獲取HTML元素內容的屬性與方法

HTML編碼

元素

開始標籤

結束標籤

內容

屬性名稱

屬性值

tag_name:元素名稱。  
text:元素內容。
location:這是字典,內含有x和y鍵值,表示元素在頁面上的座標。
clear( ):可以刪除在文字(text)欄位或文字區域(textarea)欄位的文字。
get_attribute(name):可以獲得這個元素name屬性的值。
is_displayed( ):如果元素可以看到傳回True,否則傳回False。
is_enabled( ):如果元素是可以立即使用則傳回True,否則傳回False。
is_selected( ):如果元素的核取方塊有勾選則傳回True,否則傳回False。

# 1. 引入webdriver功能
from selenium import webdriver
# 2. 設定驅動程式路徑
dirverPath = 'D:\drivers\chromedriver.exe'
# 3. 建立webdriver物件
browser = webdriver.Chrome(dirverPath)
# 4. 設定網址
url = 'http://www.au.edu.tw'
# 5. 取得網頁
browser.get(url)        # 網頁下載至瀏覽器

tag = browser.find_element_by_id('main')
print(tag.tag_name)

發生異常狀況(找不到指定的元素)...

main找不到

# 1. 引入webdriver功能
from selenium import webdriver
# 2. 設定驅動程式路徑
dirverPath = 'D:\drivers\chromedriver.exe'
# 3. 建立webdriver物件
browser = webdriver.Chrome(dirverPath)
# 4. 設定網址
url = 'http://www.au.edu.tw'
# 5. 取得網頁
browser.get(url)        # 網頁下載至瀏覽器

try:
    tag = browser.find_element_by_id('main')
    print(tag.tag_name)
except:
    print("沒有找到相符的元素")

應以try...except處理異常狀況

from selenium import webdriver  # 1. 引入webdriver功能
from selenium.webdriver.common.by import By

dirverPath = 'D:\drivers\chromedriver.exe'  # 2. 驅動程式路徑
browser = webdriver.Chrome(dirverPath)  # 3. ebdriver物件
url = 'https://www.au.edu.tw'
browser.get(url)                       # 5. 取得網頁

try:
    tag1 = browser.find_element(By.TAG_NAME, 'title')
    print("標籤=%s, 內容= %s " % (tag1.tag_name, tag1.text))

    tag3 = browser.find_elements(By.ID, 'content')           # 傳回id為content的內容
    for i in range(len(tag3)):
        print("標籤=%s, 內容= %s " % (tag3[i].tag_name, tag3[i].text))
        
    iframes = browser.find_elements(By.TAG_NAME, 'iframe')
    print(f'有{len(iframes)}個iframe')
    
    # 找第一個超連結
    link = browser.find_element(By.TAG_NAME, "a")
    print('文字部分:',link.text)
    print('連結部分:', link.get_attribute('href'))

    tag4 = browser.find_elements(By.TAG_NAME, 'p')           # 傳回<p>
    for i in range(len(tag4)):
        print("標籤=%s, 內容= %s " % (tag4[i].tag_name, tag4[i].text))

    tag5 = browser.find_elements(By.TAG_NAME, 'img')         # 傳回<img>
    for i in range(len(tag5)):
        print("標籤=%s, 內容=%s " % (tag5[i].tag_name, tag5[i].get_attribute('src')))
except:
    print("沒有找到相符的元素")

應用: 印出所有圖片網址

from selenium import webdriver  # 1. 引入webdriver功能

dirverPath = 'D:\drivers\chromedriver.exe'  # 2. 驅動程式路徑
browser = webdriver.Chrome(dirverPath)  # 3. ebdriver物件
url = 'https://tw.yahoo.com/'
browser.get(url)                       # 5. 取得網頁

try:
    images = browser.find_elements_by_tag_name('img')

    for i in range(len(images)):
        print(images[i].get_attribute('src'))
    
except:
    print("沒有找到相符的元素")
finally:
    browser.quit()
  • find_elements_by_tag_name(): 注意elemenets是複數, img是標籤名稱
  • 圖片網址: 由src屬性設定,故呼叫get_attribute('src')
  • browser.quit()關閉瀏覽器
  • finally區塊:不論是否異常,都關閉瀏覽器

應用: 印出標題與所有超連結

from selenium import webdriver  # 引入webdriver
import time

driverPath = 'D:/drivers/chromedriver.exe'
browser  = webdriver.Chrome(driverPath)

url = 'https://twbsball.dils.tku.edu.tw/wiki/index.php/%E5%88%86%E9%A1%9E:%E5%8F%B0%E7%81%A3%E6%97%85%E7%BE%8E%E7%90%83%E5%93%A1'
browser.get(url)
time.sleep(3)

try:
    print(browser.title)

    links = browser.find_elements_by_tag_name('a')
    print('總共有幾個連結:', len(links))

    for i in range(len(links)):
        print(links[i].get_attribute('href'))
except:
    print('沒有找到相符的元素')
finally:
    browser.quit()

標題: title屬性

time.sleep(): 暫停一些秒數,以免被認為是「攻擊行為」

href屬性: 指定超連結網址

網路爬蟲實作

 

☛ 單一頁面版:使用 Selenium

多頁面版:先收集相同類型網頁的「連結」

 

單一網頁版

範例網頁

電影片名

年份、長度

爛蕃茄新鮮度

關於這部電影

價格

內容分級

評分、評論

其他(不一定有):

準備工作:找出解析的關鍵詞

第一類:HTML標籤

按F12 叫出「開發人員工具」

要擷取的部份

對應的HTML碼

h1

準備工作:找出解析的關鍵詞

第一類:HTML標籤

from selenium.webdriver.common.by import By
# 找出所有h1
h1tags = browser.find_elements(By.TAG_NAME, 'h1')

使用 find_elements()  找出所有;或 find_element() 找第一個

# 印出所有h1標籤的內文
for h1 in h1_tag:
    print(h1.text)

By.TAG_NAME: HTML標籤類

.text欄位

關於HTML元素

以p元素為例

元素

內容

開始標籤

結束標籤

屬性名稱

屬性值

.text

準備工作:找出解析的關鍵詞

第一類:HTML標籤

from selenium import webdriver  # 引入webdriver'
from selenium.webdriver.common.by import By
import time
# 版本:0.1
driverPath = 'C:/DRIVERS/chromedriver.exe'
# 連線到網頁url, 回傳解析結果
def get_browser(url):
    browser  = webdriver.Chrome(driverPath)
    browser.get(url)
    time.sleep(3)
    return browser
# 主程式
電影資料={}   # 所有欄位存放的字典
url = 'https://play.google.com/store/movies/details/Minions_The_Rise_of_Gru?id=xXdWrJF8KPQ.P&hl=zh_TW&gl=SI'
#
browser = get_browser(url)    # 連結網頁, 解析網頁內容
# 電影片名
h1_tag = browser.find_element(By.TAG_NAME, 'h1') # 找第一個h1
電影片名 = h1_tag.text  # 取出文字
電影資料['電影片名'] = 電影片名  # 存到字典

準備工作:找出解析的關鍵詞

第二類:CLASS (CSS樣式)

要擷取的部份

對應的HTML碼

div

tv4jIf

class需注意指定的名稱能否抓到要的資訊

準備工作:找出解析的關鍵詞

第二類:CLASS (CSS樣式)

from selenium.webdriver.common.by import By
# 找第一個class名稱叫做'tv4jIf'的標籤
div_year = browser.find_element(By.CLASS_NAME,'tv4jIf')

使用 find_elements()  找出所有;或 find_element() 找第一個

# 印出該標籤的內文
print(div_year.text)

By.CLASS_NAME: CLASS css樣式類

2022年 • 87 分鐘

年份

電影長度

2個欄位

使用字串split()功能切成兩段

str1 = 'bear, cat, dog, elephant'
arr = str1.split(',') #以逗點切割->串列['bear', ' cat', ' dog', ' elephant']
for item in arr:
    print(item.strip())  # 去掉頭尾的空白字元'\t','\n','\r'

字串切割split()範例

div_year = browser.find_element(By.CLASS_NAME,'tv4jIf')

tokens = div_year.text.split('•')
for item in tokens:
    print(item.strip())

年份/ 電影長度

準備工作:找出解析的關鍵詞

第二類:CLASS (CSS樣式)

# 以上省略
# 版本:0.3

# 主程式
電影資料={}   # 所有欄位存放的字典
url = 'https://play.google.com/store/movies/details/Minions_The_Rise_of_Gru?id=xXdWrJF8KPQ.P&hl=zh_TW&gl=SI'
#
browser = get_browser(url)    # 連結網頁, 解析網頁內容
# 電影片名
h1_tag = browser.find_element(By.TAG_NAME, 'h1') # 找第一個h1
電影片名 = h1_tag.text  # 取出文字
電影資料['電影片名'] = 電影片名  # 存到字典
# 年份/電影長度
div_year = browser.find_element(By.CLASS_NAME,'tv4jIf')
for item in div_year.text.split('•'):
    if '年' in item:
        電影資料['年份'] = item
    else:
        電影資料['電影長度'] = item
print(電影資料)

準備工作:找出解析的關鍵詞

第二類:CLASS (CSS樣式)

# 以上省略
# 版本:0.5

# 主程式
電影資料={}   # 所有欄位存放的字典
url = 'https://play.google.com/store/movies/details/Minions_The_Rise_of_Gru?id=xXdWrJF8KPQ.P&hl=zh_TW&gl=SI'
#
# ...略...
# 爛蕃茄新鮮度
fresh_tag = browser.find_element(By.CLASS_NAME, 'ClM7O')
電影資料['新鮮度'] = fresh_tag.text
# 價格
price_tag = browser.find_element(By.CLASS_NAME, 'u4ICaf')
電影資料['價格'] = price_tag.text

print(電影資料)

準備工作:找出解析的關鍵詞

第三類:XPATH (節點定位語法)

要擷取的部份

對應的HTML碼

div

data-g-id

標籤內特定的屬性名稱

準備工作:找出解析的關鍵詞

第三類:XPATH (節點定位語法)

from selenium.webdriver.common.by import By
# 找第一個class名稱叫做'tv4jIf'的標籤
about = browser.find_element(By.XPATH, '//div[@data-g-id="description"]')

使用 find_elements()  找出所有;或 find_element() 找第一個

//div[@data-g-id="description"]

By.XPATH: 節點定位

//div[...] 

//找到某個div(相對路徑)

data-g-id屬性值是...的標籤

[@data-g-id="..."]

符號化

建立DOM樹

XPATH: 走訪DOM樹節點

準備工作:找出解析的關鍵詞

第三類:XPATH (節點定位語法)

# 以上省略
# 版本:0.5

# 主程式
電影資料={}   # 所有欄位存放的字典
url = 'https://play.google.com/store/movies/details/Minions_The_Rise_of_Gru?id=xXdWrJF8KPQ.P&hl=zh_TW&gl=SI'
#
# ...略...
# # 關於這部電影
about = browser.find_element(By.XPATH, '//div[@data-g-id="description"]')
電影資料['關於'] = about.text

print(電影資料)
from selenium import webdriver  # 引入webdriver'
from selenium.webdriver.common.by import By
import time
# 版本:1.0
driverPath = 'C:/DRIVERS/chromedriver.exe'
# 連線到網頁url, 回傳解析結果
def get_browser(url):
    browser  = webdriver.Chrome(driverPath)
    browser.get(url)
    time.sleep(3)
    return browser
# 主程式
電影資料={}   # 所有欄位存放的字典
url = 'https://play.google.com/store/movies/details/Minions_The_Rise_of_Gru?id=xXdWrJF8KPQ.P&hl=zh_TW&gl=SI'
#
browser = get_browser(url)    # 連結網頁, 解析網頁內容
# 電影片名
h1_tag = browser.find_element(By.TAG_NAME, 'h1') # 找第一個h1
電影片名 = h1_tag.text  # 取出文字
電影資料['電影片名'] = 電影片名  # 存到字典
# 年份/電影長度
div_year = browser.find_element(By.CLASS_NAME,'tv4jIf')
for item in div_year.text.split('•'):
    if '年' in item:
        電影資料['年份'] = item
    else:
        電影資料['電影長度'] = item
# 爛蕃茄新鮮度  itemprop="tomatoMeter"
# 分級 itemprop="contentRating"
# try:
    ft = browser.find_element(By.XPATH,"//span[@itemprop='tomatoMeter']")
    電影資料['新鮮度'] = ft.text
except:
    print('沒有爛番茄指數')
    電影資料['新鮮度'] = ''
# 價格
price_tag = browser.find_element(By.CLASS_NAME, 'u4ICaf')
電影資料['價格'] = price_tag.text
# # 關於這部電影
about = browser.find_element(By.XPATH, '//div[@data-g-id="description"]')
電影資料['關於'] = about.text
# 內容分級
try:
    grade_tag = browser.find_element(By.XPATH, '//div[@class="xg1aie"]')
    電影資料['分級'] = grade_tag.text
except:
    print('找不到內容分級')
print(電影資料)

單一網頁完整版

收集網頁連結

多網頁的準備工作

範例網頁

要擷取的部份

對應的HTML碼

class="zuJxTd"

擷取區段的關鍵詞

先用find_elements測試,同樣的class有兩個;第1個是要擷取的內容

擷取區段的關鍵詞

# 找到new movies 的 div class
list_tag= browser.find_element(By.CLASS_NAME, 'zuJxTd')

1. 先找到區段: 包含很多影片的標籤

2. 進一步觀察擷取每一部影片的關鍵詞

要擷取的部份

role="listitem"

步驟1結果

擷取區段的關鍵詞

# 找到new movies 的 div class
list_tag= browser.find_element(By.CLASS_NAME, 'zuJxTd')

# 在裡面找出所有role = listitem, 注意XPath最前面的. 代表從目前的層級,而非從root層級
item_tags = list_tag.find_elements(By.XPATH,'.//div[@role="listitem"]')

for item in item_tags:
    print(item)  # 但內容還包含很多HTML標籤

2. 進一步觀察擷取每一部影片的關鍵詞

item已經包含所需要的「連結」、「片名」、「價格」

3. 再觀察如何擷取所需欄位

擷取區段的關鍵詞

3. 再觀察如何擷取所需欄位

步驟1結果

步驟2結果

a標籤的href屬性

for item in item_tags:
    a_tag = item.find_element(By.TAG_NAME, 'a')
    href = a_tag.get_attribute('href')  # 讀取href屬性

擷取區段的關鍵詞

3. 再觀察如何擷取所需欄位

電影名稱class="Epkrse"

for item in item_tags:
    name = a_tag.find_element(By.CLASS_NAME,"Epkrse")  # 找岀電影名
    price = a_tag.find_element(By.CLASS_NAME, "LrNMN") # 找出價格

價格

class="LrNMN"

from selenium import webdriver  # 引入webdriver'
from selenium.webdriver.common.by import By
import time
import csv
import os

driverPath = 'C:/DRIVERS/chromedriver.exe'

def get_browser(url):
    browser  = webdriver.Chrome(driverPath)
    browser.get(url)
    time.sleep(3)
    return browser

url = 'https://play.google.com/store/movies?hl=zh_TW&gl=SI'
browser = get_browser(url)


# 找到new movies 的 div class
list_tag= browser.find_element(By.CLASS_NAME, 'zuJxTd')

# 在裡面找出所有role = listitem, 注意XPath最前面的. 代表從目前的層級,而非從root層級
item_tags = list_tag.find_elements(By.XPATH,'.//div[@role="listitem"]')

# 將連結存起來
links = []
names = []
prices = []
for item in item_tags:
    a_tag = item.find_element(By.TAG_NAME, 'a')
    href = a_tag.get_attribute('href')  # 讀取href屬性
    name = a_tag.find_element(By.CLASS_NAME,"Epkrse")  # 找岀電影名
    price = a_tag.find_element(By.CLASS_NAME, "LrNMN") # 找出價格
    links.append(href)
    names.append(name.text)
    prices.append(price.text)
    print(f'{href},{name.text},{price.text}')

# 輸出CSV檔, 以'\t'間隔 三個欄位
fn = os.path.dirname(__file__)+'/out_links.csv'
with open(fn, mode='w+', encoding='utf-8', newline='') as cf:
    cf_writer = csv.writer(cf, delimiter='\t')
    cf_writer.writerow(['片名','網址','價格'])
    for row in zip(names, links, prices):   # zip是將三個串列'縫'在一起
        cf_writer.writerow(row)
    cf.close()

收集連結完整版

輸出csv至out_links.csv

網路爬蟲實作

 

☛ 單一頁面版:使用 Requests + BeautilfulSoup4

 

Web Scraping範例(單一頁面版)

pip install requests

pip install beautifulsoup4

pip install lxml

安裝Requests, BeautilfulSoup套件(使用pippip3)

parser部分使用lxml(速度快,也可解析XML檔)

若系統找不到pip,改用pip3

import requests, bs4

# (1) 連線
url = "https://www.au.edu.tw/"  # url: 要擷取的網址
resp = requests.get(url)  # 發出HTTP request

# (2) 網頁解析
soup = bs4.BeautifulSoup(resp.content, "lxml")  # 分析網頁

# (3) 擷取內容

print(f'編碼:{resp.encoding}') # HTTP回應有部份訊息可供使用

print('網頁標題', soup.title)   # 解析後有也有資訊可用

BeatuifulSoup範例網頁標題.title

解析後的網頁才是重點!

import requests, bs4

url = "https://www.au.edu.tw/"  # url: 要擷取的網址
resp = requests.get(url)  # 發出HTTP request
## 檢查是否連線成功
if resp.status_code == 200:        # 代碼200表示連線成功
    soup = bs4.BeautifulSoup(resp.content, "lxml")  # 分析網頁
else:                              # 連線失敗或其他
    exit(1)      # 非200代碼,程式直接結束

print(f'編碼:{resp.encoding}') # HTTP回應有部份訊息可供使用

print('網頁標題', soup.title)   # 解析後有也有資訊可用

BeatuifulSoup範例網頁標題.title

實務上,會加上連線成功(代碼200)的檢查

import requests, bs4

# (1) 連線
url = "https://www.au.edu.tw/"  # url: 要擷取的網址
resp = requests.get(url)  # 發出HTTP request

# (2) 網頁解析
soup = bs4.BeautifulSoup(resp.content, "lxml")  # 分析網頁

# (3) 擷取內容

print(f'編碼:{resp.encoding}') # HTTP回應有部份訊息可供使用

print('網頁標題', soup.title)   # 解析後有也有資訊可用

BeatuifulSoup範例找tag名稱

解析後的網頁才是重點!

完整範例

以台灣棒球維基館為例

Web Scraping範例單一頁面版

import requests
from bs4 import BeautifulSoup

# Step 1: 發出Http Request, 解析(parse)Http Response 
## url: 要擷取的網址
base_url = "http://twbsball.dils.tku.edu.tw" # 網站基礎網址,補絕對網址用
url = "http://twbsball.dils.tku.edu.tw/wiki/index.php/%E5%88%86%E9%A1%9E:%E5%8F%B0%E7%81%A3%E7%90%83%E5%93%A1"
resp = requests.get(url)  # 發出HTTP request
if resp.status_code == 200:                     # 代碼200表示連線正常
    soup = BeautifulSoup(resp.content, "lxml")  # parsing網頁內容
else:
    exit(1)      # 非200代碼,程式直接結束

### 維基百科頁面標題特徵: id="firstHeading"
title = soup.find(id="firstHeading")  # 根據id值進行搜尋
print(f'頁面標題{title.text}')

### 維基百科頁面內容:假設只對包含在id="mw-pages"內的超連結感興趣
### 注意!!!每個網頁內容的id可能都不一樣!!!必須先詳細了解網頁格式
all_links = soup.find(id="mw-pages").find_all("a")  # 找出所有<A>標籤
print(f"超連結數量:{len(all_links):10}")

# 走訪過濾後的<A>標籤:
#### 1. 只對此Wiki站內連結感興趣
#### 2. 只處理球員連結
num_of_link = 0
for link in all_links:
    if link['href'].find("/wiki/") == -1:   # 站外連結跳過不處理
        continue
    if link.text == '之後200':        # 網頁特定內容,跳過不處理
        print(f'type of link:{type(link)}')
        continue
    print(f"球員:{link.text} 網址:{base_url + link['href']}")
    num_of_link += 1
print(f'球員總數{num_of_link:10}')

webscraping-link.py

重要:必須先了解該網頁的HTML結構

重點觀察

  • 屬性值(id, class),
  • 特定標籤<div>, <h>系列, <SPAN>

前往目標網址後,開啟「開發人員工具」(F12)

任務 1: 爬取「球員連結」

但不是「所有超連結」都是球員

移動游標尋找目標區域

div標籤, id值 "mw-pages"

# 以'lxml' parser解析 resp.content的內容
soup = BeautifulSoup(resp.content, "lxml") 
...
# 找出mw-pages段落裡面所有<A>標籤
all_links = soup.find(id="mw-pages").find_all("a") 

任務 2: 擷取「頁面內容」

有興趣的段落沒有 id, class等屬性值可用

dl, dd, ul, li等標籤無法區分是否為感興趣的段落 

url = "擷取的網址"
resp = requests.get(url)  # (1)連線
if resp.status_code == 200:                     # 代碼200表示連線正常
    soup = BeautifulSoup(resp.content, "lxml")  # (2)分析
else:
    exit(1)      # 非200代碼,程式直接結束
import requests
from bs4 import BeautifulSoup

import需要的模組

連線與分析(parsing)

### 維基百科頁面標題特徵: id="firstHeading"
title = soup.find(id="firstHeading")  # 根據id值進行搜尋
print(f'頁面標題{title.text}')

擷取任務1: 取得標題

### 維基百科頁面內容:假設只對包含在id="mw-pages"內的超連結感興趣
### 注意!!!每個網頁內容的id可能都不一樣!!!必須先詳細了解網頁格式
all_links = soup.find(id="mw-pages").find_all("a")  #找出所有<A>標籤

# 走訪過濾後的<A>標籤:
for link in all_links:
    if link['href'].find("/wiki/") == -1:   # 站外連結跳過不處理
        continue
    if link.text == '之後200':        # 網頁特定內容,跳過不處理
        continue
    print(f"球員:{link.text} 網址:{link['href']}") # 印出網址

擷取任務2: 取出段落中的超連結

Web Scraping範例單一頁面版#2

Web Scraping範例單一頁面版#2

webscraping-player.py

from bs4 import BeautifulSoup
import requests
import pandas

sections = [    # data frame的column name,共9個
    '簡介', # 同義詞段落'生平簡介'
    '基本資料',
    '經歷',
    '個人年表',
    '特殊事蹟',
    '職棒生涯成績',
    '備註',
    '註釋或參考文獻',
    '外部連結',
]

sid = {                        # 台灣棒球維基館的section id
    '基本資料': '.E5.9F.BA.E6.9C.AC.E8.B3.87.E6.96.99',
    '經歷': '.E7.B6.93.E6.AD.B7',
    '個人年表': '.E5.80.8B.E4.BA.BA.E5.B9.B4.E8.A1.A8',
    '特殊事蹟': '.E7.89.B9.E6.AE.8A.E4.BA.8B.E8.B9.9F',
    '職棒生涯成績':'.E8.81.B7.E6.A3.92.E7.94.9F.E6.B6.AF.E6.88.90.E7.B8.BE',
    '備註':'.E5.82.99.E8.A8.BB',
    '註釋或參考文獻': '.E8.A8.BB.E9.87.8B.E6.88.96.E5.8F.83.E8.80.83.E6.96.87.E7.8D.BB',
    '外部連結': '.E5.A4.96.E9.83.A8.E9.80.A3.E7.B5.90',
    '結尾': 'stub'
}
intro = {
    '簡介': '.E7.B0.A1.E4.BB.8B',
    '生平簡介': '.E7.94.9F.E5.B9.B3.E7.B0.A1.E4.BB.8B'
}

def extract_introduction(content, key, value):
    pos = str(content).find(f'id="{value}"')  # 簡介 / 生平簡介
    if pos == -1:
        return None
    else:
        next_pos = str(content).find('<span class="mw-headline" id=".E5.9F.BA.E6.9C.AC.E8.B3.87.E6.96.99">基本資料</span>')
        
        intro_str = str(content)[pos:next_pos]         # 擷取 「簡介/生平簡介」 段落
        # print('*****',key, len(key))
        intro_str = intro_str[intro_str.find(key):]  # 去掉最前面未切乾淨的html碼 
        # print(intro_str)
        
        intro_soup = BeautifulSoup(intro_str, 'lxml')  # parsing
        texts = intro_soup.find_all(text=True)         # 取出所有元素的文字,不含標籤屬性
                                                       # 資料格式為串列
        return u"".join(t.strip() for t in texts)      # 將串列元素接在一起形單一字串,u""為unicode

####使用字串.find()和取得子字串的功能,切出每個段落大致的內容
# 參數:
#   content: http response回應的內容部份
#   start: 開始字串(不含),要裁切內容的開始位置
#   end: 結尾字串(不含), 裁切內容的結束位置 
# 回傳:
#     裁切好的子字串
def get_sub_content(content, start, end):
    start_pos = str(content).find(f'id="{start}"')
    print(f'sp:{start_pos}')
    if start_pos == -1:     # 找不到start字串
        return None
    else:                   # 找到 start字串
        end_pos = str(content).find(f'id="{end}"')   # 尋找end字串
        
        if end_pos == -1:
            return None     # 找不到end字串
        else:
            print(f'ep:{end_pos}')
            return str(content)[start_pos: end_pos]  # 回傳裁切子字串

#### 使用BeatuifulSoup的功能,取出以<dd></dd>包起來的段落內容
# 參數:
#      html_str: 要分析的段落內容字串
# 回傳:
#      段落內容串列,每一個li為一個串列元素
def extract_dlist(html_str):
    soup = BeautifulSoup(html_str, 'lxml')  # parsing
    li = soup.find_all('li')         # 找到<li>
    li_texts = []    # 空字串, 將存入所有li的文字
    for item in li:
        item_text = item.find_all(text=True)
        if u"".join(item_text).strip() != '':
            li_texts.append(u"".join(item_text).strip())
    return li_texts
     
# Step 1: 發出Http Request, 解析(parse)Http Response 
## url: 要擷取的網址
base_url = "http://twbsball.dils.tku.edu.tw"
url = "http://twbsball.dils.tku.edu.tw/wiki/index.php/%E4%BD%95%E9%9C%87%E7%83%8A"
url = "http://twbsball.dils.tku.edu.tw/wiki/index.php/%E7%8E%8B%E5%BB%BA%E6%B0%91(1980)"
resp = requests.get(url)  # 發出HTTP request
if resp.status_code == 200:
    soup = BeautifulSoup(resp.content, "lxml")  # parsing網頁內容
else:
    exit(1)
final_result = {}  # 最後擷取的全文結果
all_content = soup.find(id="bodyContent") # 取得全部HTML內文
for key, value in intro.items():
    pp = extract_introduction(all_content, key, value)  # 取得 簡介
    if pp != None:
        print(f'找到{key}')
        final_result[key] = pp  # 簡介
        break
    else:
        print(f'找不到{key}')

start_str = ''  # 擷取段落用的開始字串
end_str = ''    # 擷取段落用的結束字串
title = ''      # 目前擷取的段落名稱:對應至sid的key
next_title = '' # 下一個擷取的段落名稱:對應至sid的key

for key, value in sid.items():  # 遍訪sid串列,取出定位字串
    if start_str == '':  # 1. 設定開始字串
        start_str = value # 1.1開始字串
        title = key       # 1.2段落名稱
        continue
    else:
        if end_str == '':  # 2. 設定結束字串
            end_str = value  # 2.1 結束字串
            next_title = key # 2.2 下一個段落名稱
            print(title)
            # 使用開始字串,結束字串取得中間的html內容
            sub_content_str = get_sub_content(all_content, start_str, end_str)
            if sub_content_str == None:  # 如果擷取回來沒有內容
                end_str = ''   # 清除結束字串,繼續處理下一個sid項目
                continue
            # 處理擷取回來的段落內容
            li_list = extract_dlist(sub_content_str) # 取出當中的li
            final_result[title] = ''.join(li_list)  # 紀錄起來
            start_str = end_str # 設定下一段的開始字串
            end_str = ''        # 清除結束字串
            title =next_title   # 設定下一段的段落名稱
# print(final_result)  # 印出擷取的資料
fr_array = []      # 準備空白表格資料
fr_array.append(final_result)  # 加入一筆資料至表格
index=[i for i in range(len(fr_array))]   # data frame的索引
### 建立dataframe
#   1. fr_array: 表格資料(目前只有一筆)
#   2. index=index:  前者是參數名稱,後者是第9行建立的索引串列
#   3. columns=sections: 前者是參數名稱, 後者是先前定義的欄位串列
df = pandas.DataFrame(fr_array,index=index, columns=sections)

df.to_csv('out_file.csv')   # 寫出檔案(utf-8編碼)

裁切子字串的函式:指定開始、結束字串,切出中間的內容

####使用字串.find()和取得子字串的功能,切出每個段落大致的內容
# 參數:
#   content: http response回應的內容部份
#   start: 開始字串(不含),要裁切內容的開始位置
#   end: 結尾字串(不含), 裁切內容的結束位置 
# 回傳:
#     裁切好的子字串
def get_sub_content(content, start, end):
    start_pos = str(content).find(f'id="{start}"')
    print(f'sp:{start_pos}')
    if start_pos == -1:     # 找不到start字串
        return None
    else:                   # 找到 start字串
        end_pos = str(content).find(f'id="{end}"')   # 尋找end字串
        
        if end_pos == -1:
            return None     # 找不到end字串
        else:
            print(f'ep:{end_pos}')
            return str(content)[start_pos: end_pos]  # 回傳裁切子字串

開始字串、結束字串包括:

sid = {                        # 台灣棒球維基館的section id
    '基本資料': '.E5.9F.BA.E6.9C.AC.E8.B3.87.E6.96.99',
    '經歷': '.E7.B6.93.E6.AD.B7',
    '個人年表': '.E5.80.8B.E4.BA.BA.E5.B9.B4.E8.A1.A8',
    '特殊事蹟': '.E7.89.B9.E6.AE.8A.E4.BA.8B.E8.B9.9F',
    '職棒生涯成績':'.E8.81.B7.E6.A3.92.E7.94.9F.E6.B6.AF.E6.88.90.E7.B8.BE',
    '備註':'.E5.82.99.E8.A8.BB',
    '註釋或參考文獻': '.E8.A8.BB.E9.87.8B.E6.88.96.E5.8F.83.E8.80.83.E6.96.87.E7.8D.BB',
    '外部連結': '.E5.A4.96.E9.83.A8.E9.80.A3.E7.B5.90',
    '結尾': 'stub'
}

開始字串、結束字串段落內都是<dd></dd>, 找出當中的<li>

#### 使用BeatuifulSoup的功能,取出以<dd></dd>包起來的段落內容
# 參數:
#      html_str: 要分析的段落內容字串
# 回傳:
#      段落內容串列,每一個li為一個串列元素
def extract_dlist(html_str):
    soup = BeautifulSoup(html_str, 'lxml')  # parsing
    li = soup.find_all('li')         # 找到<li>
    li_texts = []    # 空字串, 將存入所有li的文字
    for item in li:
        item_text = item.find_all(text=True)  # 只要內文
        if u"".join(item_text).strip() != '':
            li_texts.append(u"".join(item_text).strip())
    return li_texts

li的內文才是要擷取的主體內容

球員簡介段落的格式與其他段落不同

def extract_introduction(content, value):
    pos = str(content).find(f'id="{value}"')  # 簡介 / 生平簡介
    if pos == -1:
        return None
    else:
        next_pos = str(content).find('<span class="mw-headline" id=".E5.9F.BA.E6.9C.AC.E8.B3.87.E6.96.99">基本資料</span>')
        
        intro_str = str(content)[pos:next_pos]         # 擷取 「簡介/生平簡介」 段落
       
        intro_str = intro_str[intro_str.find(key):]  # 去掉最前面未切乾淨的html碼 
        
        intro_soup = BeautifulSoup(intro_str, 'lxml')  # parsing
        texts = intro_soup.find_all(text=True)         # 取出所有元素的文字,不含標籤屬性
                                                       # 資料格式為串列
        return u"".join(t.strip() for t in texts)      # 將串列元素接在一起形單一字串,u""為unicode
intro = {
    '簡介': '.E7.B0.A1.E4.BB.8B',
    '生平簡介': '.E7.94.9F.E5.B9.B3.E7.B0.A1.E4.BB.8B'
}

簡介段落有下列兩種網頁格式

soup = BeautifulSoup(resp.content, "lxml")
all_content = soup.find(id="bodyContent") # 取得全部HTML內文
start_str = ''  # 擷取段落用的開始字串
end_str = ''    # 擷取段落用的結束字串
title = ''      # 目前擷取的段落名稱:對應至sid的key
next_title = '' # 下一個擷取的段落名稱:對應至sid的key
final_result = {}
for key, value in sid.items():  # 遍訪sid串列,取出定位字串
    if start_str == '':  # 1. 設定開始字串
        start_str = value # 1.1開始字串
        title = key       # 1.2段落名稱
        continue
    else:
        if end_str == '':  # 2. 設定結束字串
            end_str = value  # 2.1 結束字串
            next_title = key # 2.2 下一個段落名稱
            print(title)
            # 使用開始字串,結束字串取得中間的html內容
            sub_content_str = get_sub_content(all_content, start_str, end_str)
            if sub_content_str == None:  # 如果擷取回來沒有內容
                end_str = ''   # 清除結束字串,繼續處理下一個sid項目
                continue
            # 處理擷取回來的段落內容
            li_list = extract_dlist(sub_content_str) # 取出當中的li
            final_result[title] = li_list  # 紀錄起來
            start_str = end_str # 設定下一段的開始字串
            end_str = ''        # 清除結束字串
            title =next_title   # 設定下一段的段落名稱
print(final_result)  # 印出擷取的資料

取得各段落內文

Python程式設計

By Leuo-Hong Wang

Python程式設計

Lesson 14: 網路爬蟲

  • 1,912