第四次作业-何玮鑫
作业4
一、沪深 A 股数据爬取:Ajax 动态数据抓取与结构化存储
1.1 实现方案与核心代码
需求背景与整体思路
本次任务核心目标是爬取东方财富网沪深 A 股、上证 A 股、深证 A 股三大板块的股票数据,解决Ajax 动态加载数据无法直接抓取、网站反爬检测、多板块批量爬取、数据结构化存储四大核心问题。整体设计思路采用面向对象编程,将爬虫拆分为「初始化(浏览器 / 数据库)」「数据解析」「数据存储」「分页爬取」四大模块,保证代码的可维护性和扩展性。
核心代码逐模块解析
浏览器配置:通过禁用自动化检测、自定义 User-Agent、重写webdriver属性,规避东方财富网的反爬机制;
等待机制:结合隐式等待和显式等待,解决 Ajax 动态加载导致的「元素未找到」问题;
目标配置:将三大板块的 URL 参数封装为字典,便于批量遍历爬取。
class EastMoneyStockCrawler:
def __init__(self):
# 1. 浏览器配置:规避反爬检测
chrome_options = Options()
# 解决Linux系统下Chrome运行权限问题
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
# 关闭日志冗余输出
chrome_options.add_experimental_option('excludeSwitches', ['enable-logging'])
# 自定义User-Agent,模拟真实浏览器请求
chrome_options.add_argument(
'user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.7444.176 Safari/537.36')
# 禁用自动化检测(核心反爬规避)
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
chrome_options.add_experimental_option('useAutomationExtension', False)
# 初始化Chrome驱动
self.driver = webdriver.Chrome(
service=Service(), # 自动管理ChromeDriver版本
options=chrome_options
)
# 重写navigator.webdriver属性,彻底规避反爬检测
self.driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
})
# 2. 等待机制配置:保证元素加载完成后再操作
self.driver.implicitly_wait(20) # 隐式等待:全局元素等待20秒
self.wait = WebDriverWait(self.driver, 35) # 显式等待:针对关键元素的精准等待
# 3. 数据库初始化:连接MySQL并创建数据表
self.db_conn = self._init_db_connection()
self.db_cursor = self.db_conn.cursor()
# 4. 爬取目标配置:三大板块的URL参数
self.boards = {
"沪深A股": "hs_a_board",
"上证A股": "sh_a_board",
"深证A股": "sz_a_board"
}
self.base_url = "http://quote.eastmoney.com/center/gridlist.html#{}"
数据库配置:连接数据库,并创建表
conn = pymysql.connect(
host=os.getenv("MYSQL_HOST", "localhost"),
port=int(os.getenv("MYSQL_PORT", 3306)),
user=os.getenv("MYSQL_USER"),
password=os.getenv("MYSQL_PASSWORD"),
database=os.getenv("MYSQL_DB", "stock_data"),
charset='utf8mb4', # 支持中文及特殊字符
cursorclass=pymysql.cursors.DictCursor # 返回字典格式数据,便于解析
)
# 创建股票信息表:字段严格匹配爬取的股票数据维度
create_table_sql = """
CREATE TABLE IF NOT EXISTS a_stock_info (
id INT AUTO_INCREMENT PRIMARY KEY, # 自增主键
stock_no VARCHAR(20) NOT NULL, # 股票代码
stock_name VARCHAR(50) NOT NULL, # 股票名称
latest_price DECIMAL(10, 2), # 最新报价(小数保留2位)
price_change_rate VARCHAR(20), # 涨跌幅(含%符号,用字符串存储)
price_change_amount DECIMAL(10, 2), # 涨跌额
volume VARCHAR(50), # 成交量(含万/亿单位,字符串存储)
turnover VARCHAR(50), # 成交额
amplitude VARCHAR(20), # 振幅
highest_price DECIMAL(10, 2), # 最高价格
lowest_price DECIMAL(10, 2), # 最低价格
open_price DECIMAL(10, 2), # 今开价格
prev_close_price DECIMAL(10, 2), # 昨收价格
board VARCHAR(50) NOT NULL, # 所属板块(沪深/上证/深证)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, # 数据创建时间
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, # 数据更新时间
UNIQUE KEY unique_stock (stock_no, board) # 唯一索引:避免同一股票重复存储
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"""
核心爬虫:
表格定位:通过表头关键词(序号、代码、名称)精准定位股票表格,避免定位错误;
分页处理:
先定位分页容器,再找 “下一页” 按钮,避免全局匹配导致的元素定位错误;
判断按钮状态(禁用 / 可用),终止最后一页的循环;
分页后等待 3 秒,保证新页面数据加载完成;
进度反馈:打印每页爬取的条数和成功保存的条数,便于监控爬取进度。
def crawl_board(self, board_name, board_param):
"""核心功能:爬取单个板块的股票数据,处理分页逻辑"""
print(f"\n📊 开始爬取【{board_name}】...")
target_url = self.base_url.format(board_param)
self.driver.get(target_url)
# 滚动到表格区域:解决页面加载后表格不在可视区域的问题
self.driver.execute_script("window.scrollTo(0, 600);")
time.sleep(2)
try:
# 显式等待表格加载:通过表头关键词定位,确保表格完全加载
stock_table = self.wait.until(
EC.presence_of_element_located(
(By.XPATH, "//table[.//th[text()='序号'] and .//th[text()='代码'] and .//th[text()='名称']]"))
)
print(f"✅ 【{board_name}】表格加载完成!")
# 等待表格行加载
self.wait.until(EC.presence_of_element_located((By.XPATH, "//table[.//th[text()='序号']]//tbody/tr")))
page_count = 1
while True:
print(f"\n📄 第 {page_count} 页...")
# 定位当前页的所有行
rows = stock_table.find_elements(By.XPATH, ".//tbody/tr")
print(f"🔍 找到 {len(rows)} 条数据")
# 遍历解析每行数据并存储
success_count = 0
for row in rows:
data = self._parse_stock_row(row, board_name)
if data and self._save_to_db(data):
success_count += 1
print(f"✅ 成功保存 {success_count} 条数据")
# 分页逻辑:定位分页区域的“下一页”按钮
try:
# 先定位分页容器,再找下一页按钮,避免全局匹配错误
page_container = self.wait.until(
EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'paginate')]"))
)
next_btn = page_container.find_element(By.XPATH, ".//a[text()='下一页']")
# 判断是否为最后一页:按钮禁用则终止分页
if "disabled" in next_btn.get_attribute("class") or not next_btn.is_enabled():
print(f"📌 已到【{board_name}】最后一页")
break
next_btn.click() # 点击下一页
time.sleep(3) # 等待新页面加载
page_count += 1
except (TimeoutException, NoSuchElementException):
print(f"📌 【{board_name}】无更多页面")
break
except TimeoutException:
print(f"❌ 【{board_name}】表格定位超时!")
except Exception as e:
print(f"❌ 【{board_name}】爬取异常:{e}")
2. 结果展示

