Python+selenium实现机票价格监测(一)

一、前言

        由于3月份海南有周杰伦的演唱会,但是机票价格太贵了,每天都会有变动,于是产生了一个能够监控机票价格的想法。本项目是通过python+selenium实现操作浏览器,通过OTA平台飞猪来查询机票价格的一个程序。

二、环境部署

安装python

        1.打开python官网:Download Python | Python.org,根据自己系统选择所需要的python版本进行下载(博主这里是3.9.13版本)

        2.下载完成后,双击打开刚刚下载的安装程序:

        3.这里我们选择自定义安装,并且勾选添加环境变量;

        4.直接点击Next;

        5.可以选择你要安装的路径,选择完毕后点击Install。等待安装成功即可。

验证python环境

        1.按组合键win+r,输入cmd打开命令行;

        2.在窗口中输入python,如果弹出版本号即说明python环境安装成功

安装VSCODE

        vscode不是一定的,如果你有使用熟悉的IDE(例如pycharm)直接使用就好,博主这里习惯使用vscode,因此这里是安装vscode的教学:

        1.打开vscode官网:Visual Studio Code - Code Editing. Redefined,windows系统可以直接点击页面上的下载,MacOs需要点击other platform进入其他平台下载页面去下载。

        2.下载完成后,双击打开刚下载完成的安装程序,勾选“我同意协议”,点击下一步。

        3.默认勾选即可,可勾可不勾创建桌面快捷方式,点击下一步。

        4.点击安装。

        5.安装完成后重启电脑,会自动将vscode路径添加到环境变量PATH中。

三、编码准备

        1.打开vscode,选择左侧导航栏的扩展按钮(ctrl+shift+X),在应用商店中分别搜索并安装:

                (1) 简体中文:将vscode翻译成中文

                (2) MarsCode AI:豆包AI大模型,目前已接入deepseek R1,免费使用,推荐

                (3) Pylance:提供python智能代码提示、自动补全、类型判断等功能

                (4) Python:最重要的扩展,提供python环境、代码调试

                (5) Python Preview:实现可视化工具

        

        2.在vscode中新建文件夹,文件夹名:机票价格监测,之后都将此文件夹作为主文件夹

        3.在主文件夹下分别创建config文件夹(用于存放webdriver配置文件)、function文件夹(用于存放各个封装函数,提升代码简洁和可读性)、drivers文件夹(用于存放浏览器驱动chomedriver.exe)。

        4.因为本项目用到了selenium自动化操作web,所以需要下载浏览器驱动,博主这里使用的是谷歌浏览器,因此需要下载对应浏览器对应版本的浏览器驱动:

                (1)查看自己浏览器的版本号,在网址栏输入chrome://settings/help 可以查看版本号

                (2)在浏览器输入https://storage.googleapis.com/chrome-for-testing-public/132.0.6834.197/win64/chromedriver-win64.zip, 将其中132.0.6834.197更换成自己浏览器的版本号,就可以直接下载驱动

                (3)将下载完成的压缩包内的chromedriver.exe解压出来复制到drivers文件夹中即可。

四、需求分析

我们打开飞猪官网:飞机票查询-机票预订、酒店预订查询、客栈民宿、旅游度假、门票签证【飞猪旅行】,我们可以发现在本项目中实际需要用到的功能只有机票查询

将机票查询分为:出发城市、到达城市、选择出发日期、点击搜索四个操作。

点击搜索之后会弹出一个新的页面,里面列出了满足条件的所有航班,所以只需要将所有航班信息处理成自己想要的信息。

五、开始编码

config.py

在config文件夹下创建config.py文件,将浏览器相关的配置集中管理

from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service as ChromeService

# 设置 Chrome 的路径
options = Options()
options.binary_location = r"G:\Program Files (x86)\Chrome\Application\chrome.exe"  # 替换为你的 Chrome 安装路径
options.add_experimental_option("detach", True)  # 保持浏览器窗口打开
#添加无头模式的选项
# options.add_argument("--headless")
#设置Chromedriver的路径
service = ChromeService(executable_path=r"C:\Users\75505\Desktop\机票价格实时检测\drivers\chromedriver.exe")
driver = webdriver.Chrome(service=service, options=options)

