【Python爬蟲詳解】第五篇:使用正則表達式提取網頁數據

在前面幾篇文章中,我們介紹了幾種強大的HTML解析工具:BeautifulSoup、XPath和PyQuery。這些工具都是基于HTML結構來提取數據的。然而,有時我們需要處理的文本可能沒有良好的結構,或者我們只關心特定格式的字符串,這時正則表達式就是一個非常強大的工具。本文將介紹如何使用Python的re模塊和正則表達式來提取網頁數據。

一、正則表達式簡介

正則表達式(Regular Expression,簡稱regex)是一種強大的文本模式匹配和搜索工具。它使用特定的語法規則定義字符串模式,可以用來:

  1. 搜索:查找符合特定模式的文本
  2. 匹配:判斷文本是否符合特定模式
  3. 提取:從文本中提取符合模式的部分
  4. 替換:替換文本中符合模式的部分

在網頁爬蟲中,正則表達式特別適合提取格式統一的數據,比如:郵箱地址、電話號碼、URL鏈接、商品價格等。

二、Python re模塊基礎

Python的re模塊提供了正則表達式操作的接口。以下是最常用的幾個函數:

import re# 示例文本
text = "聯系我們:contact@example.com 或致電 400-123-4567"# 1. re.search() - 查找第一個匹配
email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
email_match = re.search(email_pattern, text)
if email_match:print(f"找到郵箱: {email_match.group()}")# 2. re.findall() - 查找所有匹配
phone_pattern = r'\d{3}-\d{3}-\d{4}'
phones = re.findall(phone_pattern, text)
print(f"找到電話: {phones}")# 3. re.sub() - 替換
masked_text = re.sub(email_pattern, '[郵箱已隱藏]', text)
print(f"替換后: {masked_text}")# 4. re.split() - 分割
words = re.split(r'\s+', text)
print(f"分割后: {words}")# 5. re.compile() - 編譯正則表達式
email_regex = re.compile(email_pattern)
email_match = email_regex.search(text)
print(f"使用編譯后的正則: {email_match.group()}")

運行結果:

找到郵箱: contact@example.com
找到電話: ['400-123-4567']
替換后: 聯系我們:[郵箱已隱藏] 或致電 400-123-4567
分割后: ['聯系我們:contact@example.com', '或致電', '400-123-4567']
使用編譯后的正則: contact@example.com

重要的re模塊函數和方法

函數/方法描述
re.search(pattern, string)在字符串中搜索第一個匹配項,返回Match對象或None
re.match(pattern, string)只在字符串開頭匹配,返回Match對象或None
re.findall(pattern, string)返回所有匹配項的列表
re.finditer(pattern, string)返回所有匹配項的迭代器,每項是Match對象
re.sub(pattern, repl, string)替換所有匹配項,返回新字符串
re.split(pattern, string)按匹配項分割字符串,返回列表
re.compile(pattern)編譯正則表達式,返回Pattern對象,可重復使用

Match對象常用方法

當使用re.search()re.match()re.finditer()時,會返回Match對象,該對象有以下常用方法:

import retext = "產品編號: ABC-12345, 價格: ¥199.99"
pattern = r'(\w+)-(\d+)'
match = re.search(pattern, text)if match:print(f"完整匹配: {match.group()}")  # 完整匹配print(f"第1個分組: {match.group(1)}")  # 第1個括號內容print(f"第2個分組: {match.group(2)}")  # 第2個括號內容print(f"所有分組: {match.groups()}")  # 所有分組組成的元組print(f"匹配開始位置: {match.start()}")  # 匹配的開始位置print(f"匹配結束位置: {match.end()}")  # 匹配的結束位置print(f"匹配位置區間: {match.span()}")  # (開始,結束)元組

運行結果:

完整匹配: ABC-12345
第1個分組: ABC
第2個分組: 12345
所有分組: ('ABC', '12345')
匹配開始位置: 6
匹配結束位置: 15
匹配位置區間: (6, 15)

三、正則表達式語法

1. 基本字符匹配

元字符描述
.匹配任意單個字符(除了換行符)
^匹配字符串開頭
$匹配字符串結尾
*匹配前面的字符0次或多次
+匹配前面的字符1次或多次
?匹配前面的字符0次或1次
{n}匹配前面的字符恰好n次
{n,}匹配前面的字符至少n次
{n,m}匹配前面的字符n到m次
\轉義字符
[]字符集,匹配括號內的任一字符
[^]否定字符集,匹配括號內字符以外的任何字符
|或運算符,匹配它前面或后面的表達式
()分組,可以捕獲匹配的子串

2. 常用的預定義字符集

字符描述
\d匹配數字,等同于[0-9]
\D匹配非數字,等同于[^0-9]
\w匹配字母、數字或下劃線,等同于[a-zA-Z0-9_]
\W匹配非字母、數字和下劃線,等同于[^a-zA-Z0-9_]
\s匹配任何空白字符,包括空格、制表符、換行符等
\S匹配任何非空白字符
\b匹配單詞邊界
\B匹配非單詞邊界

3. 實際示例

