這篇部落格中,選取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() # 顯示整個圖形