调试时关闭无头模式,等代码趋于稳定后可打开无头模式,因为在主函数中有鼠标操作,如果未打开无头模式在后台挂着该脚本有可能出现前台鼠标移动会影响脚本运行。

fly_pig.py

在function文件夹下创建fly_pig.py文件,将实际操作封装成一个个函数

1.选择地点(select_city):

在飞猪官网,通过F12查找元素可以发现,出发城市和目的城市的<input>只是id差异,其他全都一致,因此可以封装成为一个方法

方法如下:        

def select_city(driver, input_id, city_name):
    """
    在页面上选择城市
    :param driver: 浏览器驱动实例
    :param input_id: 城市输入框的 id
    :param city_name: 要选择的城市名称
    """
    search_input = driver.find_element(By.XPATH, f"//input[@id='{input_id}']")
    search_input.click()
    WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, 'div.ant-select.ant-select-borderless.ant-select-in-form-item.ant-select-auto-complete.ant-popover-open.css-1ihjxt5.ant-select-single.ant-select-show-search'))
    )
    city_element = driver.find_element(By.XPATH, f"//div[@class='city-search-tab-content-item-li' and text()='{city_name}']")
    return city_element

2.选择日期(select_month & select_date):

通过日期选择框,我们不难发现,如果想要定位到自己所需要的日期,首先需要将当前的月份和目标月份做判断,如果目标月份大于当前月份,那么需要将月份往后推,直到目标月份。

select_month如下:

def select_month(driver, target_month):
    """
    此函数用于在页面上选择指定的月份。
    :param driver: Selenium WebDriver 实例
    :param target_month: 想要选择的月份(整数)
    :return: 选中的月份按钮元素,如果未找到则返回 None
    """
    while True:
        try:
            # 定位所有 <button class="ant-picker-month-btn"> 元素
            month_buttons = driver.find_elements(By.CSS_SELECTOR, 'button.ant-picker-month-btn')
            for button in month_buttons:
                # 提取按钮文本中的月份数字
                button_text = button.text
                cleaned_text = month_num = int(button_text.replace('月', ''))
                # month_num = int(button_text.replace('月', '')) #month_num = 3
                if cleaned_text:
                    month_num = int(cleaned_text)
                else:
                    print("提取的月份文本为空,跳过该按钮")
                    continue

                if month_num > target_month:
                    # 月份大于目标月份,点击上一个月按钮
                    prev_button = driver.find_element(By.CSS_SELECTOR, 'button.ant-picker-header-prev-btn')
                    prev_button.click()
                    break
                elif month_num < target_month:
                    # 月份小于目标月份,点击下一个月按钮
                    next_button = driver.find_element(By.CSS_SELECTOR, 'button.ant-picker-header-next-btn')
                    next_button.click()
                    break
                else:
                    # 月份等于目标月份,点击该按钮并返回
                    # button.click()
                    return target_month
            else:
                # 如果没有找到匹配的月份,继续循环
                continue
        except Exception as e:
            print(f"出现异常: {e}")
            time.sleep(1)
    return None

月份选择完毕之后,该选择具体的日子,在这里添加条件,根据“一三五七八十腊,三十一天永不差”可知1、3、5、7、8、10、12月有31天,2月目前只做了28天,剩下4、6、9、11月只有30天,做一个异常处理,然后根据文本值选择对应日期

select_date如下:

def select_date(driver, current_month, target_date):
    """
    此函数用于在页面上选择指定的日期。
    :param driver: Selenium WebDriver 实例
    :param current_month: 当前已选择的月份(整数)
    :param target_date: 想要选择的日期(整数)
    :return: 选中的日期元素,如果未找到或日期不合法则返回 None
    """
    # 定义每个月的最大日期
    max_days = {
        1: 31, 3: 31, 5: 31, 7: 31, 8: 31, 10: 31, 12: 31,
        4: 30, 6: 30, 9: 30, 11: 30,
        2: 28
    }
    try:
        # 检查日期是否在合法范围内
        if current_month not in max_days or target_date < 1 or target_date > max_days[current_month]:
            print(f"日期 {target_date} 号在 {current_month} 月是不合法的。")
            return None
        # 定位所有符合条件的日期单元格
        date_cells = driver.find_elements(By.CSS_SELECTOR, 'td.ant-picker-cell.ant-picker-cell-in-view')
        # print(f"找到 {len(date_cells)} 个日期单元格")  # 输出找到的日期单元格数量
        for index, cell in enumerate(date_cells):
            try:
                # 定位单元格内的日期数字元素
                date_inner = cell.find_element(By.CSS_SELECTOR, 'div.ant-picker-cell-inner')
                # 获取日期数字文本
                date_text = date_inner.text
                # print(f"第 {index + 1} 个单元格的日期文本: '{date_text}'")  # 输出每个单元格的日期文本
                # 增加对空字符串的检查
                if date_text and date_text.isdigit() and int(date_text) == target_date:
                    # 找到匹配的日期,点击该元素并返回
                    # date_inner.click()
                    return date_inner
            except Exception as e:
                print(f"处理第 {index + 1} 个单元格时出现异常: {e}")
        return None
    except Exception as e:
        print(f"选择日期时出现异常: {e}")
        return None

