使用 Python Requests 和 BeautifulSoup 進行網頁爬蟲

探索網頁資料結構

在進行網頁爬蟲時,首先需要分析和識別網頁內容的結構。在本例中,我們要爬取的是一個單字列表及其定義。為了有效地識別這些元素,我們可以使用 Chrome DevTools 來檢查網頁的 HTML 結構。這些 HTML 元素的資訊將幫助我們定位單字及其定義,從而在爬蟲過程中精確提取。

要開始分析,請在 Chrome 瀏覽器中開啟目標 URL (http://www.majortests.com/gre/wordlist_01),然後右鍵點選網頁選擇「Inspect」(檢查元素)。

透過檢查,我們可以識別出單字列表的 HTML 結構如下:

<div class="grid_9 alpha">
    <h3>Group 1</h3>
    <a name="1"></a>
    <table class="wordlist">
        <tbody>
            <tr>
                <th>Abhor</th>
                <td>hate</td>
            </tr>
            <tr>
                <th>Bigot</th>
                <td>narrow-minded, prejudiced person</td>
            </tr>
            ...
        </tbody>
    </table>
</div>

分析這個結構,我們可以得出以下理解:

  1. 每個網頁包含一個單字列表
  2. 每個單字列表有多個單字群組,它們都定義在相同的 div 標籤中
  3. 所有單字群組中的單字都被描述在具有 wordlist 類別屬性的表格中
  4. 表格中的每一行 (tr) 都代表一個單字及其定義,分別使用 th 和 td 標籤

使用 BeautifulSoup 解析網頁內容

現在我們已經瞭解了網頁的結構,接下來使用 BeautifulSoup4 作為網頁爬蟲工具來解析我們透過 requests 模組取得的網頁內容。

首先,我們建立一個函式將 HTML 字串轉換為 BeautifulSoup 物件:

def make_soup(html_string):
    return BeautifulSoup(html_string)

這個 make_soup 函式接收 HTML 內容字串,並回傳一個 BeautifulSoup 物件。

提取所需的資料

有了 BeautifulSoup 物件後,我們可以使用它來提取所需的單字及其定義。利用 BeautifulSoup 提供的方法,我們能夠導航 HTML 回應並提取單字列表及定義:

def get_words_from_soup(soup):
    words = {}
    for count, wordlist_table in enumerate(
            soup.find_all(class_='wordlist')):
        title = "Group %d" % (count + 1)
        new_words = {}
        for word_entry in wordlist_table.find_all('tr'):
            new_words[word_entry.th.text] = word_entry.td.text
        words[title] = new_words
    return words

在這段程式碼中,get_words_from_soup 函式接受一個 BeautifulSoup 物件,然後使用 find_all() 方法尋找所有 class 為 ‘wordlist’ 的元素。對於每個找到的表格,我們遍歷其中的每一行 (tr),提取單字 (th) 和定義 (td),最後回傳一個包含所有單字的字典。

儲存爬取的資料

接著,我們需要將取得的單字典儲存為 JSON 檔案:

def save_as_json(data, output_file):
    """ 將給定的資料寫入指定的輸出檔案 """
    with open(output_file, 'w') as outfile:
        json.dump(data, outfile)

這個輔助方法將資料字典寫入指定的輸出檔案。

整合爬蟲流程

最後,我們可以整合上述所有步驟,建立一個完整的爬蟲程式:

import json
import time
import requests
from bs4 import BeautifulSoup

START_PAGE, END_PAGE, OUTPUT_FILE = 1, 10, 'words.json'

這是我們爬蟲程式的起始部分,匯入所需的模組並設定起始頁、結束頁和輸出檔案名稱。從這裡開始,我們將實作完整的爬蟲流程,包括取得網頁內容、解析 HTML、提取資料和儲存結果。

在實際開發爬蟲時,玄貓建議始終遵循網站的使用條款和爬蟲禮節,如新增適當的延遲(使用 time.sleep())以避免對目標伺服器造成過大壓力。此外,使用 User-Agent 標頭以識別你的爬蟲也是一個良好的實踐。

在爬取過程中,我發現有些網站會根據請求的頻率和模式實施防爬蟲措施。在這種情況下,可能需要調整爬蟲策略,如隨機化請求間隔、使用代理伺服器或模擬真實使用者行為等進階技術。

Python網路爬蟲:從理論到實踐的完整

在資料驅動的時代,網路爬蟲已成為取得資料的重要手段。在我擔任資料科學顧問的經驗中,發現許多企業常需要從網站上擷取結構化資料以支援決策分析。這篇文章將帶你深入瞭解如何使用Python的Requests與BeautifulSoup工具組合,建立一個強大的網路爬蟲系統,並將其整合至Flask網頁應用程式中。

網路爬蟲的基礎建設:Requests與BeautifulSoup

網路爬蟲的核心在於兩個主要步驟:取得網頁內容和解析HTML結構。Python的Requests函式庫第一步,而BeautifulSoup則專門負責第二步。這種組合在我多年的爬蟲開發經驗中,證明是最為靈活與高效的方案。

爬蟲工作流程的設計思路

當我設計網路爬蟲系統時,通常遵循這樣的工作流程:

  1. 識別目標URL並生成URL列表
  2. 使用Requests取得網頁資源
  3. 透過BeautifulSoup解析HTML內容
  4. 擷取所需的資料元素
  5. 將資料轉換為結構化格式並儲存

這個流程不僅有邏輯性,也便於後續的維護和擴充套件。接下來,我們將透過一個實際的GRE單字爬蟲專案來實作這個流程。

實戰專案:GRE單字爬蟲機器人

讓我們開發一個爬蟲機器人,用於擷取GRE單字列表。這個例項將展示如何將上述流程轉化為實際的程式碼。

第一步:識別並生成URL列表

首先,我們需要確定目標網站的URL模式,並生成一個URL列表:

# 識別URL模式
URL = "http://www.majortests.com/gre/wordlist_0%d"

def generate_urls(url, start_page, end_page):
    """
    生成URL列表
    
    引數:
      url: URL範本
      start_page: 起始頁碼
      end_page: 結束頁碼
      
    回傳:
      生成的URL列表
    """
    urls = []
    for page in range(start_page, end_page):
        urls.append(url % page)
    return urls

這段程式碼建立了一個函式,可以根據指定的頁碼範圍生成多個URL。這種方法特別適用於需要爬取多個頁面的情況,比如我在為教育科技公司開發爬蟲時,常用類別似的模式來爬取課程資料。

第二步:取得網頁資源

接下來,我們需要建立一個函式來取得網頁內容:

def get_resource(url):
    """
    取得網頁資源
    
    引數:
      url: 目標URL
      
    回傳:
      requests.Response物件
    """
    return requests.get(url)

這個函式雖然簡單,但在實際專案中,我會加入更多的錯誤處理、重試機制和請求頭設定,以提高爬蟲的穩定性和模擬真實瀏覽器行為。

第三步:解析HTML內容

取得網頁後,我們需要使用BeautifulSoup來解析HTML:

def make_soup(html_string):
    """
    將HTML字串轉換為BeautifulSoup物件
    
    引數:
      html_string: HTML內容字串
      
    回傳:
      BeautifulSoup物件
    """
    return BeautifulSoup(html_string)

BeautifulSoup是一個非常強大的HTML解析工具,它能夠將雜亂的HTML轉換為結構化的物件,便於後續的資料擷取。

第四步:擷取所需資料

現在,我們可以從BeautifulSoup物件中擷取所需的資料:

def get_words_from_soup(soup):
    """
    從BeautifulSoup物件中擷取單字組
    
    引數:
      soup: BeautifulSoup物件
      
    回傳:
      擷取的單字組字典
    """
    words = {}
    count = 0
    for wordlist_table in soup.find_all(class_='wordlist'):
        count += 1
        title = "Group %d" % count
        new_words = {}
        for word_entry in wordlist_table.find_all('tr'):
            new_words[word_entry.th.text] = word_entry.td.text
        words[title] = new_words
        print " - - Extracted words from %s" % title
    return words

在這個函式中,我們使用BeautifulSoup的選擇器功能來找到所有具有’wordlist’類別的元素,然後從中擷取單字及其解釋。這種方法展現了BeautifulSoup的強大之處,它能夠讓我們精確地定位所需的HTML元素。

第五步:儲存擷取的資料

最後,我們將擷取的資料儲存為JSON格式:

def save_as_json(data, output_file):
    """ 
    將資料寫入指定的輸出檔案
    """
    json.dump(data, open(output_file, 'w'))

JSON格式是儲存結構化資料的理想選擇,因為它易於讀取和處理,與與多種程式語言相容。

爬蟲機器人的整合

現在,讓我們整合上述功能,建立一個完整的爬蟲機器人:

def scrapper_bot(urls):
    """
    爬蟲機器人:
    
    引數:
      urls: URL列表
      
    回傳:
      包含不同單字組的字典
    """
    gre_words = {}
    for url in urls:
        print "Scrapping %s" % url.split('/')[-1]
        # 步驟1:取得URL
        
        # 步驟2:使用requests取得HTML
        html = requests.get(url)
        
        # 步驟3:使用瀏覽器工具識別所需資料
        
        # 步驟4:建立BeautifulSoup物件
        soup = make_soup(html.text)
        
        # 步驟5:從soup中擷取單字
        words = get_words_from_soup(soup)
        gre_words[url.split('/')[-1]] = words
        
        print "sleeping for 5 seconds now"
        time.sleep(5)  # 避免過度頻繁請求
    return gre_words

if __name__ == '__main__':
    urls = generate_urls(URL, START_PAGE, END_PAGE+1)
    gre_words = scrapper_bot(urls)
    save_as_json(gre_words, OUTPUT_FILE)

這個爬蟲機器人依序處理每個URL,擷取單字資料,並在每次請求之間暫停5秒,以避免對目標網站造成過大負擔。這種禮貌性的爬蟲設計是我在開發爬蟲系統時始終堅持的原則。

爬蟲結果:結構化的單字資料

爬蟲執行完成後,我們會得到一個結構良好的JSON檔案,內容如下:

{
  "wordlist_04": {
    "Group 10": {
      "Devoured": "greedily eaten/consumed",
      "Magnate": "powerful businessman",
      "Cavalcade": "procession of vehicles",
      "Extradite": "deport from one country back to the home..."
      // 更多單字...
    }
    // 更多單字組...
  }
  // 更多單字列表...
}

這種結構化的資料格式便於後續的處理和應用,例如建立單字學習應用或進行詞彙分析。

進階主題:使用Flask建立網頁應用

擷取資料只是第一步,如何將這些資料轉化為有價值的應用才是關鍵。接下來,我們將探討如何使用Flask框架建立一個簡單的網頁應用。

Flask簡介:輕量級但功能強大的框架

Flask是一個輕量級的Python網頁框架,由Armin Ronacher於2010年4月1日發布。它的設計哲學是"簡單但不簡陋",這也是我選擇它作為許多專案基礎的原因。

Flask的主要優勢包括:

  1. 內建開發伺服器:便於開發和測試
  2. 簡易的錯誤記錄:具有互動式網頁除錯器
  3. RESTful API支援:路由裝飾器可接受HTTP方法作為引數
  4. Jinja2範本引擎:靈活的範本渲染系統
  5. Session物件:儲存使用者工作階段資訊
  6. WSGI相容性:100%符合Web伺服器閘道介面協定

Flask入門:第一個應用

讓我們從一個簡單的Flask應用開始,瞭解其基本結構:

from flask import Flask
app = Flask(__name__)

@app.route("/")
def home():
    return "Hello Guest!"

if __name__ == "__main__":
    app.run()

這個簡單的應用展示了Flask的三個基本步驟:

  1. 建立WSGI應使用案例項
  2. 定義路由方法
  3. 啟動應用伺服器

當執行這段程式碼時,存取根路徑("/")將顯示"Hello Guest!"。

環境設定:使用虛擬環境

在開始Flask應用開發前,建立一個隔離的開發環境是最佳實踐。以下是使用virtualenvwrapper設定虛擬環境的步驟:

  1. 安裝virtualenvwrapper:

    $ pip install virtualenvwrapper
    
  2. 設定環境變數:

    $ export WORKON_HOME=~/Envs
    
  3. 建立工作目錄:

    $ mkdir -p $WORKON_HOME
    
  4. 啟用shell指令碼:

    $ source /usr/local/bin/virtualenvwrapper.sh
    
  5. 建立新的虛擬環境:

    $ mkvirtualenv survey
    
  6. 安裝所需套件:

    (survey)~ $ pip install flask flask-sqlalchemy requests httpretty beautifulsoup4
    

例項專案:使用Flask開發投票應用

現在,讓我們開發一個名為"Survey"的簡單投票應用,用於記錄對調查問題的"是"、“否"和"也許"回應。

檔案結構設計

遵循MVC設計模式,我們的Flask應用將有以下檔案結構:

survey_project/
├── __init__.py      # 初始化專案並新增到PYTHONPATH
├── server.py        # 啟動應用開發伺服器
└── survey/
    ├── __init__.py  # 初始化應用並整合各元件
    ├── app.db       # SQLite3資料函式庫
    ├── models.py    # 定義應用模型
    ├── templates/   # Jinja2範本目錄
    ├── tests.py     # 應用測試案例
    └── views.py     # 定義應用路由

讓我們首先建立專案初始化檔案。在survey_project/__init__.py中:

import os
import sys
current_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir))
sys.path.insert(0, parent_dir)

