爬取2023中国最好学科排名 - Python多线程爬虫实现

本文将介绍如何使用Python编写一个多线程爬虫,用于抓取2023年中国最好学科排名的数据,并将这些数据保存至CSV文件中。我们将从分析网页结构入手,逐步构建爬虫框架,并通过多线程技术提高数据采集效率。

一、需求分析与环境准备

需求分析

我们的目标是从软科网站上抓取学科排名相关信息,包括学科分类、大学排名以及相关指标等。这些信息分布在不同的页面中,因此我们需要设计一种方法来遍历这些页面,并提取所需数据。

环境准备

  • Python 3.x
  • requests 库用于发起HTTP请求
  • lxml 库用于解析HTML文档
  • csv 库用于处理CSV文件
  • threading 库用于实现多线程
  • fake_useragent 库用于生成随机User-Agent

安装依赖库:

pip install requests lxml fake-useragent

二、爬虫设计与实现

类定义 - ThreadCrawl

我们创建了一个名为ThreadCrawl的类,该类包含了整个爬虫的核心逻辑。

1.初始化
class ThreadCrawl:
    def __init__(self, base_url=BASE_URL):
        self.base_url = base_url
        self.session = requests.Session()
        self.ua = UserAgent()  # 生成随机的user-agent
        self.stop_event = threading.Event()  # 线程停止事件
  • base_url: 爬虫的基础URL。
  • session: 使用requests.Session()创建一个会话对象,用于保持同一个会话中的状态信息。
  • ua: 使用fake_useragent库生成随机的User-Agent,以模拟真实的浏览器访问。
  • stop_event: 使用threading.Event来控制线程的停止条件,这里暂时没有具体用途,但可以用来控制线程。
2.获取学科列表

