本文将介绍如何使用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的多线程技术和相关库来构建一个高效的网页数据爬取程序。通过合理组织代码结构,我们可以更好地管理爬虫的不同部分,并且保证了程序的可读性和可维护性。此外,本示例还强调了异常处理的重要性,以确保在网络请求不稳定的情况下,爬虫依然能稳健运行。
希望这篇文章对你有所帮助,如果有任何问题或建议,请随时联系。