這段程式碼確保專案目錄被新增到Python路徑中,使模組可以被正確匯入。

應用功能設計

我們的Survey應用將具有以下功能:

  1. 建立調查問題
  2. 檢視所有問題列表
  3. 檢視特定問題
  4. 修改問題
  5. 刪除問題
  6. 為問題投票

每個問題將包含以下資料欄位:

  • id:唯一識別每個問題的主鍵
  • question_text:描述調查內容
  • number_of_yes_votes:記錄"是"票數
  • number_of_no_votes:記錄"否"票數
  • number_of_maybe_votes:記錄"也許"票數

資源路由設計

為了實作上述功能,我們需要設計相應的URL路由和HTTP方法。在Flask中,我們可以使用路由裝飾器來關聯URL和處理函式。

在下一階段,我們將實作這些路由並建立完整的模型-

Flask 應用程式開發全:從 URL 設計到資料模型構建

Flask 是一個輕量與彈性的 Python Web 框架,讓開發者能夠快速構建各種網頁應用程式。本文將探討如何使用 Flask 和 Flask-SQLAlchemy 設計一個完整的調查應用程式,包括 URL 設計、資料模型定義以及檢視函式實作。

RESTful API 設計:明確的 URL 結構

在開發 Web 應用程式時,良好的 URL 設計是關鍵的第一步。以下是我們調查應用程式的 URL 設計表:

任務HTTP 方法URL
檢視所有問題GEThttp://[hostname:port]/
建立調查問題POSThttp://[hostname:port]/questions
檢視特定問題GEThttp://[hostname:port]/questions/[question_id]
修改問題PUThttp://[hostname:port]/questions/[question_id]
刪除問題DELETEhttp://[hostname:port]/questions/[question_id]
為問題投票POSThttp://[hostname:port]/questions/[question_id]/vote
投票表單GEThttp://[hostname:port]/questions/[question_id]/vote
新問題表單GEThttp://[hostname:port]/questions/new

這種設計遵循 RESTful 原則,讓 URL 直觀反映資源和操作,使 API 更容易理解和維護。

使用 Flask-SQLAlchemy 定義資料模型

當我開發較複雜的應用程式時,總是傾向使用 ORM (物件關聯對映) 來簡化資料函式庫。Flask-SQLAlchemy 是 Flask 的擴充套件,為 Flask 應用程式提供 SQLAlchemy 支援。

資料模型定義的三個步驟

  1. 建立資料函式庫
  2. 使用該例項定義模型
  3. 呼叫資料函式庫的方法來建立資料表

建立資料函式庫

首先,我們在 survey/__init__.py 中設定資料函式庫:

import os
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy

BASE_DIR = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = \
    'sqlite:///' + os.path.join(BASE_DIR, 'app.db')