3.点击搜索框(click_search_button)

出发城市、目的城市、日期都选择完毕过后,需要点击搜索按钮,将点击按钮封装成一个方法

click_search_button方法如下:

def click_search_button(driver):
    """
    点击搜索按钮
    :param driver: 浏览器驱动实例
    """
    search_button = driver.find_element(By.XPATH, "//button[@data-spm-click='gostr=/tbtrip;locaid=dflightSearch1;id=flight-searchbar-click1;title=机票搜索-国内']")
    search_button.click()

4.切换到新窗口(switch_to_new_window)

点击搜索过后会弹出一个新窗口来显示航班信息,因此我们需要将此时driver选中的页面切换到新窗口中,通过切换窗口句柄就可以实现。

switch_to_new_window方法如下:

def switch_to_new_window(driver):
    """
    切换到新打开的窗口
    :param driver: 浏览器驱动实例
    """
    time.sleep(2)
    current_window_handle = driver.current_window_handle
    all_window_handles = driver.window_handles
    # print(all_window_handles)
    if len(all_window_handles) > 1 and current_window_handle == all_window_handles[0]:
        driver.switch_to.window(all_window_handles[-1])
        print("已切换到新窗口")

5.获取航班信息(extract_flight_info)

切换到新窗口之后,发现每一行航班都是在<div id=J_FlightListBox>下的class相同的div,因此我们可以选择到id=J_FlightListBox,再用该div去选择下面所有的div,从而返回一个航班列表。

我们选中一个航班列表,发现里面的数据是通过<table>来存储,那么我们就可以自己选择需要的数据来进行处理,这里我只选择了航班名、起降时间、起抵机场、价格。

extract_flight_info方法如下:

def extract_flight_info(flight_list):
    """
    从 flight_list 中提取每个航班的特定标签内容,并将其存储在一个二维列表中。

    :param flight_list: 包含航班信息元素的列表
    :return: 二维列表,每个子列表包含一个航班的特定标签内容
    """
    flight_info_list = []
    for flight in flight_list:
        if isinstance(flight, WebElement):
            try:
                # 提取所需标签的内容
                line = flight.find_element(By.CSS_SELECTOR, 'span.J_line.J_TestFlight').text
                deptime = flight.find_element(By.CSS_SELECTOR, 'p.flight-time-deptime').text
                s_time = flight.find_element(By.CSS_SELECTOR, 'span.s-time').text
                port_dep = flight.find_element(By.CSS_SELECTOR, 'p.port-dep').text
                port_arr = flight.find_element(By.CSS_SELECTOR, 'p.port-arr').text
                price = flight.find_element(By.CSS_SELECTOR, 'span.J_FlightListPrice').text

                arrtime = deptime + "-" + s_time
                target_arr = port_dep + "-" + port_arr

                # 将提取的内容保存到一个列表中
                flight_info = [line, arrtime, target_arr, "¥"+price]
                # 将该列表添加到总列表中
                flight_info_list.append(flight_info)
            except Exception as e:
                print(f"提取航班信息时出现异常: {e}")
        else:
            print(f"列表中的元素 {flight} 不是 WebElement 类型,跳过该元素。")
    return flight_info_list

截止目前,我们所需要的方法就已经编写完毕,接下来只需要在主函数中调用并做一些额外的处理即可。最后将所需要的库导入进来:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
from selenium.webdriver.remote.webelement import WebElement

