Python資料爬取處理視覺化,手把手全流程教學

你这过氧化氢掺水了發表於2024-12-01

這篇部落格中,選取openjudge網站上“百練”小組中的使用者答題資料,作為材料進行教學

目錄
  • 爬取主頁面內容
  • 主頁面內容提取
  • 需求資料爬取
  • 資料處理
  • 資料分析

網站地址:http://bailian.openjudge.cn/

使用到的Python包:requests、pandas、re、BeautifulSoup、time、matplotlib、seaborn




爬取主頁面內容

使用requests庫,來請求主介面的內容

import requests  # 匯入requests庫以便進行HTTP請求

# 定義目標URL,指向需要獲取內容的網頁
url = "http://bailian.openjudge.cn/"
# 傳送GET請求到指定的URL,並將響應儲存到response變數中
response = requests.get(url)
# 初始化一個變數,用於儲存HTML內容,預設為None
html_context_save = None

# 檢查HTTP響應的狀態碼,以確定請求是否成功
if response.status_code == 200:
    # 如果狀態碼為200,表示請求成功
    html_content = response.text  # 獲取響應的HTML內容
    html_context_save = html_content  # 儲存HTML內容到變數
    print(html_content)  # 列印HTML內容以供檢視
else:
    # 如果狀態碼不為200,表示請求失敗
    print("failed to receive the context")  # 輸出失敗資訊

輸出結果:拿到了主頁面的HTML內容




主頁面內容提取

F12進入控制檯,在元素裡找到“練習”,然後複製路徑,得到
document.querySelector("#main > div.main-content > ul > li > h3 > a"),為我們所需要的子頁面位置

提取子網頁地址:

from bs4 import BeautifulSoup  # 匯入BeautifulSoup庫用於解析HTML文件

# 使用 BeautifulSoup 庫解析 HTML 內容,將其儲存在 soup 變數中
soup = BeautifulSoup(html_context_save, 'html.parser')  # 傳入HTML內容和解析器型別

# 查詢所有具有類名為 "current-contest label" 的HTML元素,並將結果儲存在 contests 列表中
contests = soup.find_all(class_="current-contest label")  # 返回一個符合條件的標籤列表

import pandas as pd  # 匯入pandas庫用於資料處理和儲存

# 建立一個列表來儲存未結束的比賽資訊
data = []

# 遍歷每個比賽上下文以獲取比賽資訊
for context in contests:
    # 獲取比賽標題,提取h3標籤中的文字,並去除多餘的空白符
    h3_text = context.find('h3').get_text(strip=True)

    # 查詢h3標籤下的錨標籤(a)並提取其連結和文字
    a_tag = context.find('a')  # 查詢第一個<a>標籤
    a_href = a_tag['href'] if a_tag else None  # 若存在a_tag,提取其href屬性
    a_text = a_tag.get_text(strip=True) if a_tag else None  # 若存在a_tag,提取其文字內容

    # 將比賽資訊新增到列表中,用字典格式儲存標題和連結
    data.append({
        '標題': a_text,  # 比賽標題
        '連結': f"http://bailian.openjudge.cn{a_href}",  # 完整連結,結合基礎URL
    })

# 將資料儲存為DataFrame,以便於後續的操作和展示
df = pd.DataFrame(data)

# 輸出DataFrame,顯示所有未結束的比賽資訊
print(df)

獲得了子網頁的地址:




需求資料爬取

進入子網頁,點選“狀態”,看到上方網站頁面多出了“status/”內容,內部的內容為我們需要的資料,可以看到關雲長,趙子龍等同學都在提交作業。
在這裡我們提取前67頁資料作為資料分析的素材。
爬取內容存放在openjudge_data.csv檔案

import requests  # 匯入請求庫,用於傳送HTTP請求
from bs4 import BeautifulSoup  # 匯入BeautifulSoup庫,用於解析HTML文件
import pandas as pd  # 匯入Pandas庫,用於資料處理和儲存
import time  # 匯入時間庫,用於延遲請求


# 定義函式從指定的連結和頁碼獲取資料
def fetch_data(contest_url, page_number):
    try:
        # 傳送GET請求,並設定請求引數為頁碼
        response = requests.get(contest_url, params={'page': page_number}, timeout=10)
        response.raise_for_status()  # 檢查請求是否成功
        return response.text  # 返回頁面的HTML文字
    except Exception as e:
        # 如果請求失敗,輸出錯誤資訊
        print(f"Error accessing {contest_url}?page={page_number}: {e}")
        return None  # 返回None表示獲取資料失敗