db = SQLAlchemy(app)

這段程式碼完成了幾件重要的事:

  • 建立 Flask 應用程式例項
  • 設定 SQLite 資料函式庫徑
  • 初始化 SQLAlchemy 資料函式庫

建立調查模型

接下來在 survey/models.py 中定義 Question 模型:

class Question(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    question_text = db.Column(db.String(200))
    number_of_yes_votes = db.Column(db.Integer, default=0)
    number_of_no_votes = db.Column(db.Integer, default=0)
    number_of_maybe_votes = db.Column(db.Integer, default=0)
    
    def __init__(self,
                question_text,
                number_of_yes_votes=0,
                number_of_no_votes=0,
                number_of_maybe_votes=0):
        self.question_text = question_text
        self.number_of_yes_votes = number_of_yes_votes
        self.number_of_maybe_votes = number_of_maybe_votes
        self.number_of_no_votes = number_of_no_votes
        
    def vote(self, vote_type):
        if vote_type == 'yes':
            self.number_of_yes_votes += 1
        elif vote_type == 'no':
            self.number_of_no_votes += 1
        elif vote_type == 'maybe':
            self.number_of_maybe_votes += 1
        else:
            raise Exception("Invalid vote type")

這個模型包含:

  • 五個欄位:id、問題文字,以及三種投票選項的計數
  • 建構子方法,用於初始化問題和投票計數
  • vote() 方法,根據投票類別增加相應的計數器

建立資料函式庫

定義好模型後,需要在資料函式庫立對應的表格。這通常在應用程式啟動前執行:

# 在 runserver.py 中
from survey import db
db.create_all()

資料函式庫操作

SQLAlchemy ORM 提供了簡潔的方式來執行 CRUD (建立、讀取、更新、刪除) 操作:

建立資料

question = Question("Are you an American?")
db.session.add(question)
db.session.commit()

讀取資料

# 取得所有問題
Question.query.all()

# 根據 ID 取得特定問題
Question.query.get(1)

更新資料

question = Question.query.get(1)
question.vote('yes')
db.session.add(question)
db.session.commit()

刪除資料

question = Question.query.get(1)
db.session.delete(question)
db.session.commit()

Flask 檢視函式實作

檢視函式負責處理 HTTP 請求並回傳適當的回應。以下是調查應用程式的主要檢視實作:

顯示所有問題

from flask import render_template
from survey import app
from survey.models import Question

@app.route('/', methods=['GET'])
def home():
    questions = Question.query.all()
    context = {'questions': questions,
              'number_of_questions': len(questions)}
    return render_template('index.html',
                         **context)

新問題表單

@app.route('/questions/new', methods=['GET'])
def new_questions():
    return render_template('new.html')

建立新問題

@app.route('/questions', methods=['POST'])
def create_questions():
    if request.form["question_text"].strip() != "":
        new_question = Question(question_text=request.form["question_text"])
        db.session.add(new_question)
        db.session.commit()
        message = "Succefully added a new poll!"
    else:
        message = "Poll question should not be an empty string!"
    context = {'questions': Question.query.all(),
              'message': message}
    return render_template('index.html',
                         **context)

顯示特定問題

@app.route('/questions/<int:question_id>', methods=['GET'])
def show_questions(question_id):
    context = {'question': Question.query.get(question_id)}
    return render_template('show.html',
                         **context)

更新問題

@app.route('/questions/<int:question_id>', methods=['PUT'])
def update_questions(question_id):
    question = Question.query.get(question_id)
    if request.form["question_text"].strip() != "":
        question.question_text = request.form["question_text"]
        db.session.add(question)
        db.session.commit()
        message = "Successfully updated a poll!"
    else:
        message = "Question cannot be empty!"
    context = {'question': question,
              'message': message}
    return render_template('show.html',
                         **context)

刪除問題

@app.route('/questions/<int:question_id>', methods=['DELETE'])
def delete_questions(question_id):
    question = Question.query.get(question_id)
    db.session.delete(question)
    db.session.commit()
    context = {'questions': Question.query.all(),
              'message': 'Successfully deleted'}
    return render_template('index.html',
                         **context)

投票表單

@app.route('/questions/<int:question_id>/vote', methods=['GET'])
def new_vote_questions(question_id):
    question = Question.query.get(question_id)
    context = {'question': question}
    return render_template('vote.html',
                         **context)

投票處理

@app.route('/questions/<int:question_id>/vote', methods=['POST'])
def vote_questions(question_id):
    question = Question.query.get(question_id)
    vote_type = request.form["vote_type"]
    question.vote(vote_type)
    db.session.add(question)
    db.session.commit()
    context = {'question': question}
    return render_template('show.html', **context)