网络爬虫:大学排名数据抓取与分析

这个脚本的主要功能是从某个排名网站(如上海软科排名网站)抓取关于大学排名的数据,并将这些数据按一级分类和二级分类分别存储到 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() 函数调用,程序开始执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值