fetch_subjects_list 方法用于从主页面抓取所有的学科分类信息,包括一级分类和二级分类。

    def fetch_subjects_list(self):
        retry = 3
        while retry > 0:
            try:
                response = self.session.get(self.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 = subject.xpath('.//span[@class="subject-code"]/text()')[0].strip()
                    main_title_text = subject.xpath('.//div[@class="subject-title"]/text()')[0].strip()
                    main_full_title = f'{main_code}{main_title_text}'

                    subjects.append({
                        'main_full_title': main_full_title,
                        'sub_items': []
                    })

                    sub_items = subject.xpath('.//div[@class="subject-list"]//a')
                    for item in sub_items:
                        sub_code = item.xpath('.//span[1]/text()')[0].strip()
                        sub_title = item.xpath('.//span[2]/text()')[0].strip()
                        sub_full_title = f'{sub_code}{sub_title}'
                        sub_url = f"{self.base_url}/{sub_code}"

                        subjects[-1]['sub_items'].append({
                            'sub_full_title': sub_full_title,
                            'sub_url': sub_url,
                            'sub_code': sub_code  # 添加二级分类代码用于排序
                        })

                return subjects
            except requests.RequestException as e:
                print(f"获取主页面出错: {e}")
                retry -= 1
                time.sleep(5)  # 等待5秒后重试
        raise Exception("多次尝试后仍无法获取主页面。")
  • 重试机制: 当请求失败时,通过一个retry变量控制重试次数。
  • 解析页面: 使用lxml库中的html.fromstring方法解析HTML文档。
  • 提取数据: 通过XPath表达式提取一级分类和二级分类的信息,并构造为字典形式。
3.提取大学数据

extract_data_from_details_page 方法用于从详细页面中提取单个大学的排名信息。

# 提取大学数据函数
    def extract_data_from_details_page(self, 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()')  # 更新 XPath 以提取总分

                if not univ_name:
                    continue

                data.append({
                    '2023年排名': ranking_2023[0].strip() if ranking_2023 else '空',
                    '2022年排名': ranking_2022[0].strip() if ranking_2022 else '空',
                    '全部层次': overall_rank[0].strip() if overall_rank else '空',
                    '大学名称': univ_name[0].strip() if univ_name else '空',
                    'Logo链接': logo_url[0].strip() if logo_url else '空',
                    '总分': total_score[0].strip() if total_score else '空'
                })
            except Exception as e:
                print(f"提取数据出错: {e}")
                continue

        return data
  • 解析详细页面: 使用lxml解析详细页面的HTML内容。
  • 提取数据: 通过XPath表达式提取每一行中的数据,并构造为字典形式。
  • 错误处理: 通过try-except语句捕获可能出现的异常,并跳过有问题的行。
4.数据保存

save_to_csv 方法负责将提取的数据保存至CSV文件。

# 保存到 CSV 文件
    def save_to_csv(self, data, filename='school_rankings2.csv'):
        fieldnames = ['序号', '一级分类', '二级分类', '2023年排名', '2022年排名', '全部层次', '大学名称', 'Logo链接',
                      '总分']

        # 获取当前的最大序号
        max_seq = 0
        try:
            with open(filename, 'r', newline='', encoding='utf-8') as csvfile:
                reader = csv.DictReader(csvfile)
                for row in reader:
                    seq = int(row['序号'])
                    if seq > max_seq:
                        max_seq = seq
        except FileNotFoundError:
            pass  # 文件不存在则从1开始计数

        with open(filename, 'a+', newline='', encoding='utf-8') as csvfile:
            writer = csv.DictWriter(csvfile, fieldnames=fieldnames)

            if csvfile.tell() == 0:
                writer.writeheader()

            for row in sorted(data, key=lambda x: x['二级分类']):
                max_seq += 1
                row['序号'] = max_seq
                writer.writerow(row)
        print(f"数据已成功追加到 {filename}")
  • 确定文件格式: 设置CSV文件的字段名。
  • 读取已有数据: 读取已有的CSV文件,确定当前最大序号。
  • 写入新数据: 使用csv.DictWriter写入新的数据行,并自动添加表头。
  • 排序与写入: 对数据进行排序,并追加写入新的数据行。
5.爬取子学科数据

scrape_sub_subject 方法用于多线程环境下抓取子学科数据。

# 多线程采集数据函数
    def scrape_sub_subject(self, sub_subject, main_full_title, queue):
        sub_url = sub_subject['sub_url']
        print(f"正在获取数据: {sub_url}")

        retry = 3
        while retry > 0:
            try:
                response = self.session.get(sub_url, timeout=10)
                response.raise_for_status()
                details_html_content = response.content

                # 提取数据
                university_data = self.extract_data_from_details_page(details_html_content)
                for data in university_data:
                    full_data = {
                        '一级分类': main_full_title,
                        '二级分类': sub_subject['sub_full_title'],
                        **data
                    }
                    queue.put(full_data)
                break  # 如果成功,跳出重试循环
            except requests.RequestException as e:
                print(f"获取详情页面 {sub_url} 出错: {e}")
                retry -= 1
                time.sleep(5)  # 等待5秒后重试
        else:
            print(f"多次尝试后仍无法获取 {sub_url} 的数据")
  • 获取数据: 发起HTTP请求获取二级分类的详细页面。
  • 提取数据: 调用extract_data_from_details_page方法提取数据。
  • 保存数据: 将提取的数据存入队列中。
6.主逻辑

最后,在main方法中实现了主逻辑流程。

# 主抓取逻辑
    def main(self):
        subjects = self.fetch_subjects_list()

        # 输出所有的一级分类
        print("以下是一级分类的列表:")
        for subject in subjects:
            print(subject['main_full_title'])

        main_title = input("请输入要采集的一级分类名称:")

        # 检查用户输入的一级分类是否存在
        selected_subjects = [subject for subject in subjects if subject['main_full_title'] == main_title]
        if not selected_subjects:
            print(f"未找到名为 '{main_title}' 的一级分类,请检查输入后重新运行程序。")
            return

        all_data = []
        for subject in selected_subjects:
            print(f"正在处理一级分类: {subject['main_full_title']}")

            # 创建一个队列来存储数据
            queue = Queue()
            # 创建并启动线程
            threads = []
            for sub_subject in sorted(subject['sub_items'], key=lambda x: x['sub_code']):
                thread = threading.Thread(target=self.scrape_sub_subject, args=(sub_subject, subject['main_full_title'], queue))
                thread.start()
                threads.append(thread)

            # 等待所有线程完成
            for thread in threads:
                thread.join()

            # 从队列中获取数据
            while not queue.empty():
                data = queue.get()
                all_data.append(data)

            # 对收集到的所有数据排序后再保存
            self.save_to_csv(all_data)  # 单独保存每条数据

            # 在一个线程池完成后等待3秒
            time.sleep(3)  # 这里添加等待时间


if __name__ == "__main__":
    crawler = ThreadCrawl()
    crawler.main()
  • 用户交互: 显示所有一级分类,并让用户选择要采集的一级分类。
  • 多线程处理: 对选中的每个二级分类创建并启动一个线程。
  • 同步数据: 使用队列同步各个线程的数据。
  • 保存结果: 保存所有收集到的数据。

三、总结

本文通过一个具体的例子展示了如何利用Python的多线程技术和相关库来构建一个高效的网页数据爬取程序。通过合理组织代码结构,我们可以更好地管理爬虫的不同部分,并且保证了程序的可读性和可维护性。此外,本示例还强调了异常处理的重要性,以确保在网络请求不稳定的情况下,爬虫依然能稳健运行。

希望这篇文章对你有所帮助,如果有任何问题或建议,请随时联系。

好的,下面是一个简单的Python多线程爬虫示例: ```python import requests import threading from queue import Queue # 定义一个全局变量用来存储爬取到的数据 data = [] # 定义一个线程锁,防止多个线程同时写入数据导致数据错乱 lock = threading.Lock() # 定义一个队列,用来存储待爬取的URL url_queue = Queue() # 定义一个爬取线程类 class CrawlerThread(threading.Thread): def __init__(self, url_queue): super().__init__() self.url_queue = url_queue def run(self): while True: # 从队列中获取一个URL url = self.url_queue.get() try: # 发送请求并解析响应数据 response = requests.get(url) content = response.text # 对响应数据进行处理(此处省略) # ... # 将处理后的数据存入全局变量 with lock: data.append(processed_data) # 标记该URL已被处理 self.url_queue.task_done() except Exception as e: print(f"Error occurred while crawling {url}: {e}") # 如果发生错误,将该URL重新放回队列 self.url_queue.put(url) self.url_queue.task_done() # 定义一个入口函数 def main(): # 初始化待爬取的URL列表 urls = ["http://www.example.com/page{}".format(i) for i in range(1, 11)] # 将URL列表添加到队列中 for url in urls: url_queue.put(url) # 创建多个爬取线程并启动 for i in range(5): t = CrawlerThread(url_queue) t.start() # 阻塞主线程,直到所有URL都被处理完毕 url_queue.join() # 输出爬取结果 print(data) if __name__ == '__main__': main() ``` 以上示例中,我们定义了一个`CrawlerThread`类来表示爬取线程,定义了一个`url_queue`队列来存储待爬取的URL,定义了一个`data`列表来存储爬取到的数据,以及定义了一个`lock`线程锁来保证多个线程访问`data`时不会出现数据错乱的情况。 在`main`函数中,我们首先将待爬取的URL列表添加到`url_queue`队列中,然后创建多个`CrawlerThread`实例并启动它们。最后,我们使用`url_queue.join()`方法来阻塞主线程,直到所有的URL都被处理完毕。 在`CrawlerThread`类的`run`方法中,我们使用`self.url_queue.get()`方法从`url_queue`队列中获取一个URL,然后发送请求并解析响应数据。如果处理过程中出现错误,我们将该URL重新放回队列中,以便后续线程重新处理。如果处理成功,我们将处理后的数据存入`data`列表中,并使用`self.url_queue.task_done()`方法标记该URL已被处理完毕。 需要注意的是,在多线程爬虫中,如果爬取的网站对IP访问频率有限制,可能会导致IP被封禁的情况。因此,在实际应用中,我们需要合理设置线程数量和请求频率,以避免被封禁的情况发生。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值