图 2:MySQL 中 a_stock_info 表存储的沪深 A 股数据截图,可见股票代码、最新报价等字段精准匹配
3.心得体会
东方财富网通过检测navigator.webdriver属性识别自动化程序,最初直接使用 Selenium 爬取时频繁被拦截。解决方案的核心思路是「模拟真实浏览器环境」;最初爬取时出现「数据错位」问题,核心原因是列索引匹配错误。解决思路:先手动分析页面表格的列顺序,绘制索引映射表;增加列数校验(至少 14 列),过滤无效行;自定义数据清洗函数,过滤页面中的无关文本(如 “股吧”);数值字段增加空值判断,避免类型转换异常。
二、中国 MOOC 网课程爬取:模拟登录与多维度数据提取
1. 实现方案与核心代码(附详细思路解析)
需求背景与整体思路
本次任务核心目标是爬取中国 MOOC 网的课程资源信息,核心难点包括「iframe 嵌套的登录表单处理」「多标签页的稳定切换」「动态加载的课程卡片解析」「结构化数据存储」。整体设计思路延续面向对象风格,拆分为「登录模块」「数据库模块」「课程列表爬取模块」「课程详情解析模块」,重点解决登录验证和动态元素定位问题。
核心代码实现
为实现模拟登录,我们对每一个按钮提取了XPATH,让点击实现精确:
浏览器配置:新增--disable-popup-blocking关闭弹窗拦截,避免登录弹窗被 Chrome 拦截;设置窗口大小,模拟真实用户的浏览环境;
多 XPath 配置:为登录按钮、输入框等核心元素配置多组 XPath,适配 MOOC 网不同页面版本的元素结构,避免单一 XPath 定位失败;
异常捕获:浏览器启动异常直接终止程序,避免后续无效操作。
class MoocSpider:
def __init__(self):
# 1. 数据库初始化:提前创建数据表,保证爬取前环境就绪
self.conn = None
self.cursor = None
self.init_db()
# 2. 浏览器配置:延续反爬规避策略,适配MOOC网的检测机制
self.chrome_options = Options()
self.chrome_options.add_argument('--no-sandbox')
self.chrome_options.add_argument('--disable-dev-shm-usage')
self.chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
self.chrome_options.add_experimental_option('useAutomationExtension', False)
self.chrome_options.add_argument(
'user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.7444.176 Safari/537.36')
self.chrome_options.add_argument('--disable-popup-blocking') # 关闭弹窗拦截,避免登录弹窗被拦截
self.chrome_options.add_argument('window-size=1200,800') # 设置窗口大小,模拟真实浏览
# 启动浏览器,捕获启动异常
print("🔍 正在启动 Chrome 浏览器...")
try:
self.driver = webdriver.Chrome(options=self.chrome_options)
print("✅ Chrome 浏览器启动成功!")
except Exception as e:
print(f"❌ Chrome 启动失败:{str(e)}")
raise SystemExit(1)
# 3. 反爬强化:重写webdriver属性
self.driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
})
# 4. 等待机制:延长等待时间,适配MOOC网较慢的加载速度
self.driver.maximize_window()
self.driver.implicitly_wait(15)
self.wait = WebDriverWait(self.driver, 35)
# 5. 登录元素定位配置:多XPath匹配,适配不同页面版本
self.login_btn_xpaths = ["//div[contains(text(), '登录') and contains(@role, 'button')]"]
self.iframe_locator = (By.XPATH, "//iframe[contains(@src, 'reg.icourse163.org') and @frameborder='0']")
self.phone_input_xpaths = ["//input[@placeholder='请输入手机号']", "//input[@id='phoneipt']"]
self.pwd_input_xpaths = ["//input[@placeholder='请输入密码']", "//input[@type='password']"]
self.submit_btn_xpaths = ["//*[@id='submitBtn']", "//div[text()='登录' and contains(@class, 'ubtn-primary')]",
"//div[contains(@class, 'ubtn-large') and contains(text(), '登录')]"]
self.login_success_xpath = "//span[contains(text(), 'Hi,') or contains(text(), '我的学习')]"
有一个细节,就是mooc的登录点击后会跳出iframe的登录框,所以需要切换到iframe中进行输入和点击
iframe 切换是登录的核心难点:MOOC 网的登录表单嵌套在独立的 iframe 中,直接定位输入框会失败;
def switch_to_login_iframe(self):
"""核心功能:切换到登录表单所在的iframe,解决嵌套表单定位问题"""
try:
print("🔍 正在查找登录表单所在的iframe...")
# 显式等待iframe加载完成并切换
WebDriverWait(self.driver, 10).until(EC.frame_to_be_available_and_switch_to_it(self.iframe_locator))
print("✅ 已成功切换到登录表单的iframe")
return True
except (TimeoutException, NoSuchFrameException):
print("❌ 未找到登录表单的iframe,无法继续登录")
return False
接着设计数据库的表结构,根据字段要求创建表,并完成数据库的链接
def init_db(self):
"""核心功能:初始化MOOC课程数据库,创建数据表并清空历史数据"""
try:
self.conn = pymysql.connect(**DB_CONFIG)
self.cursor = self.conn.cursor()
print("✅ 数据库连接成功")
# 创建课程信息表:字段严格匹配爬取的课程维度
create_table_sql = """
CREATE TABLE IF NOT EXISTS course_info (
id INT AUTO_INCREMENT PRIMARY KEY, # 自增主键
cCourse VARCHAR(255) NOT NULL, # 课程名称
cCollege VARCHAR(255), # 学校名称
cTeacher VARCHAR(255), # 主讲教师
cTeam TEXT, # 团队成员(可能多个,用TEXT存储)
cCount VARCHAR(50), # 参加人数(含单位,字符串存储)
cProcess VARCHAR(255), # 课程进度/时间
cBrief TEXT, # 课程简介(长文本)
course_id VARCHAR(50) UNIQUE # 课程唯一ID(从URL解析)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
"""
self.cursor.execute(create_table_sql)
# 清空历史数据:避免重复爬取导致的数据冗余
truncate_table_sql = "TRUNCATE TABLE course_info"
self.cursor.execute(truncate_table_sql)
self.conn.commit()
print("✅ 数据库表course_info创建成功并已清空")
except Exception as e:
print(f"❌ 数据库连接或表创建失败: {e}")
核心爬虫
由于主页课程较少,我让爬虫先跳转到https://www.icourse163.org/channel/2001.htm,然后通过模拟滚动,加载更多课程卡片,但是会发现这些卡片里,没有课程ID等信息,具体的课程团队信息需要点击进详情页才能知晓,因此我们通过模拟点击,进入页面,获取其真实url,通过正则解析url可以提取其课程ID,然后其他的信息可以通过XPATH提取,非常方便,另外详情页中没有学校名称,所以我们在点击具体课程卡片的时候,可以通过解析html文档获取学校名称。
def scrape_channel_page(self):
"""核心功能:爬取课程列表页,提取课程链接、名称、学校等基础信息"""
target_url = "https://www.icourse163.org/channel/2001.htm" # 计算机类课程频道
print(f"\n🌐 前往目标频道: {target_url}")
# 导航到频道页,等待关键元素加载
self.driver.get(target_url)
WebDriverWait(self.driver, 15).until(
EC.presence_of_element_located((By.CLASS_NAME, "_2mbYw")) # 频道页核心元素
)
print(f"✅ 已导航到频道页面: {target_url}")
time.sleep(2)
course_info_list = [] # 存储课程基础信息
MAX_SCROLLS = 1 # 限制滚动次数,加快测试效率
for scroll in range(MAX_SCROLLS):
print(f"\n📜 正在进行第 {scroll + 1}/{MAX_SCROLLS} 次滚动...")
# 滚动到页面底部,加载更多课程卡片
self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(3) # 等待新内容加载
# 定位所有课程卡片:多class匹配,适配不同样式的卡片
xpath_card = "//div[contains(@class, 'course-card') or contains(@class, 'commonCourseCardItem')]"
course_cards = self.driver.find_elements(By.XPATH, xpath_card)
print(f"找到 {len(course_cards)} 个课程卡片")
# 遍历前10个课程卡片,避免爬取过多数据
main_window = self.driver.current_window_handle # 记录主窗口句柄
for i, card in enumerate(course_cards[:10]):
try:
print(f"\n处理第 {i+1} 个课程卡片...")
# 提取课程名称和学校(卡片上的基础信息)
course_name = ""
course_college = ""
try:
# 多XPath提取课程名称,提升成功率
course_name_xpaths = [
".//h3[contains(@class, '_3EwZv')]",
".//h3[contains(@class, '_1VDzh')]",
".//span[contains(@class, 'course-card-name') or contains(@class, 'course-title')]",
".//div[contains(@class, 'course-card-name') or contains(@class, 'course-title')]",
".//a[contains(@class, 'course-card-name') or contains(@class, 'course-title')]",
".//span[contains(text(), '课程名称')]/following-sibling::*"
]
for xpath in course_name_xpaths:
try:
course_name_element = card.find_element(By.XPATH, xpath)
course_name = course_name_element.text.strip()
if course_name:
print(f"从卡片中提取课程名称: {course_name}")
break
except Exception:
continue
# 多XPath提取学校名称
college_xpaths = [
".//p[contains(@class, '_2lZi3')]",
".//div[contains(@class, 'course-card-school') or contains(@class, 'school-name') or contains(@class, 'university')]",
".//span[contains(@class, 'course-card-school') or contains(@class, 'school-name') or contains(@class, 'university')]",
".//a[contains(@class, 'course-card-school') or contains(@class, 'school-name') or contains(@class, 'university')]",
".//div[contains(@class, 'course-provider') or contains(@class, 'provider')]",
".//span[contains(text(), '学校') or contains(text(), '大学') or contains(text(), '学院')]/following-sibling::*"
]
for xpath in college_xpaths:
try:
college_element = card.find_element(By.XPATH, xpath)
course_college = college_element.text.strip()
if course_college:
print(f"从卡片中提取学校名称: {course_college}")
break
except Exception:
continue
except Exception as e:
print(f"从卡片中提取信息时出错: {e}")
# 点击课程卡片,打开详情页
print("尝试点击课程卡片...")
# 滚动到卡片可视区域,避免点击失败
self.driver.execute_script("arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});", card)
time.sleep(1)
# 记录初始窗口数量,判断新窗口是否打开
initial_window_count = len(self.driver.window_handles)
print(f"初始窗口数量: {initial_window_count}")
# 点击卡片:先尝试直接点击,失败则用JavaScript
try:
card.click()
except:
print("直接点击失败,尝试使用JavaScript点击...")
self.driver.execute_script("arguments[0].click();", card)
# 等待新窗口打开
wait = WebDriverWait(self.driver, 10)
try:
wait.until(EC.number_of_windows_to_be(initial_window_count + 1))
print(f"新窗口已打开,当前窗口数量: {len(self.driver.window_handles)}")
# 切换到新窗口,提取详情页URL
new_window = self.driver.window_handles[-1]
self.driver.switch_to.window(new_window)
current_url = self.driver.current_url
print(f"课程详情页URL: {current_url}")
# 验证URL是否为课程详情页
if '/course/' in current_url or '/learn/' in current_url:
course_info = {
'url': current_url,
'name': course_name,
'college': course_college
}
course_info_list.append(course_info)
print(f"成功添加课程信息: {course_info}")
# 关闭新窗口,切回主窗口
self.driver.close()
self.driver.switch_to.window(main_window)
time.sleep(2)
except TimeoutException:
print(f"等待新窗口打开超时")
# 清理异常窗口
current_window_count = len(self.driver.window_handles)
if current_window_count > initial_window_count:
try:
new_window = self.driver.window_handles[-1]
self.driver.switch_to.window(new_window)
self.driver.close()
self.driver.switch_to.window(main_window)
print(f"已关闭意外打开的新窗口")
except Exception as close_error:
print(f"关闭新窗口时出错: {close_error}")
except Exception as e:
print(f"处理新窗口时出错: {e}")
# 恢复主窗口
try:
self.driver.switch_to.window(main_window)
except Exception as switch_error:
print(f"切换回主窗口时出错: {switch_error}")
except Exception as e:
print(f"处理第 {i+1} 个课程卡片时出错: {e}")
# 清理所有额外窗口,恢复主窗口
try:
while len(self.driver.window_handles) > 1:
self.driver.switch_to.window(self.driver.window_handles[-1])
self.driver.close()
self.driver.switch_to.window(main_window)
except Exception as close_error:
print(f"关闭标签页时出错: {close_error}")
continue
# 爬取5个课程后提前终止,加快测试
if len(course_info_list) >= 5:
break
print(f"\n✅ 频道页面滚动和课程提取完成。共收集到 {len(course_info_list)} 个课程信息。")
return course_info_list
def scrape_detail_page(self, course_info):
"""核心功能:爬取课程详情页,提取完整的课程信息"""
url = course_info['url']
card_course_name = course_info['name'] # 卡片提取的课程名称
card_course_college = course_info['college'] # 卡片提取的学校名称
print(f"\n👉 正在爬取详情: {url}")
main_window = self.driver.current_window_handle # 记录主窗口
# 打开新窗口,避免覆盖主窗口
self.driver.execute_script(f"window.open('{url}');")
self.driver.switch_to.window(self.driver.window_handles[-1])
try:
# 等待详情页核心元素加载
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.XPATH, "//span[contains(@class, 'course-title')] | //h1")))
except:
print("❌ 详情页加载超时,跳过。")
self.driver.close()
self.driver.switch_to.window(main_window)
return
# 初始化数据字典,设置默认值
data = {
'cCourse': '', 'cCollege': '', 'cTeacher': '', 'cTeam': '',
'cCount': '0', 'cProcess': '', 'cBrief': '', 'course_id': ''
}
try:
# 解析课程ID:从URL中提取(如/course/123456 → 123456)
match = re.search(r'course/([A-Za-z0-9_-]+)', url)
if match: data['course_id'] = match.group(1)
# 1. 课程名称:优先使用卡片提取的信息,避免详情页定位失败
try:
if card_course_name:
data['cCourse'] = card_course_name
else:
data['cCourse'] = self.driver.find_element(By.XPATH,
"//span[contains(@class, 'course-title')] | //h1").text.strip()
except:
pass
# 2. 学校名称:优先使用卡片提取的信息,多XPath补充
try:
if card_course_college:
data['cCollege'] = card_course_college
else:
school_xpaths = [
"//a[contains(@class, 'school')]",
"//div[contains(@class, 'uni-name')]",
"//div[contains(@class, 'school-name')]",
"//span[contains(text(), '学校')]/following-sibling::*",
"//div[contains(@class, 'course-header')]//a[contains(@href, 'university')]",
"//div[contains(@class, 'course-provider')]//a"
]
for xpath in school_xpaths:
try:
school_elem = self.driver.find_element(By.XPATH, xpath)
if school_elem.text.strip():
data['cCollege'] = school_elem.text.strip()
break
except:
continue
except:
pass
# 3. 主讲教师:多XPath定位
try:
data['cTeacher'] = self.driver.find_element(By.XPATH,
"//a[contains(@class, 'teacher-name')] | //div[contains(@class, 'teacher_info')]//a | //div[contains(@class,'u-tchcard')]//h3").text.strip()
except:
pass
# 4. 课程进度/时间:处理换行符,格式化显示
try:
data['cProcess'] = self.driver.find_element(By.XPATH,
"//div[contains(@class, 'course-enroll-info')]//div[contains(@class, 'time')] | //div[contains(@class, 'term-progress')]//span[contains(@class,'text')]").text.replace(
'\n', ' ').strip()
except:
data['cProcess'] = "暂无时间信息" # 空值默认值
# 5. 参加人数:提取含单位的原始信息
try:
data['cCount'] = self.driver.find_element(By.XPATH,
"//span[contains(@class, 'course-enroll-count')] | //span[contains(@class,'count')] | //span[contains(text(), '人参加')]").text.strip()
except:
pass
# 6. 课程简介:截取前500字,避免文本过长
try:
brief_elem = self.driver.find_element(By.XPATH,
"//div[contains(@class, 'category-content')] | //div[contains(@class, 'm-course-content')] | //div[@class='content']")
data['cBrief'] = brief_elem.text.strip()[:500] + "..."
except:
data['cBrief'] = "无简介" # 空值默认值
# 7. 团队成员:提取所有教师,过滤主讲教师
try:
team_members = []
team_elems = self.driver.find_elements(By.XPATH,
"//div[contains(@class, 'm-teachers')]//h3 | //div[contains(@class, 'u-tchcard')]//h3 | //div[contains(@class, 'teacher-list')]//h3")
for t in team_elems:
name = t.text.strip()
if name:
team_members.append(name)
# 数据清洗:过滤主讲教师,避免重复
if len(team_members) > 1:
filtered_members = [name for name in team_members if name != data['cTeacher']]
data['cTeam'] = ", ".join(filtered_members) if filtered_members else ", ".join(team_members)
elif len(team_members) == 1:
data['cTeam'] = team_members[0]
else:
data['cTeam'] = data['cTeacher'] if data['cTeacher'] else "无团队成员信息"
except:
data['cTeam'] = data['cTeacher'] if data['cTeacher'] else "无团队成员信息"
# 保存数据到数据库
self.save_to_mysql(data)
except Exception as e:
print(f"❌ 解析详情页出错: {e}")
finally:
# 无论是否异常,都关闭详情页窗口,切回主窗口
self.driver.close()
self.driver.switch_to.window(main_window)
以下是分析图片


