这个脚本的主要功能是从某个排名网站(如上海软科排名网站)抓取关于大学排名的数据,并将这些数据按一级分类和二级分类分别存储到 CSV 文件中。该过程使用多线程来提高爬取速度。
让我们逐步分析和讲解这个脚本的各个部分:
1. 基础 URL 和主页面的抓取函数 (fetch_subjects_list
)
BASE_URL = "https://www.shanghairanking.cn/rankings/bcsr/2023"
def fetch_subjects_list():
session = requests.Session()
retry = 3
while retry > 0:
try:
response = session.get(BASE_URL, timeout=10)
response.raise_for_status()
tree = html.fromstring(response.content)
subjects = []
subject_items = tree.xpath('//div[@class="subject-item"]')
for subject in subject_items:
main_code, main_title = [el.strip() for el in subject.xpath('.//span[@class="subject-code"]/text()|.//div[@class="subject-title"]/text()')]
main_full_title = f'{main_code}{main_title}'
sub_items = subject.xpath('.//div[@class="subject-list"]//a')
for item in sub_items:
sub_code, sub_title = [el.strip() for el in item.xpath('.//span[1]/text()|.//span[2]/text()')]
sub_full_title = f'{sub_code}{sub_title}'
sub_url = f"{BASE_URL}/{sub_code}"
subjects.append({
'main_full_title': main_full_title,
'sub_full_title': sub_full_title,
'sub_url': sub_url
})
return subjects
except requests.RequestException as e:
print(f"获取主页面出错: {e}")
retry -= 1
time.sleep(5)
raise Exception("多次尝试后无法获取主页面。")
解释:
-
该函数从基础的 URL 开始,使用
requests
库获取主页面的 HTML 内容。 -
通过 XPath 提取每个学科的一级分类(如理科、人文学科等)和其下的二级分类(具体的学科,如物理学、哲学等)。
-
数据结构通过一个包含多个字典的列表表示,字典中存储了每个学科的分类标题及其相应的 URL。
-
该函数还包含重试机制,如果请求失败(如网络问题),它会最多尝试三次。
2. 从详细页面中提取大学排名数据 (extract_data_from_details_page
)
def extract_data_from_details_page(details_html_content):
tree = html.fromstring(details_html_content)
rows = tree.xpath('//tr')
data = []
for row in rows:
try:
ranking_2023 = row.xpath('.//td/div[@class="ranking"]/text()')
ranking_2022 = row.xpath('.//td/span[@data-v-6c038bb7=""]/text()')
overall_rank = row.xpath('.//td[contains(text(), "前")]/text()')
univ_name = row.xpath('.//td//span[@class="name-cn"]/text()')
logo_url = row.xpath('.//td//div[@class="logo"]/img/@src')
total_score = row.xpath('.//td[last()]/text()')
if not univ_name:
continue
data.append({
'2023年排名': get_first_or_default(ranking_2023),
'2022年排名': get_first_or_default(ranking_2022),
'全部层次': get_first_or_default(overall_rank),
'大学名称': get_first_or_default(univ_name),
'Logo链接': get_first_or_default(logo_url),
'总分': get_first_or_default(total_score)
})
except Exception as e:
print(f"从行中提取数据时出错: {e}")
continue
return data
解释:
-
该函数处理每个详细页面的 HTML 内容,提取包含大学排名的表格。
-
使用 XPath 来选择表格中的每一行,然后提取多个字段(如大学名称、2023年排名、2022年排名、Logo 链接等)。
-
使用了
get_first_or_default()
函数来确保即使某个字段为空,代码也不会出错。
3. 处理每个学科分类的函数 (fetch_and_process_subject
)
def fetch_and_process_subject(subject):
sub_url = subject['sub_url']
print(f"正在获取数据: {sub_url}")
session = requests.Session()
retry = 3
while retry > 0:
try:
response = session.get(sub_url, timeout=10)
response.raise_for_status()
details_html_content = response.content
university_data = extract_data_from_details_page(details_html_content)
full_data = [
{
'一级分类': subject['main_full_title'],
'二级分类': subject['sub_full_title'],
**data
}
for data in university_data
]
return full_data
except requests.RequestException as e:
print(f"获取详情页面出错 {sub_url}: {e}")
retry -= 1
time.sleep(5)
print(f"多次尝试后无法获取 {sub_url} 的数据。")
return []
解释:
-
该函数负责处理每个二级分类页面的数据抓取,提取并返回该学科的详细数据。
-
重试机制同样适用于页面抓取,以防请求失败。
4. 保存数据到 CSV 文件 (save_to_csv
)
def save_to_csv(data, main_full_title, mode='w'):
file_name = f"{main_full_title.replace('/', '_').replace(' ', '_')}.csv"
file_path = os.path.join(os.getcwd(), file_name)
with open(file_path, mode, newline='', encoding='utf-8') as csvfile:
fieldnames = ['序号', '一级分类', '二级分类', '2023年排名', '2022年排名', '全部层次', '大学名称', 'Logo链接', '总分']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
if mode == 'w':
writer.writeheader()
for index, row in enumerate(data, start=1):
row['序号'] = index
writer.writerow(row)
print(f"数据已成功保存到 {file_name},模式:{mode}")
解释:
-
该函数将抓取的数据写入 CSV 文件中。
-
文件名基于一级分类名称生成,处理了文件名中的特殊字符以避免文件系统问题。
-
支持
w
(写入)和a
(追加)两种模式,使得数据可以分批写入文件。
5. 主抓取逻辑 (main
)
def main():
subjects = fetch_subjects_list()
user_input = input("请输入您想要采集的一级分类名称:")
selected_subjects = [subject for subject in subjects if user_input in subject['main_full_title']]
if not selected_subjects:
print(f"未找到匹配的一级分类:{user_input}")
return
for subject in selected_subjects:
all_data = []
main_full_title = subject['main_full_title']
with ThreadPoolExecutor(max_workers=5) as executor:
futures = {executor.submit(fetch_and_process_subject, sub_subject): sub_subject for sub_subject in selected_subjects}
for future in as_completed(futures):
sub_subject = futures[future]
try:
data = future.result()
if data:
save_to_csv(data, main_full_title, mode='a') # 追加数据到对应的 CSV 文件
all_data.extend(data)
except Exception as exc:
print(f"处理学科 {sub_subject['sub_url']} 时出错: {exc}")
解释:
-
主函数调用
fetch_subjects_list()
获取全部学科列表,允许用户输入一个一级分类进行筛选。 -
使用
ThreadPoolExecutor
多线程加速数据抓取。 -
最终将抓取的数据按一级分类保存到 CSV 文件中。
6. 运行脚本
通过 main()
函数调用,程序开始执行。