使用 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>
分析這個結構,我們可以得出以下理解:
- 每個網頁包含一個單字列表
- 每個單字列表有多個單字群組,它們都定義在相同的 div 標籤中
- 所有單字群組中的單字都被描述在具有
wordlist
類別屬性的表格中 - 表格中的每一行 (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則專門負責第二步。這種組合在我多年的爬蟲開發經驗中,證明是最為靈活與高效的方案。
爬蟲工作流程的設計思路
當我設計網路爬蟲系統時,通常遵循這樣的工作流程:
- 識別目標URL並生成URL列表
- 使用Requests取得網頁資源
- 透過BeautifulSoup解析HTML內容
- 擷取所需的資料元素
- 將資料轉換為結構化格式並儲存
這個流程不僅有邏輯性,也便於後續的維護和擴充套件。接下來,我們將透過一個實際的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的主要優勢包括:
- 內建開發伺服器:便於開發和測試
- 簡易的錯誤記錄:具有互動式網頁除錯器
- RESTful API支援:路由裝飾器可接受HTTP方法作為引數
- Jinja2範本引擎:靈活的範本渲染系統
- Session物件:儲存使用者工作階段資訊
- 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的三個基本步驟:
- 建立WSGI應使用案例項
- 定義路由方法
- 啟動應用伺服器
當執行這段程式碼時,存取根路徑("/")將顯示"Hello Guest!"。
環境設定:使用虛擬環境
在開始Flask應用開發前,建立一個隔離的開發環境是最佳實踐。以下是使用virtualenvwrapper設定虛擬環境的步驟:
安裝virtualenvwrapper:
$ pip install virtualenvwrapper
設定環境變數:
$ export WORKON_HOME=~/Envs
建立工作目錄:
$ mkdir -p $WORKON_HOME
啟用shell指令碼:
$ source /usr/local/bin/virtualenvwrapper.sh
建立新的虛擬環境:
$ mkvirtualenv survey
安裝所需套件:
(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應用將具有以下功能:
- 建立調查問題
- 檢視所有問題列表
- 檢視特定問題
- 修改問題
- 刪除問題
- 為問題投票
每個問題將包含以下資料欄位:
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 |
---|---|---|
檢視所有問題 | GET | http://[hostname:port]/ |
建立調查問題 | POST | http://[hostname:port]/questions |
檢視特定問題 | GET | http://[hostname:port]/questions/[question_id] |
修改問題 | PUT | http://[hostname:port]/questions/[question_id] |
刪除問題 | DELETE | http://[hostname:port]/questions/[question_id] |
為問題投票 | POST | http://[hostname:port]/questions/[question_id]/vote |
投票表單 | GET | http://[hostname:port]/questions/[question_id]/vote |
新問題表單 | GET | http://[hostname:port]/questions/new |
這種設計遵循 RESTful 原則,讓 URL 直觀反映資源和操作,使 API 更容易理解和維護。
使用 Flask-SQLAlchemy 定義資料模型
當我開發較複雜的應用程式時,總是傾向使用 ORM (物件關聯對映) 來簡化資料函式庫。Flask-SQLAlchemy 是 Flask 的擴充套件,為 Flask 應用程式提供 SQLAlchemy 支援。
資料模型定義的三個步驟
- 建立資料函式庫
- 使用該例項定義模型
- 呼叫資料函式庫的方法來建立資料表
建立資料函式庫
首先,我們在 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)