測試自動化與持續整合的最佳實踐

在我多年的開發經驗

模擬HTTP請求的利器:HTTPretty詳解與實戰

在網路應用開發中,測試與外部API的互動是一個常見挑戰。當我們的應用需要與第三方服務通訊時,如何在不實際傳送請求的情況下模擬這些互動?這正是HTTPretty這類別HTTP模擬函式庫值所在。

利用HTTPretty處理複雜的HTTP模擬場景

在前面的例子中,我們已經看到了HTTPretty的基本用法。現在讓我們探討一些更複雜的應用場景,這些場景在實際開發中經常遇到。

模擬序列回應

在實際應用中,我們可能需要模擬一個API在不同請求下回傳不同的回應。HTTPretty允許我們透過responses引數來註冊一系列按順序回傳的回應:

import httpretty
import requests
from sure import expect

@httpretty.activate
def sequence_responses_example():
    URL = "http://api.example.com/v1/users"
    RESPONSE_1 = '{"id": 1, "name": "John"}'
    RESPONSE_2 = '{"error": "rate limit exceeded"}'
    RESPONSE_3 = '{"id": 2, "name": "Mark"}'
    
    # 註冊多個序列回應
    httpretty.register_uri(
        httpretty.GET, 
        URL,
        responses=[
            httpretty.Response(body=RESPONSE_1, status=201),
            httpretty.Response(body=RESPONSE_2, status=202),
            httpretty.Response(body=RESPONSE_3, status=201)
        ]
    )
    
    # 傳送四個請求測試回應序列
    response_1 = requests.get(URL)
    expect(response_1.status_code).to.equal(201)
    expect(response_1.text).to.equal(RESPONSE_1)
    
    response_2 = requests.get(URL)
    expect(response_2.status_code).to.equal(202)
    expect(response_2.text).to.equal(RESPONSE_2)
    
    response_3 = requests.get(URL)
    expect(response_3.status_code).to.equal(201)
    expect(response_3.text).to.equal(RESPONSE_3)
    
    # 第四個請求將回傳最後註冊的回應
    response_4 = requests.get(URL)
    expect(response_4.status_code).to.equal(201)
    expect(response_4.text).to.equal(RESPONSE_3)

這個例子中,我們透過responses引數註冊了三個不同的回應。當我們傳送四個請求時,前三個請求分別獲得對應的回應,從第四個請求開始將獲得最後一個註冊的回應。這種機制非常適合測試API限流、錯誤處理等情境。

模擬流式回應

有些API會回傳流式回應(Streaming Response),這種回應沒有Content-Length頭,而是使用Transfer-Encoding: chunked頭,並將回應體分成多個塊逐步傳送。HTTPretty也支援模擬這種回應:

import httpretty
import requests
from time import sleep
from sure import expect

def mock_streaming_repos(repos):
    for repo in repos:
        sleep(.5)  # 模擬每0.5秒傳送一個資料塊
        yield repo

@httpretty.activate
def streaming_responses_example():
    URL = "https://api.github.com/orgs/python/repos"
    REPOS = [
        '{"name": "repo-1", "id": 1}\r\n',
        '\r\n',
        '{"name": "repo-2", "id": 2}\r\n'
    ]
    
    # 註冊流式回應
    httpretty.register_uri(
        httpretty.GET,
        URL,
        body=mock_streaming_repos(REPOS),
        streaming=True
    )
    
    response = requests.get(URL, data={"track": "requests"})
    line_iter = response.iter_lines()
    
    # 驗證每個流式回應塊
    for i in range(len(REPOS)):
        expect(next(line_iter).strip()).to.equal(REPOS[i].strip())

這個例子中,我們建立了一個生成器函式mock_streaming_repos,它會每隔0.5秒產生一個回應塊。透過將streaming引數設為True,HTTPretty知道這是一個流式回應,並會正確模擬這種行為。

使用回呼實作動態回應

在更複雜的情境下,API的回應可能取決於請求的內容。HTTPretty允許我們使用回呼函式來動態生成回應:

import httpretty
import requests
from sure import expect

@httpretty.activate
def dynamic_responses_example():
    # 定義回呼函式生成動態回應
    def request_callback(request, uri, response_headers):
        return (200, response_headers, f"The {request.method} response from {uri}")
    
    # 註冊使用回呼的URI
    httpretty.register_uri(
        httpretty.GET, 
        "http://example.com/sample/path",
        body=request_callback
    )
    
    response = requests.get("http://example.com/sample/path")
    expect(response.text).to.equal('The GET response from http://example.com/sample/path')

在這個例子中,request_callback函式會根據請求的方法和URI動態生成回應內容。這種方式特別適合測試那些根據請求引數或內容回傳不同結果的API。

社交媒體API互動:理解與應用

除了HTTP模擬,讓我們也看如何使用Requests函式庫交媒體API進行互動,這是現代應用開發中常見的需求。

API基本概念