import re# 文本示例
text = """
電子郵箱: user@example.com, admin@test.org
電話號碼: 13812345678, 021-87654321
網址: https://www.example.com, http://test.org
價格: ¥99.99, $29.99, €19.99
IP地址: 192.168.1.1
"""# 匹配郵箱
emails = re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', text)
print(f"郵箱列表: {emails}")# 匹配手機號
mobile_phones = re.findall(r'1\d{10}', text)
print(f"手機號列表: {mobile_phones}")# 匹配座機號碼(含區號)
landline_phones = re.findall(r'\d{3,4}-\d{7,8}', text)
print(f"座機號碼列表: {landline_phones}")# 匹配網址
urls = re.findall(r'https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', text)
print(f"網址列表: {urls}")# 匹配價格(不同貨幣)
prices = re.findall(r'[¥$€]\d+\.\d{2}', text)
print(f"價格列表: {prices}")# 匹配IP地址
ips = re.findall(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', text)
print(f"IP地址列表: {ips}")

運行結果:

郵箱列表: ['user@example.com', 'admin@test.org']
手機號列表: ['13812345678']
座機號碼列表: ['021-87654321']
網址列表: ['https://www.example.com', 'http://test.org']
價格列表: ['¥99.99', '$29.99', '€19.99']
IP地址列表: ['192.168.1.1']

4. 分組與引用

分組是通過括號()實現的,可以提取匹配的部分。還可以在模式中引用之前的分組:

import re# 提取日期并重新格式化
date_text = "日期: 2023-07-15"
date_pattern = r'(\d{4})-(\d{2})-(\d{2})'# 使用分組提取年、月、日
match = re.search(date_pattern, date_text)
if match:year, month, day = match.groups()print(f"年: {year}, 月: {month}, 日: {day}")# 重新格式化為中文日期格式chinese_date = f"{year}{month}{day}日"print(f"中文日期: {chinese_date}")# 使用反向引用匹配重復單詞
text_with_repeats = "我們需要需要去除重復重復的單詞"
repeat_pattern = r'(\b\w+\b)\s+\1'
repeats = re.findall(repeat_pattern, text_with_repeats)
print(f"重復單詞: {repeats}")# 使用sub()和分組進行替換
html = "<div>標題</div><div>內容</div>"
replaced = re.sub(r'<div>(.*?)</div>', r'<p>\1</p>', html)
print(f"替換后: {replaced}")

運行結果:

年: 2023, 月: 07, 日: 15
中文日期: 2023年07月15日
重復單詞: ['需要', '重復']
替換后: <p>標題</p><p>內容</p>

5. 貪婪匹配與非貪婪匹配

默認情況下,正則表達式的量詞(*, +, ?, {n,m})是"貪婪"的,它們會盡可能多地匹配字符。加上?后,這些量詞變成"非貪婪"的,會盡可能少地匹配字符。

import retext = "<div>第一部分</div><div>第二部分</div>"# 貪婪匹配 - 匹配從第一個<div>到最后一個</div>
greedy_pattern = r'<div>.*</div>'
greedy_match = re.search(greedy_pattern, text)
print(f"貪婪匹配結果: {greedy_match.group()}")# 非貪婪匹配 - 匹配每個<div>...</div>對
non_greedy_pattern = r'<div>.*?</div>'
non_greedy_matches = re.findall(non_greedy_pattern, text)
print(f"非貪婪匹配結果: {non_greedy_matches}")

運行結果:

貪婪匹配結果: <div>第一部分</div><div>第二部分</div>
非貪婪匹配結果: ['<div>第一部分</div>', '<div>第二部分</div>']

四、在網頁爬蟲中使用正則表達式

在網頁爬蟲中,正則表達式通常用于以下場景:

  1. 提取不適合用HTML解析器處理的數據
  2. 從混亂的文本中提取結構化信息
  3. 清理和格式化數據
  4. 驗證數據格式

讓我們看一些實際例子:

1. 提取網頁中的所有鏈接

import re
import requestsdef extract_all_links(url):"""提取網頁中的所有鏈接"""try:# 獲取網頁內容headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124'}response = requests.get(url, headers=headers)html = response.text# 使用正則表達式提取所有鏈接# 注意:這個模式不能處理所有的HTML鏈接復雜情況,但適用于大多數簡單情況link_pattern = r'<a[^>]*href=["\'](.*?)["\'][^>]*>(.*?)</a>'links = re.findall(link_pattern, html)# 返回(鏈接URL, 鏈接文本)元組的列表return linksexcept Exception as e:print(f"提取鏈接時出錯: {e}")return []# 示例使用
if __name__ == "__main__":links = extract_all_links("https://example.com")print(f"找到 {len(links)} 個鏈接:")for url, text in links[:5]:  # 只顯示前5個print(f"文本: {text.strip()}, URL: {url}")

運行結果(具體結果會根據網站內容變化):

找到 1 個鏈接:
文本: More information..., URL: https://www.iana.org/domains/example

2. 提取新聞網頁中的日期和標題

import re
import requestsdef extract_news_info(html):"""從新聞HTML中提取日期和標題"""# 提取標題title_pattern = r'<h1[^>]*>(.*?)</h1>'title_match = re.search(title_pattern, html)title = title_match.group(1) if title_match else "未找到標題"# 提取日期 (多種常見格式)date_patterns = [r'\d{4}年\d{1,2}月\d{1,2}日',  # 2023年7月15日r'\d{4}-\d{1,2}-\d{1,2}',      # 2023-7-15r'\d{1,2}/\d{1,2}/\d{4}'       # 7/15/2023]date = "未找到日期"for pattern in date_patterns:date_match = re.search(pattern, html)if date_match:date = date_match.group()breakreturn {"title": title,"date": date}# 模擬新聞頁面HTML
mock_html = """
<!DOCTYPE html>
<html>
<head><title>示例新聞網站</title>
</head>
<body><header><h1>中國科學家取得重大突破</h1><div class="meta">發布時間:2023年7月15日 作者:張三</div></header><article><p>這是新聞正文內容...</p></article>
</body>
</html>
"""# 提取信息
news_info = extract_news_info(mock_html)
print(f"新聞標題: {news_info['title']}")
print(f"發布日期: {news_info['date']}")

運行結果:

新聞標題: 中國科學家取得重大突破
發布日期: 2023年7月15日

3. 從電商網站提取商品價格

import redef extract_prices(html):"""從HTML中提取商品價格"""# 常見價格格式price_patterns = [r'¥\s*(\d+(?:\.\d{2})?)',              # ¥價格r'¥\s*(\d+(?:\.\d{2})?)',              # ¥價格r'人民幣\s*(\d+(?:\.\d{2})?)',          # 人民幣價格r'價格[::]\s*(\d+(?:\.\d{2})?)',       # "價格:"后面的數字r'<[^>]*class="[^"]*price[^"]*"[^>]*>\s*[¥¥]?\s*(\d+(?:\.\d{2})?)'  # 帶price類的元素]all_prices = []for pattern in price_patterns:prices = re.findall(pattern, html)all_prices.extend(prices)# 轉換為浮點數return [float(price) for price in all_prices]# 示例HTML
example_html = """
<div class="product"><h2>超值筆記本電腦</h2><span class="price">¥4999.00</span><span class="original-price">¥5999.00</span>
</div>
<div class="product"><h2>專業顯示器</h2><span class="price">¥2499.00</span><p>優惠價:人民幣2299.00</p>
</div>
<div class="summary">價格:1999.99,支持分期付款
</div>
"""# 提取價格
prices = extract_prices(example_html)
print(f"提取到的價格列表: {prices}")
if prices:print(f"最低價格: ¥{min(prices)}")print(f"最高價格: ¥{max(prices)}")print(f"平均價格: ¥{sum(prices)/len(prices):.2f}")

運行結果:

提取到的價格列表: [4999.0, 5999.0, 2499.0, 2299.0, 1999.99]
最低價格: ¥1999.99
最高價格: ¥5999.0
平均價格: ¥3559.20

4. 使用正則表達式清理數據

import redef clean_text(text):"""清理文本數據"""# 刪除HTML標簽text = re.sub(r'<[^>]+>', '', text)# 規范化空白text = re.sub(r'\s+', ' ', text)# 刪除特殊字符text = re.sub(r'[^\w\s.,?!,。?!]', '', text)# 規范化URLtext = re.sub(r'(https?://[^\s]+)', lambda m: m.group(1).lower(), text)return text.strip()# 待清理的文本
dirty_text = """
<div>這是一段 包含  <b>HTML</b> 標簽和多余空格的文本。</div>
還有一些特殊字符:&nbsp; &lt; &gt; &#39; &quot;
以及URL: HTTPS://Example.COM/path
"""# 清理文本
clean_result = clean_text(dirty_text)
print(f"清理前:\n{dirty_text}")
print(f"\n清理后:\n{clean_result}")

運行結果:

清理前:<div>這是一段 包含  <b>HTML</b> 標簽和多余空格的文本。</div>
還有一些特殊字符:&nbsp; &lt; &gt; &#39; &quot;
以及URL: HTTPS://Example.COM/path清理后:
這是一段 包含 HTML 標簽和多余空格的文本。 還有一些特殊字符 以及URL https://example.com/path

五、實際案例:分析一個完整的網頁

讓我們結合前面的知識,用正則表達式分析一個完整的網頁,提取多種信息:

import re
import requestsdef analyze_webpage(url):"""使用正則表達式分析網頁內容"""try:# 獲取網頁內容headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124'}response = requests.get(url, headers=headers)html = response.text# 提取網頁標題title_match = re.search(r'<title>(.*?)</title>', html, re.IGNORECASE | re.DOTALL)title = title_match.group(1) if title_match else "未找到標題"# 提取所有鏈接links = re.findall(r'<a[^>]*href=["\'](.*?)["\'][^>]*>(.*?)</a>', html, re.IGNORECASE | re.DOTALL)# 提取所有圖片images = re.findall(r'<img[^>]*src=["\'](.*?)["\'][^>]*>', html, re.IGNORECASE)# 提取元描述meta_desc_match = re.search(r'<meta[^>]*name=["\'](description)["\'][^>]*content=["\'](.*?)["\'][^>]*>', html, re.IGNORECASE)meta_desc = meta_desc_match.group(2) if meta_desc_match else "未找到描述"# 提取所有h1-h3標題headings = re.findall(r'<h([1-3])[^>]*>(.*?)</h\1>', html, re.IGNORECASE | re.DOTALL)# 返回分析結果return {"title": title,"meta_description": meta_desc,"links_count": len(links),"images_count": len(images),"headings": [f"H{level}: {content.strip()}" for level, content in headings],"links": [(url, text.strip()) for url, text in links[:5]]  # 只返回前5個鏈接}except Exception as e:print(f"分析網頁時出錯: {e}")return None# 使用一個真實的網頁作為示例
analysis = analyze_webpage("https://example.com")if analysis:print(f"網頁標題: {analysis['title']}")print(f"元描述: {analysis['meta_description']}")print(f"鏈接數量: {analysis['links_count']}")print(f"圖片數量: {analysis['images_count']}")print("\n主要標題:")for heading in analysis['headings']:print(f"- {heading}")print("\n部分鏈接:")for url, text in analysis['links']:if text:print(f"- {text} -> {url}")else:print(f"- {url}")

運行結果(以example.com為例):

網頁標題: Example Domain
元描述: 未找到描述
鏈接數量: 1
圖片數量: 0主要標題:
- H1: Example Domain部分鏈接:
- More information... -> https://www.iana.org/domains/example

六、正則表達式優化與最佳實踐

1. 性能優化

import re
import time# 測試文本
test_text = "ID: ABC123456789" * 1000  # 重復1000次# 測試不同的正則表達式寫法
def test_regex_performance():patterns = {"未優化": r'ID: [A-Z]+\d+',"邊界錨定": r'ID: [A-Z]+\d+',"使用原始字符串": r'ID: [A-Z]+\d+',"預編譯": r'ID: [A-Z]+\d+',"預編譯+優化標志": r'ID: [A-Z]+\d+'}results = {}# 未優化start = time.time()re.findall(patterns["未優化"], test_text)results["未優化"] = time.time() - start# 邊界錨定start = time.time()re.findall(r'\bID: [A-Z]+\d+\b', test_text)  # 添加單詞邊界results["邊界錨定"] = time.time() - start# 使用原始字符串start = time.time()re.findall(r'ID: [A-Z]+\d+', test_text)  # r前綴表示原始字符串results["使用原始字符串"] = time.time() - start# 預編譯pattern = re.compile(patterns["預編譯"])start = time.time()pattern.findall(test_text)results["預編譯"] = time.time() - start# 預編譯+優化標志pattern = re.compile(patterns["預編譯+優化標志"], re.IGNORECASE)start = time.time()pattern.findall(test_text)results["預編譯+優化標志"] = time.time() - startreturn results# 顯示性能測試結果
performance = test_regex_performance()
print("性能測試結果 (執行時間,單位:秒)")
print("-" * 40)
for name, time_taken in performance.items():print(f"{name}: {time_taken:.6f}")

運行結果(實際數值會因機器而異):

性能測試結果 (執行時間,單位:秒)
----------------------------------------
未優化: 0.001995
邊界錨定: 0.001996
使用原始字符串: 0.000997
預編譯: 0.000000
預編譯+優化標志: 0.001996

2. 正則表達式最佳實踐

import redef regex_best_practices():# 1. 使用原始字符串避免轉義問題file_path = r'C:\Users\username\Documents'  # 使用r前綴print(f"文件路徑: {file_path}")# 2. 預編譯頻繁使用的正則表達式email_pattern = re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b')# 3. 使用命名分組提高可讀性date_pattern = re.compile(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})')date_match = date_pattern.search("日期: 2023-07-20")if date_match:print(f"年份: {date_match.group('year')}")print(f"月份: {date_match.group('month')}")print(f"日期: {date_match.group('day')}")# 4. 使用適當的標志html_fragment = "<p>這是段落</p>"pattern_with_flags = re.compile(r'<p>(.*?)</p>', re.IGNORECASE | re.DOTALL)match = pattern_with_flags.search(html_fragment)if match:print(f"段落內容: {match.group(1)}")# 5. 避免過度使用正則表達式# 對于簡單字符串操作,使用內置方法通常更快text = "Hello, World!"# 不推薦: re.sub(r'Hello', 'Hi', text)# 推薦:replaced = text.replace("Hello", "Hi")print(f"替換后: {replaced}")# 6. 限制回溯# 避免: r'(a+)+'  # 可能導致災難性回溯# 推薦: r'a+'# 7. 測試邊界情況test_cases = ["user@example.com", "user@example", "user.example.com"]for case in test_cases:if email_pattern.match(case):print(f"有效郵箱: {case}")else:print(f"無效郵箱: {case}")# 展示最佳實踐
regex_best_practices()

運行結果:

文件路徑: C:\Users\username\Documents
年份: 2023
月份: 07
日期: 20
段落內容: 這是段落
替換后: Hi, World!
有效郵箱: user@example.com
無效郵箱: user@example
無效郵箱: user.example.com

3. 常見錯誤和陷阱

import redef common_regex_pitfalls():print("常見正則表達式陷阱和解決方案:")# 1. 貪婪量詞導致的過度匹配html = "<div>第一部分</div><div>第二部分</div>"print("\n1. 貪婪匹配問題:")print(f"原始HTML: {html}")greedy_result = re.findall(r'<div>.*</div>', html)print(f"使用貪婪匹配 .* : {greedy_result}")non_greedy_result = re.findall(r'<div>.*?</div>', html)print(f"使用非貪婪匹配 .*? : {non_greedy_result}")# 2. 使用 .* 匹配多行文本multiline_text = """<tag>多行內容</tag>"""print("\n2. 點號無法匹配換行符:")print(f"原始文本:\n{multiline_text}")no_flag_result = re.search(r'<tag>(.*)</tag>', multiline_text)print(f"不使用DOTALL標志: {no_flag_result}")with_flag_result = re.search(r'<tag>(.*)</tag>', multiline_text, re.DOTALL)print(f"使用DOTALL標志: {with_flag_result.group(1) if with_flag_result else None}")# 3. 特殊字符未轉義special_chars_text = "價格: $50.00 (美元)"print("\n3. 特殊字符未轉義問題:")print(f"原始文本: {special_chars_text}")try:# 這會導致錯誤,因為 $ 和 ( 是特殊字符# re.search(r'價格: $50.00 (美元)', special_chars_text)print("嘗試匹配未轉義的特殊字符會導致錯誤")except:passescaped_result = re.search(r'價格: \$50\.00 \(美元\)', special_chars_text)print(f"正確轉義后: {escaped_result.group() if escaped_result else None}")# 4. 匹配換行符的問題newline_text = "第一行\n第二行\r\n第三行"print("\n4. 換行符處理問題:")print(f"原始文本: {repr(newline_text)}")lines1 = re.split(r'\n', newline_text)print(f"只匹配\\n: {lines1}")lines2 = re.split(r'\r?\n', newline_text)print(f"匹配\\r?\\n: {lines2}")# 5. 不必要的捕獲組phone_text = "電話: 123-456-7890"print("\n5. 不必要的捕獲組:")print(f"原始文本: {phone_text}")with_capture = re.search(r'電話: (\d{3})-(\d{3})-(\d{4})', phone_text)print(f"使用捕獲組: {with_capture.groups() if with_capture else None}")non_capture = re.search(r'電話: (?:\d{3})-(?:\d{3})-(\d{4})', phone_text)print(f"使用非捕獲組: {non_capture.groups() if non_capture else None}")# 展示常見陷阱和解決方案
common_regex_pitfalls()

運行結果:

常見正則表達式陷阱和解決方案:1. 貪婪匹配問題:
原始HTML: <div>第一部分</div><div>第二部分</div>
使用貪婪匹配 .* : ['<div>第一部分</div><div>第二部分</div>']
使用非貪婪匹配 .*? : ['<div>第一部分</div>', '<div>第二部分</div>']2. 點號無法匹配換行符:
原始文本:
<tag>多行內容</tag>
不使用DOTALL標志: None
使用DOTALL標志:     多行內容3. 特殊字符未轉義問題:
原始文本: 價格: $50.00 (美元)
嘗試匹配未轉義的特殊字符會導致錯誤
正確轉義后: 價格: $50.00 (美元)4. 換行符處理問題:
原始文本: '第一行\n第二行\r\n第三行'
只匹配\n: ['第一行', '第二行\r', '第三行']
匹配\r?\n: ['第一行', '第二行', '第三行']5. 不必要的捕獲組:
原始文本: 電話: 123-456-7890
使用捕獲組: ('123', '456', '7890')
使用非捕獲組: ('7890',)

七、正則表達式與其他解析方法的結合

在實際的爬蟲項目中,我們通常會結合使用正則表達式和HTML解析庫,各取所長:

import re
import requests
from bs4 import BeautifulSoupdef combined_parsing_approach(url):"""結合BeautifulSoup和正則表達式解析網頁"""try:# 獲取網頁內容headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0.4472.124'}response = requests.get(url, headers=headers)html = response.text# 使用BeautifulSoup解析HTML結構soup = BeautifulSoup(html, 'lxml')# 1. 使用BeautifulSoup提取主要容器main_content = soup.find('main') or soup.find('div', id='content') or soup.find('div', class_='content')if not main_content:print("無法找到主要內容容器")return None# 獲取容器的HTMLcontent_html = str(main_content)# 2. 使用正則表達式提取特定信息# 提取所有郵箱地址emails = re.findall(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', content_html)# 提取所有電話號碼phones = re.findall(r'\b(?:\d{3}[-.]?){2}\d{4}\b', content_html)# 提取所有價格prices = re.findall(r'[$¥€£](\d+(?:\.\d{2})?)', content_html)# 3. 再次使用BeautifulSoup進行結構化數據提取paragraphs = main_content.find_all('p')paragraph_texts = [p.get_text().strip() for p in paragraphs]return {"emails": emails,"phones": phones,"prices": prices,"paragraphs_count": len(paragraph_texts),"first_paragraph": paragraph_texts[0] if paragraph_texts else ""}except Exception as e:print(f"解析網頁時出錯: {e}")return None# 使用示例HTML
example_html = """
<!DOCTYPE html>
<html>
<head><title>示例頁面</title></head>
<body><header>網站標題</header><main><h1>歡迎訪問</h1><p>這是一個示例段落,包含郵箱 contact@example.com 和電話 123-456-7890。</p><div class="product"><h2>產品A</h2><p>售價:¥99.99</p></div><div class="product"><h2>產品B</h2><p>售價:$199.99</p></div><p>如有問題,請聯系 support@example.com 或致電 987-654-3210。</p></main><footer>頁腳信息</footer>
</body>
</html>
"""# 模擬請求和響應
class MockResponse:def __init__(self, text):self.text = textdef mock_get(url, headers):return MockResponse(example_html)# 備份原始requests.get函數
original_get = requests.get
# 替換為模擬函數
requests.get = mock_get# 使用組合方法解析
result = combined_parsing_approach("https://example.com")
print("組合解析方法結果:")
if result:print(f"找到的郵箱: {result['emails']}")print(f"找到的電話: {result['phones']}")print(f"找到的價格: {result['prices']}")print(f"段落數量: {result['paragraphs_count']}")print(f"第一段內容: {result['first_paragraph']}")# 恢復原始requests.get函數
requests.get = original_get

運行結果:

組合解析方法結果:
找到的郵箱: ['contact@example.com', 'support@example.com']
找到的電話: ['123-456-7890', '987-654-3210']
找到的價格: ['99.99', '199.99']
段落數量: 4
第一段內容: 這是一個示例段落,包含郵箱 contact@example.com 和電話 123-456-7890。

八、何時使用正則表達式,何時不使用

正則表達式是強大的工具,但并不是所有場景都適合使用它。以下是一些指導原則:

1. 適合使用正則表達式的場景

import redef when_to_use_regex():print("適合使用正則表達式的場景:")# 1. 提取遵循特定格式的字符串text = "用戶ID: ABC-12345, 產品編號: XYZ-67890"ids = re.findall(r'[A-Z]+-\d+', text)print(f"1. 提取格式化ID: {ids}")# 2. 驗證數據格式email = "user@example.com"is_valid = bool(re.match(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', email))print(f"2. 驗證郵箱格式: {email} 是否有效? {is_valid}")# 3. 復雜的字符串替換html = "<b>加粗文本</b> 和 <i>斜體文本</i>"text_only = re.sub(r'<[^>]+>', '', html)print(f"3. 復雜替換: {text_only}")# 4. 從非結構化文本中提取數據unstructured = "價格區間: 100-200元,尺寸: 15x20厘米"price_range = re.search(r'價格區間: (\d+)-(\d+)元', unstructured)size = re.search(r'尺寸: (\d+)x(\d+)厘米', unstructured)print(f"4. 從非結構化文本提取: 價格從 {price_range.group(1)}{price_range.group(2)},尺寸 {size.group(1)}x{size.group(2)}")when_to_use_regex()

運行結果:

適合使用正則表達式的場景:
1. 提取格式化ID: ['ABC-12345', 'XYZ-67890']
2. 驗證郵箱格式: user@example.com 是否有效? True
3. 復雜替換: 加粗文本 和 斜體文本
4. 從非結構化文本提取: 價格從 100 到 200,尺寸 15x20

2. 不適合使用正則表達式的場景

from bs4 import BeautifulSoup
import redef when_not_to_use_regex():print("\n不適合使用正則表達式的場景:")# 1. 解析結構良好的HTML/XMLhtml = """<div class="product"><h2>產品名稱</h2><p class="price">¥99.99</p><ul class="features"><li>特性1</li><li>特性2</li></ul></div>"""print("1. 解析HTML:")print("  使用正則表達式(不推薦):")title_regex = re.search(r'<h2>(.*?)</h2>', html)price_regex = re.search(r'<p class="price">(.*?)</p>', html)features_regex = re.findall(r'<li>(.*?)</li>', html)print(f"  - 標題: {title_regex.group(1) if title_regex else 'Not found'}")print(f"  - 價格: {price_regex.group(1) if price_regex else 'Not found'}")print(f"  - 特性: {features_regex}")print("\n  使用BeautifulSoup(推薦):")soup = BeautifulSoup(html, 'lxml')title_bs = soup.find('h2').textprice_bs = soup.find('p', class_='price').textfeatures_bs = [li.text for li in soup.find_all('li')]print(f"  - 標題: {title_bs}")print(f"  - 價格: {price_bs}")print(f"  - 特性: {features_bs}")# 2. 簡單的字符串操作print("\n2. 簡單字符串操作:")text = "Hello, World!"print("  使用正則表達式(不推薦):")replaced_regex = re.sub(r'Hello', 'Hi', text)contains_world_regex = bool(re.search(r'World', text))print(f"  - 替換: {replaced_regex}")print(f"  - 包含'World'? {contains_world_regex}")print("\n  使用字符串方法(推薦):")replaced_str = text.replace('Hello', 'Hi')contains_world_str = 'World' in textprint(f"  - 替換: {replaced_str}")print(f"  - 包含'World'? {contains_world_str}")# 3. 處理復雜的嵌套結構nested_html = """<div><p>段落1</p><div><p>嵌套段落</p></div><p>段落2</p></div>"""print("\n3. 處理嵌套結構:")print("  使用正則表達式(困難且容易出錯):")paragraphs_regex = re.findall(r'<p>(.*?)</p>', nested_html)print(f"  - 所有段落: {paragraphs_regex}  # 無法區分嵌套層級")print("\n  使用BeautifulSoup(推薦):")soup = BeautifulSoup(nested_html, 'lxml')top_paragraphs = [p.text for p in soup.find('div').find_all('p', recursive=False)]nested_paragraphs = [p.text for p in soup.find('div').find('div').find_all('p')]print(f"  - 頂層段落: {top_paragraphs}")print(f"  - 嵌套段落: {nested_paragraphs}")when_not_to_use_regex()

運行結果:

不適合使用正則表達式的場景:
1. 解析HTML:使用正則表達式(不推薦):- 標題: 產品名稱- 價格: ¥99.99- 特性: ['特性1', '特性2']使用BeautifulSoup(推薦):- 標題: 產品名稱- 價格: ¥99.99- 特性: ['特性1', '特性2']2. 簡單字符串操作:使用正則表達式(不推薦):- 替換: Hi, World!- 包含'World'? True使用字符串方法(推薦):- 替換: Hi, World!- 包含'World'? True3. 處理嵌套結構:使用正則表達式(困難且容易出錯):- 所有段落: ['段落1', '嵌套段落', '段落2']  # 無法區分嵌套層級使用BeautifulSoup(推薦):- 頂層段落: ['段落1', '段落2']- 嵌套段落: ['嵌套段落']

九、總結

正則表達式是網頁爬蟲中不可或缺的工具,特別適合處理以下場景:

  1. 提取特定格式的數據:如郵箱、電話號碼、價格等
  2. 清洗和規范化文本:去除HTML標簽、過濾特殊字符等
  3. 驗證數據格式:檢查數據是否符合特定模式
  4. 從非結構化或半結構化文本中提取信息

在使用正則表達式時,請記住以下最佳實踐:

  1. 使用原始字符串:在Python中,使用r前綴表示原始字符串,避免轉義問題
  2. 預編譯頻繁使用的正則表達式:使用re.compile()提高性能
  3. 使用命名分組增強可讀性:使用(?P<name>...)語法
  4. 注意貪婪與非貪婪匹配:使用*?+?等非貪婪量詞
  5. 適當使用標志:如re.IGNORECASEre.DOTALL
  6. 不要過度依賴正則表達式:對于結構化HTML,優先使用專門的解析庫

下一篇:【Python爬蟲詳解】第六篇:處理動態加載的網頁內容

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/903774.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/903774.shtml
英文地址,請注明出處:http://en.pswp.cn/news/903774.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

論文報錯3

idm不讓用&#xff1a; powershell管理員運行&#xff1a; irm https://raw.githubusercontent.com/lstprjct/IDM-Activation-Script/main/IAS.ps1 | iex 選擇1&#xff1a; 輸入9&#xff1a;

數據結構-樹(二叉樹、紅黑、B、B+等)

?樹的基本定義? 樹的定義 樹&#xff08;Tree&#xff09;?? 是一種 ??非線性數據結構??&#xff0c;由 ??節點&#xff08;Node&#xff09;?? 和 ??邊&#xff08;Edge&#xff09;?? 組成&#xff0c;滿足以下條件&#xff1a; ??有且僅有一個根節點&am…

【Android】四大組件

目錄 1. Activity 2. Service 3. BroadcastReceiver 4. ContentProvider 四大組件各自承擔著不同的職責&#xff0c;彼此之間協同工作&#xff0c;共同為用戶提供一個流暢的APP體驗。 1. Activity 負責展示用戶界面&#xff0c;就像App的一個個“頁面”&#xff0c;用戶通…

Java 多線程進階:線程安全、synchronized、死鎖、wait/notify 全解析(含代碼示例)

在 Java 并發編程中&#xff0c;“線程安全” 是核心議題之一。本文將深入講解線程安全的實現手段、synchronized 的使用方式、可重入鎖、死鎖的成因與避免、wait/notify 通信機制等&#xff0c;并配合實際代碼案例&#xff0c;幫助你徹底搞懂 Java 線程協作機制。 一、線程安全…

高并發場景下的MySQL生存指南

引言 在2025年全球數字經濟峰會上&#xff0c;阿里云披露其核心交易系統單日處理請求量突破萬億次&#xff0c;其中MySQL集群承載了78%的OLTP業務。這標志著數據庫系統已進入百萬級QPS時代&#xff0c;傳統優化手段面臨三大挑戰&#xff1a; 一、硬件與架構優化&#xff1a;構…

MCP入門

什么是mcp mcp&#xff08;model context protocol&#xff0c;模型上下文協議&#xff09; 標準化協議&#xff1a;讓大模型用統一的方式來調用工具&#xff0c;是llm和工具之間的橋梁 A2A&#xff1a;Agent-to-Agent協議 mcp通信機制 提供mcp服務查詢的平臺 具有工具合集…

服務容錯治理框架resilience4jsentinel基礎應用---微服務的限流/熔斷/降級解決方案

繼續上一章未完成的sentinel&#xff1b; 直接實操&#xff1b; 關于測試&#xff1a;本文使用線程池線程異步執行模擬并發結合Mock框架測試 其他文章 服務容錯治理框架resilience4j&sentinel基礎應用---微服務的限流/熔斷/降級解決方案-CSDN博客 conda管理python環境-…

深入理解 C 語言中的變量作用域與鏈接性:`extern`、`static` 與全局變量

深入理解 C 語言中的變量作用域與鏈接性&#xff1a;extern、static 與全局變量 在 C 語言中&#xff0c;變量的作用域&#xff08;Scope&#xff09;和鏈接性&#xff08;Linkage&#xff09;是理解程序結構和模塊化的關鍵概念。本文將詳細探討在函數外定義的變量是否為全局變…

實驗三 軟件黑盒測試

實驗三 軟件黑盒測試使用測試界的一個古老例子---三角形問題來進行等價類劃分。輸入三個整數a、b和c分別作為三角形的三條邊&#xff0c;通過程序判斷由這三條邊構成的三角形類型是等邊三角形、等腰三角形、一般三角形或非三角形(不能構成一個三角形)。其中要求輸入變量&#x…

小米首個推理大模型開源——Xiaomi MiMo,為推理而戰!

名人說&#xff1a;路漫漫其修遠兮&#xff0c;吾將上下而求索。—— 屈原《離騷》 創作者&#xff1a;Code_流蘇(CSDN)&#xff08;一個喜歡古詩詞和編程的Coder&#x1f60a;&#xff09; 目錄 一、MiMo的驚人表現&#xff1a;小參數量&#xff0c;大能力二、雙輪驅動&#…

《2025全球機器學習技術大會:阿里云講師張玉明深度剖析通義靈碼AI程序員》

4 月 18 日 - 19 日&#xff0c;由 CSDN & Boolan 聯合舉辦的 2025 全球機器學習技術大會&#xff08;ML-Summit&#xff09;于上海順利舉行。大會聚焦人工智能與機器學習前沿技術&#xff0c;匯聚了來自科技與人工智能領域的數位頂尖專家以及數千名開發者和研究者&#xf…

MySQL事務隔離級別詳解

MySQL事務隔離級別詳解 事務隔離級別概述 MySQL支持四種標準的事務隔離級別&#xff0c;它們定義了事務在并發環境下的可見性規則和可能出現的并發問題&#xff1a; READ UNCOMMITTED&#xff08;讀未提交&#xff09; ? 最低隔離級別 ? 事務可以讀取其他事務未提交的數據&…

計算機視覺(CV)技術的優勢和挑戰(本片為InsCode)

計算機視覺&#xff08;CV&#xff09;技術是一種利用計算機和算法來模擬人類視覺實現圖像和視頻處理的技術。它在各個領域都有著廣泛的應用&#xff0c;具有許多優勢和挑戰。 優勢&#xff1a; 自動化&#xff1a;CV 技術可以自動識別、分類、跟蹤和分析圖像和視頻數據&…

Android JIT編譯:adb shell cmd package compile選項

Android JIT編譯&#xff1a;adb shell cmd package compile選項 例如&#xff1a; adb shell cmd package compile -m speed -f --full 包名 配置參數指令說明&#xff1a; compile [-r COMPILATION_REASON] [-m COMPILER_FILTER] [-p PRIORITY] [-f] [--primary-dex] …

Android Kotlin 項目集成 Firebase Cloud Messaging (FCM) 全攻略

Firebase Cloud Messaging (FCM) 是 Google 提供的跨平臺消息推送解決方案。以下是在 Android Kotlin 項目中集成 FCM 的詳細步驟。 一、前期準備 1. 創建 Firebase 項目 訪問 Firebase 控制臺點擊"添加項目"&#xff0c;按照向導創建新項目項目創建完成后&#x…

搭建PCDN大節點,服務器該怎么配

搭建P2P大節點時&#xff0c;服務器要怎么配呢&#xff1f;需要綜合考慮硬件性能、網絡帶寬、存儲能力、系統架構以及安全性等多個方面&#xff0c;以確保節點能夠高效、穩定地運行。 一、硬件配置 CPU&#xff1a;選擇高性能的多核處理器&#xff0c;以滿足高并發處理需求。核…

(done) 吳恩達版提示詞工程 8. 聊天機器人 (聊天格式設計,上下文內容,點餐機器人)

視頻&#xff1a;https://www.bilibili.com/video/BV1Z14y1Z7LJ/?spm_id_from333.337.search-card.all.click&vd_source7a1a0bc74158c6993c7355c5490fc600 別人的筆記&#xff1a;https://zhuanlan.zhihu.com/p/626966526 8. 聊天機器人&#xff08;Chatbot&#xff09; …

AtCoder Beginner Contest 403(題解ABCDEF)

A - Odd Position Sum #1.奇數數位和 #include<iostream> #include<vector> #include<stdio.h> #include<map> #include<string> #include<algorithm> #include<queue> #include<cstring> #include<stack> #include&l…

【Game】Powerful——Abandoned Ruins(9)

文章目錄 1、新增古玩2、機關機制3、探索法寶4、智斗強敵5、地圖6、參考 2025 年 1 月迎來的新玩法——荒廢遺跡 每周四個寶藏鏟&#xff08;老玩法&#xff09;或者兩個遺跡線索&#xff08;新玩法&#xff09;&#xff0c;3 個寶藏鏟也可以換一個遺跡線索&#xff0c;之前沒時…

構建網頁版IPFS去中心化網盤

前言&#xff1a;我把它命名為無限網盤 Unlimited network disks&#xff08;ULND&#xff09;&#xff0c;可以實現簡單的去中心化存儲&#xff0c;其實實現起來并不難&#xff0c;還是依靠強大的IPFS&#xff0c;跟著我一步一步做就可以了。 第一步&#xff1a;準備開發環境…