第四次作业-何玮鑫

作业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. 结果展示

image

图 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)

以下是分析图片
image
image

2.结果展示

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

3、心得体会

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

三、大数据实时分析处理实验

Python脚本生成测试数据
MRS Master节点上创建了数据模拟脚本并生成了第一批测试数据。
image

配置Kafka安装Flume客户端
image
image
image

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

任务一作业链接:
任务二作业链接:https://gitee.com/spacetime666/2025_crawl_project/blob/master/作业4/task2/task2.py

posted @ 2025-12-09 22:02  chen宇新  阅读(9)  评论(0)    收藏  举报