API(應用程式介面)是一組規則和規範,它幫助我們與不同的軟體系統進行通訊。在網路開發中,我們經常使用RESTful API,它遵循REST(表述性狀態轉移)架構的設計原則,包括:

  • 客戶端-伺服器模式
  • 無狀態通訊
  • 可快取性
  • 分層系統
  • 統一介面
  • 按需程式碼

Google Maps API、Twitter API和GitHub API都是RESTful API的例子。

Twitter API入門

要開始使用Twitter API,我們首先需要取得API金鑰,這是一種在呼叫API時傳遞的程式碼,用於識別程式並進行身份驗證。

取得API金鑰
  1. 使用Twitter帳號登入 https://apps.twitter.com/
  2. 點選「Create New App」按鈕
  3. 填寫應用程式資料:
    • 名稱:指定應用程式名稱
    • 描述:簡短描述應用程式功能
    • 網站:填寫完整的網站URL(包含http://或https://)
    • 回呼URL:成功認證後的回傳位置
    • 開發者協定:閱讀並同意條款
  4. 點選「Create your Twitter application」建立應用程式
  5. 在「Keys and Access Tokens」頁籤中,點選「Create my access token」生成存取令牌
  6. 記下Consumer Key (API Key)、Consumer Secret (API Secret)、Access Token和Access Token Secret
建立認證請求

使用OAuth1認證來存取Twitter API:

import requests
from requests_oauthlib import OAuth1

CONSUMER_KEY = 'YOUR_APP_CONSUMER_KEY'
CONSUMER_SECRET = 'YOUR_APP_CONSUMER_SECRET'
ACCESS_TOKEN = 'YOUR_APP_ACCESS_TOKEN'
ACCESS_TOKEN_SECRET = 'YOUR_APP_ACCESS_TOKEN_SECRET'

auth = OAuth1(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
取得喜愛的推文

使用Twitter API取得使用者喜愛的推文:

favorite_tweet = requests.get(
    'https://api.twitter.com/1.1/favorites/list.json?count=1', 
    auth=auth
)
tweet_data = favorite_tweet.json()
執行簡單搜尋含有特定關鍵字的推文:
search_results = requests.get(
    'https://api.twitter.com/1.1/search/tweets.json?q=%40python', 
    auth=auth
)
search_metadata = search_results.json()["search_metadata"]
取得關注者列表

取得指定使用者的關注者:

followers = requests.get(
    'https://api.twitter.com/1.1/followers/list.json', 
    auth=auth
)
followers_data = followers.json()["users"]
取得被轉發的推文

取得被他人轉發的推文:

retweets = requests.get(
    'https://api.twitter.com/1.1/statuses/retweets_of_me.json', 
    auth=auth
)
retweets_count = len(retweets.json())
first_retweet = retweets.json()[0]

實際應用與最佳實踐

在我的實際專案中,我經常將HTTPretty與社交媒體API互動結合使用。例如,當開發一個需要與Twitter互動的應用時,我會先使用HTTPretty建立模擬環境進行測試,然後再實際連線Twitter API。

這種方法有幾個優點:

  1. 快速開發:不需等待API回應,測試週期更短
  2. 可靠測試:不依賴外部服務的可用性
  3. 無限制測試:避免API呼叫限制問題
  4. 離線開發:在沒有網路連線的情況下也能工作

一個實際案例是,我曾為一家媒體監測公司開發一個Twitter情感分析工具。在開發初期,我使用HTTPretty模擬各種Twitter API回應,包括正常資料、錯誤狀態和限流情況。這讓我能夠在不實際呼叫API的情況下全面測試應用的各種行為。

當應用準備好與真實API互動時,我只需將模擬程式碼替換為實際API呼叫,而不需要修改應用的主要邏輯。

結合使用的技巧

當同時使用HTTPretty和社交媒體API時,有幾個技巧可以提高效率:

  1. 記錄真實回應:首先使用真實API取得回應,然後將其儲存為測試資料
  2. 模擬不同場景:使用HTTPretty模擬各種邊緣情況,如API錯誤、超時等
  3. 環境切換:設定環境變數來控制是使用模擬還是真實API
  4. 自動化測試:將HTTPretty測試整合到CI/CD流程中

HTTP請求模擬和社交媒體API互動是現代網路應用開發中不可或缺的部分。掌握這些技術可以幫助我們建立更健壯、更可靠的應用程式,同時提高開發效率和測試覆寫率。無論是單元測試還是整合測試,HTTPretty都是一個強大的工具,而Requests則是與各種API互動的首選函式庫 在下一篇文章中,我們將探討如何處理更複雜的API互動模式,以及如何在大型應用中組織和管理API客戶端程式碼。

Python 與社群媒體平台的無縫整合

社群媒體平台已經成為現代網路生態中不可或缺的一環,透過它們的 API,開發者能夠建立各種創新應用。在我多年的開發經驗中,社群媒體 API 整合一直是許多專案的核心需求。本文將帶領各位使用 Python 的 Requests 套件與主流社群媒體平台 API 進行互動,從認證到資料擷取,再到內容發布。

為何選擇 Requests 套件?

Python Requests 套件以其簡潔易用的 API 設計,成為處理 HTTP 請求的首選工具。在社群媒體 API 整合方面,它具有以下優勢:

  1. 語法直覺與易於理解
  2. 內建 JSON 處理功能
  3. 簡化的 OAuth 認證流程
  4. 靈活的 Session 管理

讓我們開始探索如何利用這個強大工具與各大社群平台互動。

Twitter API 整合實戰

Twitter 的 API 提供了豐富的功能,讓開發者能夠取得趨勢資訊、發布推文及互動。以下我們將探討幾個常見的 Twitter API 應用場景。

取得 Twitter 趨勢資訊

Twitter 趨勢是特定時間內熱門的主題標籤(hashtag)。要取得可用的趨勢位置資訊,我們可以使用以下端點:

import requests

# 假設 auth 變數已經包含了認證資訊
available_trends = requests.get('https://api.twitter.com/1.1/trends/available.json', auth=auth)

# 檢視可用趨勢的位置數量
print(len(available_trends.json()))
# 輸出: 467

# 檢視第十個位置的資訊
print(available_trends.json()[10])
# 輸出一個包含位置資訊的字典,如城市名稱、國家程式碼等

上面的程式碼中,我們查詢了可用趨勢的位置,發現有 467 個位置有提供趨勢資訊。接著我們查看了第十個位置的資料,得到了包含位置詳細資訊的回應。這些資訊中包含了 woeid(Where On Earth ID),這是一個用於識別地理位置的唯一識別碼。

發布 Twitter 狀態更新(推文)

要發布一則推文,我們需要向 Twitter API 傳送 POST 請求。以下是基本流程:

requests.post('https://api.twitter.com/1.1/statuses/update.json?status=This%20is%20a%20Tweet', auth=auth)

需要注意的是,Twitter 有防止重複內容的機制。如果嘗試傳送與使用者最近推文相同的內容,請求會被阻擋並回傳 403 錯誤。這意味著使用者不能連續兩次傳送完全相同的推文。

在我的開發實踐中,我常會將這些功能封裝成更易用的函式,例如:

def post_tweet(message, auth_info):
    """發布推文並處理可能的錯誤"""
    try:
        encoded_message = requests.utils.quote(message)
        response = requests.post(
            f'https://api.twitter.com/1.1/statuses/update.json?status={encoded_message}',
            auth=auth_info
        )
        response.raise_for_status()
        return response.json()
    except requests.exceptions.HTTPError as e:
        if e.response.status_code == 403:
            return {"error": "可能嘗試傳送重複內容"}
        return {"error": str(e)}

這樣的封裝不僅提高了程式碼的可讀性,也增加了錯誤處理的健壯性。

Facebook Graph API 整合

Facebook 提供了強大的 Graph API,讓開發者能夠存取 Facebook 上的各種資源,包括狀態、讚、頁面和照片等。

Facebook API 概述

Facebook 提供兩種主要的 API:Graph API 和 Ads API。Graph API 是一個 RESTful JSON API,可用於存取 Facebook 上的各種資源;而 Ads API 主要用於管理廣告活動、受眾等。

Graph API 之所以得名,是因為它將資料表示為「節點」(如使用者、照片、頁面)和「邊」(節點間的連線,如頁面的照片的評論)。

取得 Facebook API 存取令牌

要使用 Facebook API,我們需要一個存取令牌(access token)。Facebook 提供了四種類別的存取令牌:

  1. 使用者存取令牌:最常用的令牌類別,用於存取使用者資訊和在使用者時間軸上發布內容
  2. 應用程式存取令牌:用於應用程式層級的操作
  3. 頁面存取令牌:用於存取和管理 Facebook 頁面
  4. 客戶端令牌:可嵌入應用程式中,用於存取應用程式層級的 API

以下是取得應用程式存取令牌的步驟:

  1. 登入 Facebook 開發者平台
  2. 建立一個新應用程式
  3. 應用程式建立完成後,可以在應用程式頁面取得 App ID 和 App Secret

與 Twitter 不同,Facebook 不需要建立特殊的認證請求,App ID 和 App Secret 已足以提供資源存取許可權。

取得使用者個人資料

要存取目前登入使用者的個人資料,我們可以使用 https://graph.facebook.com/me 端點:

import requests

# 存取令牌
ACCESS_TOKEN = '231288990034554xxxxxxxxxxxxxxx'

# 傳送 API 請求
me = requests.get("https://graph.facebook.com/me", params={'access_token': ACCESS_TOKEN})

# 將回應轉換為 JSON 並檢視可用的鍵
print(me.json().keys())
# 輸出: [u'website', u'last_name', u'relationship_status', u'locale', ...]

# 取得使用者 ID 和姓名
user_id = me.json()['id']
user_name = me.json()['name']
print(f"使用者 ID: {user_id}, 姓名: {user_name}")

我們得到了 me.json() 回應,其中包含了使用者的各種資訊,如 ID、姓名、姓氏、家鄉、工作等。使用者 ID 是一個唯一的數字,用於識別 Facebook 上的使用者。我們將在後續範例中使用這個 ID 來取得使用者的好友、動態和相簿。

取得好友列表

要取得特定使用者的好友列表,我們可以使用 https://graph.facebook.com/<user-id>/friends 端點:

# 使用前面取得的使用者 ID
friends = requests.get(f"https://graph.facebook.com/{user_id}/friends", 
                      params={'access_token': ACCESS_TOKEN})

# 查看回應的結構
print(friends.json().keys())
# 輸出: [u'paging', u'data']

# 檢視好友數量
print(len(friends.json()['data']))
# 輸出: 32

# 檢視好友資料的結構
print(friends.json()['data'][0].keys())
# 輸出: [u'name', u'id']

回應中的 data 屬性包含了好友資訊,是一個包含好友 ID 和姓名的物件列表。在實際應用中,我們可以進一步處理這些資料,例如:

def get_friends_data(user_id, access_token):
    """取得並格式化好友資料"""
    friends_response = requests.get(
        f"https://graph.facebook.com/{user_id}/friends",
        params={'access_token': access_token}
    )
    
    if friends_response.status_code != 200:
        return []
        
    friends_data = friends_response.json().get('data', [])
    return [{'id': friend['id'], 'name': friend['name']} for friend in friends_data]

取得使用者動態

要取得使用者的動態,包括狀態更新和連結,我們可以使用 feed 引數:

# 取得使用者動態
feed = requests.get(f"https://graph.facebook.com/{user_id}/feed", 
                   params={'access_token': ACCESS_TOKEN})

# 查看回應結構
print(feed.json().keys())
# 輸出: [u'paging', u'data']

# 檢視動態數量
print(len(feed.json()["data"]))
# 輸出: 24

# 檢視動態資料結構
print(feed.json()["data"][0].keys())
# 輸出: [u'from', u'privacy', u'actions', u'updated_time', ...]

動態資料包含了許多資訊,如發布者、隱私設定、更新時間、讚等。這些資料可用於分析使用者的社交活動模式或建立自訂的動態牆。

取得相簿資訊

要存取使用者建立的相簿,我們可以使用以下方式:

# 取得使用者相簿
albums = requests.get(f"https://graph.facebook.com/{user_id}/albums", 
                     params={'access_token': ACCESS_TOKEN})

# 查看回應結構
print(albums.json().keys())
# 輸出: [u'paging', u'data']

# 檢視相簿數量
print(len(albums.json()["data"]))
# 輸出: 13

# 檢視相簿資料結構
print(albums.json()["data"][0].keys())
# 輸出: [u'count', u'from', u'name', u'privacy', ...]

# 檢視第一個相簿的名稱
print(albums.json()["data"][0]["name"])
# 輸出: u'Timeline Photos'

在實際應用中,我們可能會想進一步取得相簿中的照片:

def get_album_photos(album_id, access_token):
    """取得相簿中的所有照片"""
    photos_response = requests.get(
        f"https://graph.facebook.com/{album_id}/photos",
        params={'access_token': access_token}
    )
    
    if photos_response.status_code != 200:
        return []
        
    return photos_response.json().get('data', [])

Reddit API 整合

Reddit 是一個熱門的社群網路、娛樂和新聞網站,註冊使用者可以提交內容(如文字貼文或直接連結),並對這些提交進行投票。每個內容條目都根據興趣領域分類別,稱為「子版塊」(subreddits)。

Reddit API 概述

Reddit API 包含四個重要部分:

  1. listings:Reddit 中的端點稱為 listings,包含 after/before、limit、count、show 等引數
  2. modhashes:用於防止跨站請求偽造(CSRF)攻擊的令牌
  3. fullnames:結合事物類別和唯一 ID 的組合,形成 Reddit 上全域唯一 ID 的緊湊編碼
  4. account:處理使用者帳戶相關操作,如註冊、登入、設定 HTTPS 等

註冊新帳戶

在 Reddit 上註冊新帳戶相當簡單。首先存取 Reddit 網站,然後點選右上角的「sign in」或「create an account」連結,填寫登入檔單:

  • username:用於唯一識別 Reddit 社群成員
  • email:可選欄位,用於直接與使用者溝通
  • password:用於登入 Reddit 平台的安全密碼
  • verify password:應與密碼欄位相同
  • captcha:用於檢查嘗試登入的是人類還是機器人

在以下範例中,我們假設已建立了使用者名和密碼,分別為 OUR_USERNAMEOUR_PASSWORD

修改帳戶資訊

現在,讓我們新增之前在建立帳戶時故意留空的電子郵件:

import requests

# 建立工作階段物件,允許我們在所有請求中維護特定引數和 cookies
client = requests.session()
client.headers = {'User-Agent': 'Reddit API - update profile'}

# 建立包含使用者名、密碼和 API 類別的資料字典
DATA = {'user': 'OUR_USERNAME', 'passwd': 'OUR_PASSWORD', 'api_type': 'json'}

# 傳送 POST 請求以登入 Reddit 帳戶
response = client.post('https://ssl.reddit.com/api/login', data=DATA)

## 使用Requests操作Reddit API

在前面的章節中我們已經探討瞭如何使用Requests與Twitter和Facebook等社群媒體平台進行互動現在讓我們深入瞭解如何透過Requests函式庫作Reddit的API功能

### 更新Reddit使用者資料

當我們需要更新Reddit帳戶資訊時例如電子郵件地址我們必須先取得modhash值一種安全令牌),然後再進行更新操作以下是完整的流程

```python
# 使用之前取得的回應中提取modhash值
modhash = response.json()['json']['data']['modhash']

# 準備更新引數
update_params = {
    "api_type": "json", 
    "curpass": "OUR_PASSWORD",
    "dest": "www.reddit.com", 
    "email": "user@example.com",
    "verpass": "OUR_PASSWORD", 
    "verify": True,
    'uh': modhash
}

# 傳送更新請求
r = client.post('http://www.reddit.com/api/update', data=update_params)

這段程式碼中,我們首先從之前的認證回應中提取modhash值,這是Reddit用來防止CSRF攻擊的安全機制。接著,我們準備更新引數,包含當前密碼、目標網址、新的電子郵件地址等資訊,並傳送POST請求到Reddit的更新API端點。

驗證更新結果

當更新請求成功時,我們會收到包含空錯誤列表的JSON回應:

# 檢查狀態碼
print(r.status_code)
# 輸出: 200

# 檢查回應內容
print(r.text)
# 輸出: {"json": {"errors": []}}

若要確認電子郵件是否已成功更新,我們可以查詢當前登入使用者的資訊:

# 取得使用者資訊
me = client.get('http://www.reddit.com/api/me.json')

# 檢查是否有郵件標記
has_mail = me.json()['data']['has_mail']
# 如果has_mail為True,表示電子郵件已成功設定

在開發Reddit機器人時,這種API操作流程非常見。一次我在為客戶開發Reddit分析工具時,發現更新使用者設定後必須立即驗證更改是否生效,否則可能導致後續操作失敗。

使用Reddit的搜尋功能

Reddit提供了強大的搜尋API,我們可以用它來搜尋整個網站或特定的子版(subreddit)。以下是如何進行搜尋的範例:

# 搜尋整個Reddit
search = requests.get('http://www.reddit.com/search.json', params={'q': 'python'})

# 檢查回應結構
print(search.json().keys())
# 輸出: ['kind', 'data']

# 取得第一個搜尋結果的標題
title = search.json()['data']['children'][0]['data']['title']
print(title)
# 輸出: 'If you could change something in Python what would it be?'

# 取得作者和分數
author = search.json()['data']['children'][0]['data']['author']
score = search.json()['data']['children'][0]['data']['score']
print(f"作者: {author}, 分數: {score}")

搜尋結果以JSON格式回傳,包含在datachildren屬性中。每個搜尋結果都有豐富的元資料,如標題、作者、得分等。

搜尋子版(Subreddits)

如果要搜尋特定子版而非帖子,可以使用子版搜尋API:

# 搜尋子版
subreddit_search = requests.get('http://www.reddit.com/subreddits/search.json', 
                               params={'q': 'python'})

# 取得第一個子版的標題
subreddit_title = subreddit_search.json()['data']['children'][0]['data']['title']
print(subreddit_title)
# 輸出: 'Python'

這個API讓我們能夠根據關鍵字找到相關的子版,對於想要探索特定主題的社群非常有用。

網頁爬蟲:超越API的資料取得方式

雖然API提供了結構化的資料存取方式,但在實際開發中,我們經常會遇到以下情況:

  1. 目標網站沒有提供API
  2. 現有API限制太多或功能不足
  3. API結構突然改變而沒有提前通知

在這些情況下,網頁爬蟲(Web Scraping)成為取得網路資料的重要技術。

網頁資料類別解析

在開始爬蟲之前,瞭解網頁上的資料類別非常重要。一般來說,網路上的資料可分為三種類別:

結構化資料(Structured Data)

結構化資料具有預定義的格式,通常具有明確的關係和組織方式。這類別資料通常儲存在關聯式資料函式庫可以使用SQL查詢。例如:

  • 人口普查記錄
  • 客戶資料函式庫 產品目錄

這類別資料的特點是格式一致、機器可讀,與各資料元素之間有明確的關聯關係。

非結構化資料(Unstructured Data)

非結構化資料缺乏標準格式或組織結構,處理起來較為困難。這類別資料通常需要使用文字分析、自然語言處理(NLP)或資料挖掘等技術處理。例如:

  • 圖片和影片
  • 科學資料
  • 文字密集的內容(如報紙、健康記錄)

半結構化資料(Semistructured Data)

半結構化資料介於上述兩者之間,它有一定的結構但不夠嚴格或結構經常變化。這類別資料通常使用標籤或其他標記來建立元素間的語義關係。典型例子就是網頁上的HTML內容。

當我們傳送請求到維基百科並查看回應內容時,就能看到典型的半結構化資料:

import requests
r = requests.get("http://en.wikipedia.org/wiki/List_of_algorithms")
print(r)
# <Response [200]>

print(r.text[:200])
# '<!DOCTYPE html>\n<html lang="en" dir="ltr" class="client-nojs">\n<head>\n<meta charset="UTF-8" />\n<title>List of algorithms - Wikipedia, the free encyclopedia</title>\n...'

這種半結構化的HTML資料適合用爬蟲技術來提取特定內容。

網頁爬蟲的定義與操作原則

網頁爬蟲(Web Scraping)是從網頁資源中提取所需資料的過程。這個過程涉及與網頁資源互動、選擇適當的資料、取得資訊,以及將資料轉換為所需格式。

爬蟲的道德準則

在進行網頁爬蟲時,必須遵守一些基本的道德準則:

  1. 尊重網站的使用條款:在開始爬取之前,務必檢視網站的使用條款和robots.txt檔案,確認網站是否允許爬蟲。

  2. 不要過度請求:避免短時間內傳送大量請求,這可能導致伺服器負載過高甚至當機。在請求之間適當新增延遲。

    在我早期的爬蟲專案中,曾因為沒有控制請求頻率而被多家網站暫時封鎖IP,這是非常糟糕的經驗。現在我總是在請求間新增隨機延遲,模擬真實使用者行為。

  3. 定期監控網站變化:網站結構可能隨時變化,導致爬蟲程式失效。定期檢查並更新爬蟲程式是維護長期專案的關鍵。

網頁爬蟲的基本流程

網頁爬蟲通常涉及以下工具和步驟:

必備工具

  1. 瀏覽器開發者工具:如Chrome DevTools或Firefox的FireBug外掛,用於分析頁面結構和定位所需資訊。
  2. **HTTP函式庫:如Python的Requests,用於與伺服器互動並取得回應檔案。
  3. 爬蟲工具:如BeautifulSoup或Scrapy,用於從半結構化檔案中提取資料。

爬蟲步驟

  1. 確定要爬取的網頁URL
  2. 使用HTTP函式庫網頁內容
  3. 分析網頁結構,找出所需資料的位置
  4. 使用爬蟲工具解析網頁,將半結構化資料轉換為更結構化的格式
  5. 提取並處理所需資料

網頁爬蟲的核心任務

在爬取網頁資料時,我們主要執行以下幾項任務:

  1. 搜尋半結構化檔案:使用標籤名稱和屬性(如id、class等)來定位特定元素。
  2. 在檔案中導航:透過向下、向上、向側或前後導航來存取不同的資料元素。
  3. 修改半結構化檔案:透過修改標籤名稱或屬性來精簡檔案,便於提取所需資料。

使用BeautifulSoup進行網頁爬蟲

BeautifulSoup是Python中最受歡迎的網頁爬蟲函式庫,它提供了簡單而強大的API來解析HTML和XML檔案,並從中提取所需資料。

BeautifulSoup簡介

BeautifulSoup能夠接收HTML或XML檔案,並將其轉換為樹狀結構的Python物件,方便我們導航和搜尋。它支援多種解析器,預設使用Python的標準HTMLParser,但也可以使用其他解析器如lxml或html5lib。

安裝BeautifulSoup

安裝BeautifulSoup非常簡單,只需使用pip:

pip install beautifulsoup4

若要使用特定解析器如lxml,還需單獨安裝:

pip install lxml

BeautifulSoup的基本使用

使用BeautifulSoup的第一步是建立一個BeautifulSoup物件:

from bs4 import BeautifulSoup

# 從HTML字元串建立BeautifulSoup物件
soup = BeautifulSoup("<h1 id='message'>Hello, Requests!</h1>")

也可以從HTML檔案或網頁回應中建立:

# 從網頁回應建立
response = requests.get('https://example.com')
soup = BeautifulSoup(response.text, 'html.parser')

BeautifulSoup的核心物件

BeautifulSoup將HTML/XML檔案轉換為樹狀結構的Python物件,主要包括以下幾種:

1. 標籤(Tag)

標籤物件對應HTML/XML檔案中的標籤,具有名稱和各種屬性:

from bs4 import BeautifulSoup
soup = BeautifulSoup("<h1 id='message'>Hello, Requests!</h1>")

# 取得第一個h1標籤
tag = soup.h1
print(tag.name)  # 輸出: h1
print(tag['id'])  # 輸出: message
print(tag.text)  # 輸出: Hello, Requests!

標籤物件支援多種導航和搜尋方法,使我們能夠輕鬆地在檔案中移動和找到所需元素。

2. 導航字元串(NavigableString)

導航字元串對應標籤內的文字內容:

text = tag.string
print(type(text))  # 輸出: <class 'bs4.element.NavigableString'>
print(text)  # 輸出: Hello, Requests!

3. BeautifulSoup物件

BeautifulSoup物件本身代表整個檔案,它提供了眾多方法來搜尋和導航檔案:

# 搜尋所有p標籤
paragraphs = soup.find_all('p')

# 搜尋具有特定class的元素
elements = soup.find_all(class_='highlight')

# 使用CSS選擇器搜尋
elements = soup.select('div.content > p')

在實際開發中,BeautifulSoup的這些功能使得網頁爬蟲變得相對簡單。以下是我在一個實際專案中使用BeautifulSoup的經驗:

在開發一個技術部落格聚合器時,我需要從各個技術部落格中提取文章標題、

BeautifulSoup 基本元素解析:深入理解爬蟲核心元件

在進行網頁爬蟲時,我們需要清楚瞭解 BeautifulSoup 的幾個基本元素。這些元素構成了 BeautifulSoup 解析網頁的基礎,掌握它們對於編寫有效的爬蟲程式至關重要。

Tag 元素:網頁結構的基本單位

Tag 元素是 BeautifulSoup 中最基本的元件,代表 HTML 或 XML 檔案中的標籤。當我們從網頁中提取資料時,首先要識別並操作的就是這些標籤。以下展示如何存取標籤的類別、名稱和屬性:

# 存取標籤類別
tag = soup.h1
type(tag)  # 輸出: <class 'bs4.element.Tag'>

# 存取標籤名稱
tag.name  # 輸出: 'h1'

# 存取標籤屬性(例如 'id')
tag['id']  # 輸出: 'message'

在實際專案中,我常發現許多開發者忽略了對標籤屬性的完整利用。標籤屬性往往包含寶貴的資訊,例如 CSS 類別名稱、ID 或自定義資料屬性,這些都可以成為定位特定內容的重要依據。

BeautifulSoup 物件:整體檔案的表示

BeautifulSoup 物件代表我們要爬取的整個網頁檔案。它是所有操作的起點,包含了完整的網頁結構和內容:

from bs4 import BeautifulSoup
soup = BeautifulSoup("<h1 id='message'>Hello, Requests!</h1>")
type(soup)  # 輸出: <class 'bs4.BeautifulSoup'>

在建立 BeautifulSoup 物件時,我建議始終指定解析器(如 html.parser、lxml 或 html5lib),這能避免許多潛在問題。在我的爬蟲專案中,lxml 解析器通常能提供最佳效能,但 html5lib 在處理不嚴格的 HTML 時更為寬容。

NavigableString 物件代表標籤內的文字內容。我們可以使用 .string 屬性來存取它:

tag.string  # 輸出: 'Hello, Requests!'

在處理大量文字時,NavigableString 提供了許多有用的方法,如 .strip() 可以移除前後空白,這在清理資料時非常有用。

Comment:註解內容的特殊處理

Comment 物件是 NavigableString 的一種特殊類別,代表 HTML 檔案中的註解部分:

soup = BeautifulSoup("<p><!-- This is comment --></p>")
comment = soup.p.string
type(comment)  # 輸出: <class 'bs4.element.Comment'>

在某些情況下,網頁註解中可能包含有用的資訊,例如開發者留下的提示或被暫時註解掉的功能。瞭解如何處理這些註解可以幫助我們發現隱藏的資料來源。

BeautifulSoup 的核心爬蟲任務

接下來,我們將使用一個實際的 HTML 檔案來深入瞭解 BeautifulSoup 的核心爬蟲任務。以下是我們將使用的 HTML 檔案範例(scraping_example.html):

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>
Chapter 6 - Web Scrapping with Python Requests and
BeatuifulSoup
</title>
</head>
<body>
<div class="surveys">
<div class="survey" id="1">
<p class="question">
<a href="/surveys/1">Are you from India?</a>
</p>
<ul class="responses">
<li class="response">Yes - <span class="score">21</span>
</li>
<li class="response">No - <span class="score">19</span>
</li>
</ul>
</div>
<div class="survey" id="2">
<p class="question">
<a href="/surveys/2">Have you ever seen the rain?</a>
</p>
<ul class="responses">
<li class="response">Yes - <span class="score">40</span>
</li>
<li class="response">No - <span class="score">0</span>
</li>
</ul>
</div>
<div class="survey" id="3">
<p class="question">
<a href="/surveys/1">Do you like grapes?</a>
</p>
<ul class="responses">
<li class="response">Yes - <span class="score">34</span>
</li>
<li class="response">No - <span class="score">6</span>
</li>
</ul>
</div>
</div>
</body>
</html>

首先,我們需要建立一個 BeautifulSoup 物件來解析這個檔案:

from bs4 import BeautifulSoup
soup = BeautifulSoup(open("scraping_example.html"))

搜尋檔案樹:定位目標元素

在爬蟲過程中,定位特定元素是首要任務。BeautifulSoup 提供了多種方法來搜尋檔案樹,最常用的是 find()find_all()

find() 方法

find() 方法用於找出第一個符合條件的標籤:

find(name, attributes, recursive, text, **kwargs)

引數說明:

  • name:要搜尋的標籤名稱,可以是字串、正規表示式、列表、函式或布林值 True
  • attributes:標籤的屬性
  • recursive:布林值,如果設為 True,BeautifulSoup 將檢查所有子標籤;如果設為 False,則只檢查下一層的子標籤
  • text:用於識別包含特定字串內容的標籤

find_all() 方法

find_all() 方法用於找出所有符合條件的標籤:

find_all(name, attributes, recursive, text, limit, **kwargs)

引數說明與 find() 相似,但多了一個 limit 引數,用於限制結果數量。

在實際應用中,我發現 CSS 選擇器通常比直接使用 find()find_all() 更直觀。例如,使用 soup.select('.survey .question') 可以一次選取所有問卷問題,這在處理複雜結構時特別有用。

導航檔案樹:在元素間移動

BeautifulSoup 提供了豐富的導航方法,讓我們能在檔案樹中自由移動。

向下導航:從父元素到子元素

我們可以透過標籤名稱直接存取元素:

soup.html  # 存取 html 元素
soup.html.head.title  # 存取 title 元素
soup.html.head.meta  # 存取 meta 元素

這種導航方式在結構簡單與已知的情況下非常方便。然而,當處理複雜或動態變化的網頁時,我建議使用更穩健的搜尋方法。

橫向導航:存取同級元素

BeautifulSoup 提供了多種屬性來存取同級元素:.next_sibling.previous_sibling.next_siblings.previous_siblings

例如,在我們的檔案中,headbody 是同級元素:

# 列出 html 的所有子元素
for child in soup.html.children:
    print(child.name)
# 輸出:
# head
# body

# 找到 head 的下一個同級元素
soup.head.find_next_sibling()  # 回傳 body 元素

# 找到 body 的上一個同級元素
soup.body.find_previous_sibling()  # 回傳 head 元素

橫向導航在處理表格資料或列表時特別有用,可以讓我們在相關元素間快速移動。

向上導航:從子元素到父元素

使用 .parent.parents 屬性可以存取元素的父元素和所有祖先元素:

soup.div.parent.name  # 輸出: 'body'

for parent in soup.div.parents:
    print(parent.name)
# 輸出:
# body
# html
# [document]

向上導航在需要理解元素連貫的背景與環境或從特定元素開始反向追蹤結構時非常有用。

前後導航:在解析順序中移動

.find_previous_element.find_next_element 屬性可以在解析順序中向前或向後移動:

soup.head.find_previous().name  # 輸出: 'html'
soup.head.find_next().name  # 輸出: 'meta'

這種導航方式與檔案的實際結構無關,而是根據 BeautifulSoup 解析檔案的順序。在某些特殊情況下,這種方式比根據結構的導航更有用。

修改檔案樹:動態調整內容

BeautifulSoup 不僅可以解析和提取資料,還可以修改檔案內容。我們可以透過 .name.string.append() 等屬性和方法來改變標籤的屬性或內容,也可以使用 .new_string().new_tag().insert().insert_before().insert_after() 等方法來新增或插入內容。

例如,修改 title 標籤的內容:

# 修改前
soup.title.string  # 輸出: 'Chapter 6 - Web Scrapping with Python Requests and BeatuifulSoup'

# 修改
soup.title.string = 'Web Scrapping with Python Requests and BeatuifulSoup by Balu and Rakhi'

# 修改後
soup.title.string  # 輸出: 'Web Scrapping with Python Requests and BeatuifulSoup by Balu and Rakhi'

在我的爬蟲專案中,修改功能常用於清理資料、標準化格式或準備資料以便後續處理。例如,我曾經需要統一不同網站的日期格式,就使用了 BeautifulSoup 的修改功能來標準化所有日期字串。

實戰案例:建立網頁爬蟲機器人

現在我們已經掌握了 BeautifulSoup 的基本功能,接下來我要分享一個實際案例:建立一個自動化爬蟲機器人,從網站抓取單字列表並儲存為 JSON 檔案。

爬蟲機器人的目標與物件

我們的爬蟲機器人將從 majortests.com 網站擷取 GRE 考試單字列表。該網站包含多種測驗和 GRE 單字列表,我們的目標是擷取這些單字及其釋義,並將它們儲存為 JSON 檔案以便後續使用。

網頁爬蟲的道德與法律考量

在開始爬蟲前,我必須強調遵守網頁爬蟲的道德與法律規範:

  1. 參考網站條款:在爬取任何網站前,務必查閱該網站的使用條款,並取得必要的法律許可。

  2. 避免伺服器過載:不要短時間內傳送大量請求,這可能會對目標網站造成負擔。在我們的實作中,會使用 Python 的 time.sleep 函式來控制請求頻率。

  3. 定期檢查網站變化:確保程式碼與網站結構相容,避免因網站更新而導致爬蟲失效。建議在開始爬蟲前進行單元測試,確認預期的結構是否存在。

實作步驟

步驟一:識別目標 URL

首先,需要確定要爬取的 URL。在這個案例中,我們要爬取多個包含 GRE 單字的頁面,這些頁面的 URL 有一個共同模式:

http://www.majortests.com/gre/wordlist_01
http://www.majortests.com/gre/wordlist_02
http://www.majortests.com/gre/wordlist_03
...

我們可以使用 Python 的字串格式化功能來生成這些 URL:

START_PAGE, END_PAGE = 1, 10
URL = "http://www.majortests.com/gre/wordlist_0%d"

def generate_urls(url, start_page, end_page):
    urls = []
    for page in range(start_page, end_page):
        urls.append(url % page)
    return urls

# 生成