# 定義函式從HTML頁面中解析所需的資料
def parse_page(html, row_title):
    soup = BeautifulSoup(html, 'html.parser')  # 解析HTML文件
    problem_status_table = soup.find(id='problemStatus')  # 找到狀態表格
    if not problem_status_table:
        return []  # 如果沒有找到表格,返回空列表

    rows = problem_status_table.find_all('tr')[1:]  # 獲取所有表格行,跳過表頭
    status_data = []  # 用於儲存當前頁面的狀態資料

    for tr in rows:  # 遍歷每一行資料
        # 提取每列的資料並去除多餘空白
        submit_user = tr.find('td', class_='submit-user').text.strip()
        course_class = tr.find('td', class_='className').text.strip()
        problem_title = tr.find('td', class_='title').text.strip()
        result = tr.find('td', class_='result').text.strip()
        memory = tr.find('td', class_='memory').text.strip()
        spending_time = tr.find('td', class_='spending-time').text.strip()
        code_length = tr.find('td', class_='code-length').text.strip()
        language = tr.find('td', class_='language').text.strip()

        # 提取提交時間的絕對和相對格式
        date_element = tr.find('td', class_='date').find('abbr')
        absolute_time = date_element['title'] if date_element and 'title' in date_element.attrs else None
        relative_time = date_element.get_text(strip=True) if date_element else None

        # 將每次提交的資料新增到列表中
        status_data.append({
            '提交人': submit_user,
            '班級': course_class,
            '題目': problem_title,
            '結果': result,
            '記憶體': memory,
            '時間': spending_time,
            '程式碼長度': code_length,
            '語言': language,
            '提交時間(相對)': relative_time,
            '提交時間(絕對)': absolute_time,
            '標題': row_title  # 將標題資訊也存入資料中
        })

    return status_data  # 返回解析後資料的列表


# 用於儲存所有抓取到的結果
all_status_data = []

# 假設 df 已經被定義,幷包含所有比賽的連結和標題
for index, row in df.iterrows():
    contest_url = row['連結'] + 'status/'  # 生成當前比賽的狀態頁面連結
    page_number = 1  # 初始化當前頁碼
    previous_data = None  # 存放上一次抓取的資料,用於比較

    while page_number <= 500:  # 設定頁碼限制為500
        html = fetch_data(contest_url, page_number)  # 獲取當前頁面的HTML內容
        if html is None:
            break  # 如果獲取失敗,則退出迴圈

        # 解析當前頁面的資料
        status_data = parse_page(html, row['標題'])

        if not status_data:
            break  # 如果沒有資料,說明已到最後一頁,退出迴圈

        # 檢查當前頁的資料是否與上一頁相同
        if previous_data is not None and status_data == previous_data:
            print(f"第 {page_number} 頁資料與之前相同,跳出迴圈。")
            break  # 如果資料相同,說明沒有新資料,退出迴圈

        all_status_data.extend(status_data)  # 將當前內容新增到總的資料列表
        print(f'爬取的結果(第 {page_number} 頁):{status_data}')  # 輸出當前頁爬取的資料

        previous_data = status_data  # 更新最後獲取的資料

        # 控制請求速度,避免被目標網站封鎖
        time.sleep(0.1)  # 每次請求後短暫延遲0.1秒
        page_number += 1  # 頁碼自增1,準備抓取下一頁

# 將抓取的資料轉換為Pandas DataFrame,並儲存為CSV檔案
status_df = pd.DataFrame(all_status_data)

# 輸出整個DataFrame以確認爬取結果
print("爬取完成,資料如下:")
print(status_df)

# 儲存為CSV檔案,設定不輸出索引,編碼為utf-8
status_df.to_csv('openjudge_data.csv', index=False, encoding='utf-8')

資料處理

讀取檔案:

import pandas as pd
df = pd.read_csv("openjudge_data.csv")

檢查缺失值:

print(df.isnull().sum())

刪去缺失值最多的列:

df.drop(columns=['班級'], inplace=True)

刪除“時間”,“記憶體”,“程式碼長度”列為空值的行:

df.dropna(subset=['時間','記憶體','程式碼長度'], inplace=True)

資料型別轉換:

# 確保'記憶體'列是字串型別,並使用str.replace()方法去除字串中的'kB'
# 然後,將處理後的字串轉換為浮點數型別
df['記憶體'] = df['記憶體'].astype(str)  # 將'記憶體'列轉換為字串型別,以確保後續操作不會出錯
df['記憶體'] = df['記憶體'].str.replace('kB', '', regex=False)  # 去掉'kB'字尾
df['記憶體'] = df['記憶體'].astype(float)  # 將字串型別的'記憶體'轉換為浮點數型別

# 確保'時間'列是字串型別,並使用str.replace()方法去除字串中的'ms'
# 然後,將處理後的字串轉換為浮點數型別
df['時間'] = df['時間'].astype(str)  # 將'時間'列轉換為字串型別
df['時間'] = df['時間'].str.replace('ms', '', regex=False)  # 去掉'ms'字尾
df['時間'] = df['時間'].astype(float)  # 將字串型別的'時間'轉換為浮點數型別

# 確保'程式碼長度'列是字串型別,並使用str.replace()方法去除字串中的' B'
# 然後,將處理後的字串轉換為整數型別
df['程式碼長度'] = df['程式碼長度'].astype(str)  # 將'程式碼長度'列轉換為字串型別
df['程式碼長度'] = df['程式碼長度'].str.replace(' B', '', regex=False)  # 去掉' B'字尾
df['程式碼長度'] = df['程式碼長度'].astype(int)  # 將字串型別的'程式碼長度'轉換為整數型別

# 顯示處理後的DataFrame的前幾行
df.head()  # 透過head()方法檢視DataFrame的前幾行以驗證處理效果



資料分析

統計總的提交狀況:

print(df.groupby('結果').size())

檢視“關雲長”使用者的提交情況:

# 將“關雲長”提交的資料篩選出來
submissions = df[df['提交人'].str.contains("關雲長")]

# 計算Accepted和Wrong Answer提交的數量
status_counts = submissions['結果'].value_counts()

# 列印各狀態的提交數量
print("\n提交狀態統計:")
print(status_counts)

# 統計每次提交的時間和所需記憶體與時間
submissions['記憶體'] = submissions['記憶體']
submissions['時間'] = submissions['時間']

# 計算平均記憶體和平均時間
average_memory = submissions['記憶體'].mean()
average_time = submissions['時間'].mean()

print(f"\n關雲長的平均記憶體使用: {average_memory:.2f} kB")
print(f"關雲長的平均執行時間: {average_time:.2f} ms")

資料視覺化:

import matplotlib.pyplot as plt
import seaborn as sns

# 設定繪圖風格
sns.set(style="whitegrid")  # 設定背景風格為白色網格型

# 支援中文
plt.rcParams['font.sans-serif'] = ['SimHei']  # 設定字型為 SimHei,用來正常顯示中文標籤
plt.rcParams['axes.unicode_minus'] = False  # 用來正常顯示負號

# 1. 提交狀態的條形圖
plt.figure(figsize=(8, 6))  # 設定圖形的寬度和高度
sns.barplot(x=status_counts.index, y=status_counts.values, palette="viridis")  # 繪製條形圖
plt.title("關雲長 提交狀態統計")  # 設定圖表標題
plt.xlabel("結果")  # 設定X軸標籤
plt.ylabel("提交數量")  # 設定Y軸標籤
plt.xticks(rotation=45)  # X軸標籤旋轉45度以避免重疊
plt.show()  # 顯示圖表

# 2. 記憶體和時間的分佈圖
plt.figure(figsize=(12, 6))  # 設定整體圖形的寬度和高度

# 記憶體分佈
plt.subplot(1, 2, 1)  # 建立一個1行2列的子圖,選擇第1個位置
sns.histplot(submissions['記憶體'], bins=10, kde=True, color='blue')  # 繪製記憶體分佈直方圖
plt.title("關雲長 提交記憶體分佈")  # 設定子圖示題
plt.xlabel("記憶體 (kB)")  # 設定X軸標籤
plt.ylabel("提交數量")  # 設定Y軸標籤

# 時間分佈
plt.subplot(1, 2, 2)  # 選擇第2個子圖
sns.histplot(submissions['時間'], bins=10, kde=True, color='orange')  # 繪製時間分佈直方圖
plt.title("關雲長 提交時間分佈")  # 設定子圖示題
plt.xlabel("時間 (ms)")  # 設定X軸標籤
plt.ylabel("提交數量")  # 設定Y軸標籤

plt.tight_layout()  # 自適應調整子圖間的間距以使圖表更整齊
plt.show()  # 顯示整個圖形

相關文章