air_price.py(作为主文件)

1.打开网站

driver.get("https://www.fliggy.com/?_er_static=true")

2.选择出发城市

dep_city_element = fp.select_city(driver,"form_depCity",dep_city)
dep_city_element.click()

3.选择目的城市

        这里选择目的城市出现了一些问题,由于选择城市的弹框是通过JS动态生成,在页面刚加载时没有加载该div,需要点击出发城市或者目的城市之后,才会在页面上加载出来,并且出发和目的是分开的两个相同的div,当使用select_city去选择目的城市的时候会出现目标元素不可被点击的异常,因此这里才用了鼠标点击的方法。

#创建鼠标对象
mouse = ActionChains(driver)

to_search = driver.find_element(By.XPATH,"//input[@id='form_arrCity']")
# 点击目的城市选择框
to_search.click()
# 等待点击后的元素出现,使用不包含 ant-popover-hidden 的 class 选择器
post_click_element = WebDriverWait(driver, 20).until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'div.ant-popover.css-1ihjxt5.css-1ihjxt5:not(.ant-popover-hidden)')))

#定位到目标城市并单击
arr_city_elements = post_click_element.find_elements(By.XPATH,f".//div[@class='city-search-tab-content-item-li' and text()='{arr_city}']")
#让鼠标移动到该元素进行点击
mouse.move_to_element(arr_city_elements[0]).move_by_offset(0,20).click().perform()

4.点击搜索按钮

#选择出发日期 
go_date = driver.find_element(By.XPATH,"//input[@id='form_depDate']")
go_date.click()
#循环检测当前窗口是否选中为所需月份
TARGET_MONTH = fp.select_month(driver,month)
#定位到日期并单击
go_day = fp.select_date(driver,TARGET_MONTH,date)
try:
    go_day.click()
except Exception as e:
    print(f"未找到元素{e}")

#点击搜索按钮   
fp.click_search_button(driver)

5.切换到新窗口

#切换到新窗口
fp.switch_to_new_window(driver)

6.获取航班信息

        我这里是根据自己的需求,返回了一个航班中的最低价格,并且每执行完成一次都会通过windows10的消息弹窗来提醒,各位大佬可以根据自己需求来写这一段,如果只是返回航班列表只需要调用一次extract_flight_info方法即可。

try:
           #定位到航班列表,返回的是一个列表
            flight_list = WebDriverWait(driver, 20).until(EC.presence_of_all_elements_located((By.CSS_SELECTOR,"#J_FlightListBox .flight-list-item.clearfix.J_FlightItem")))
            if flight_list:
                flight_info_list = fp.extract_flight_info(flight_list)
                flight_info_list.sort(key=lambda x: float(x[-1].replace('¥', '')))
                for print_flight in flight_info_list:
                    print(print_flight)
                #获取最低价格
                if flight_info_list:
                    lowest_price = flight_info_list[0][-1]
                    #使用win10toast显示右下角弹窗消息
                    toaster = ToastNotifier()
                    def show_notification():
                        try:
                            toaster.show_toast("最低价格", f"最低价格为: {lowest_price}", duration=30)
                        except Exception as e:
                            print(f"显示弹窗消息时出现异常:{e}")
                    #创建一个线程来显示通知
                    notifiction_thread = threading.Thread(target=show_notification)
                    notifiction_thread.daemon = True
                    notifiction_thread.start()
                    print(f"最低价格为: {lowest_price}")
            else:
                print("未找到航班列表元素")
            #定位到航班信息
        except Exception as e:
            print(f"等待航班列表元素时出现异常:{e}")

7.资源回收

driver.quit()

记得导入所需的库:

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
import functions.fly_pig as fp
import config.config as config
import time
from win10toast import ToastNotifier
import threading

本项目到此已接近尾声,目前只支持广州、深圳、海口、三亚四个城市,因为我的需求只需要这四座城市。还有许多不足,比如可以添加循环查询,只需挂在后台就可以不断监测到当前机票价格的最低价,还可以添加失败重试,gui界面等。

六、运行结果

添加gui界面后运行结果:

新人博主,目前正在学习自动化测试,会在这里记录我的学习成果以及经验,有许多不足之处,还请各位前辈多多指教。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值