2.结果展示

图2:MySQL 中 course_info 表存储的课程数据截图,可见课程名称、学校、教师等字段精准匹配

3、心得体会
- 登录流程的核心难点与解决思路
MOOC 网登录的核心难点是「iframe 嵌套表单」和「反爬检测」,解决思路:
iframe 处理:先等待 iframe 加载完成,再切换,操作完成后切回主文档;
多 XPath 定位:为每个登录元素配置多组 XPath,适配页面更新后的元素结构;
模拟人工操作:增加输入间隔、滚动、JavaScript 点击,避免被判定为自动化程序;
MOOC 网的课程卡片、详情页元素结构多样,单一 XPath 易失效,解决思路:
多 XPath 策略:为每个字段配置多组 XPath,依次尝试直至定位成功;
优先提取列表页信息:课程名称、学校等基础信息从列表页卡片提取,减少详情页的解析压力;
三、大数据实时分析处理实验
Python脚本生成测试数据
MRS Master节点上创建了数据模拟脚本并生成了第一批测试数据。

配置Kafka安装Flume客户端




使用DLI Flink作业进行数据分析。


任务一作业链接:
任务二作业链接:https://gitee.com/spacetime666/2025_crawl_project/blob/master/作业4/task2/task2.py
浙公网安备 33010602011771号