一、前言
时隔一个月,今天来给我的程序添加一些新功能,上一期实现了飞猪平台的机票价格检测,那么这一期我会来实现携程平台的机票价格检测,一些前期准备工作可以看看上一期的文章:Python+selenium实现机票价格监测(一)。废话不多说,马上进入正文。
二、需求分析
首先打开携程网站:【携程机票】飞机票查询,机票预订,机票价格查询,打折特价机票 ,进入网页后会发现当前页面直接就将机票搜索展示在了首页上;
选择出发地、目的地、出发日期、点击查询即可查询到对应的航班信息:
然后将展示出来的数据分别取出来按自己想法做拼接即可完成携程平台的对应功能。
三、开始编码
1.配置浏览器驱动
在config文件夹下创建config.py文件,在里面添加浏览器驱动配置信息:
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium import webdriver
import random
# 设置 Chrome 的路径
options = Options()
# 替换为你的 Chrome 安装路径
options.binary_location = r"G:\Program Files (x86)\Chrome\Application\chrome.exe"
options.add_argument(
"--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36")
# 设置Chromedriver的路径
service = ChromeService(
executable_path=r"C:\Users\75505\Desktop\机票价格实时检测\drivers\chromedriver.exe")
options.add_experimental_option("detach", True) # 保持浏览器窗口打开
2.编写方法
在functions文件夹下创建ctrip_func.py文件,在这个文件里面我们要将前面需求分析细化出来的步骤抽象成一个个方法
(1)打开网站
def open_url(driver):
driver.get("https://flights.ctrip.com/online/channel/domestic")
该功能是打开携程网站。
(2)选择行程
打开网站后,根据需求去选择单程航班、往返航班还是多程航班,通过开发者调试工具(F12),左上角的小鼠标箭头,选中需要操作的元素。
我们可以发现单程、往返、多程按钮是在同一个ul列表下面,所以如果此时该ul列表可以唯一查找,那就可以通过获取ul元素下的li列表通过索引来分别获取各个按钮。
在控制台按快捷键ctrl+F,可以通过XPATH、CSS选择器等多种选择元素的方式来查找元素,这里通过CSS选择器:'ul.form-select-radio-group li',可以刚好查找到我们所需要的元素。代码如下:
def click_button(driver):
buttons = WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, 'ul.form-select-radio-group li'))
)
# 单程
button_one = buttons[0]
# 往返
button_two = buttons[1]
# 多程
button_three = buttons[2]
button_one.click()
(3)选择城市
通过网页元素查找(F12),查找出发地和目的地的元素位置,可以发现这两个元素仅仅是class的区别,那么就可以将这两个选择框封装成一个方法,只需要改变class的值就能够分别选中这两个元素;
通过CSS选择器:"div.owrt_outside div.cflt-poi input.form-input-v3[name='owDCity']"能够唯一查找到出发地;"div.owrt_outside div.cflt-poi input.form-input-v3[name='owACity']"能够唯一查找到目的地。因此代码如下:
def select_city(action: ActionChains, driver, input_name, city_name):
try:
city_element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located(
(By.CSS_SELECTOR, f"div.owrt_outside div.cflt-poi input.form-input-v3[name='{input_name}']"))
)
city_element.click()
time.sleep(0.3)
# 聚焦并全选已有内容
action.key_down(Keys.CONTROL).send_keys(
"a").key_up(Keys.CONTROL).perform() # 全选
action.send_keys(Keys.BACKSPACE).perform() # 删除
# 输入新内容
for char in city_name:
city_element.send_keys(char)
time.sleep(0.5)
except Exception as e:
print(f"选择城市时出现异常{e}")
将className通过参数传入,由于默认在输入框内有内容,这里我是通过ActionChains类来模拟键盘全选并且删除已有内容,然后延迟输入城市名(避免被检测)。
这里我们手动输入城市之后,会有选择城市的弹窗出现,当你输入完整的城市名时,弹窗第一个元素一定是你想要选择城市,因此需要在该方法中继续添加一个选择弹窗第一个的功能,代码如下:
# 选择第一个匹配项
try:
first_match = WebDriverWait(driver, 10).until(
EC.presence_of_element_located(
(By.CSS_SELECTOR, "div.owrt_outside div.cflt-poi div.cflt-poi-selector-new div.poi-address:first-child"))
)
first_match.click()
except Exception as e:
print(f"没有选择到第一个下拉框的元素{e}")
(4)选择日期
查找到日期元素所在的位置,想办法通过CSS选择器或者XPATH定位来唯一定位到该元素。
我们通过元素内的属性来唯一选择该元素:'div.date-components input[aria-label="请选择日期"][type="text"][placeholder="yyyy-mm-dd"][readonly]'。
点击日期框框后,我们会发现,日期框分为左右两个,左边的日期框是当前月份,右边的日期框是下一个月份,当点击下一页的按钮时,日期会跳转两个月。例如当前左边是3月,右边是4月,点击一次下一页会变成左边是5月,右边是6月。那么我们如果要选择目标月份,就必须对当前日期框的月份进行判断,当左侧出现目标月份时,需要选择到左侧的框;当右侧出现目标月份时,需要选择到右侧的框;若当前两个框都没有目标月份,则需要点击下一页按钮,并且继续获取月份值与目标月份进行对比。
这里会出现一个状况,当打开日期框时,如果通过F12去查找元素会出现点击过后日期框就消失的情况,这里就需要用到开发者工具的断点功能了,在F12控制台里面导航栏中找到“source”,然后选择左下角的“Event Listener Breakpoints”,找到“mouse”,勾选“click”。
给鼠标单击事件打上断点之后,点击一下日期,会将弹出的日期框停留在页面上,此时再通过F12定位到日期框元素,观察右侧结构,可以通过span.month的文本获取到月份的值,通过div.date-month定位到下面的日期元素,那接下来就好办了。
所以流程应该为:点击日期->获取左右日期框的月份数->与目标月份做比较->获取下方日期元素->选择目标日期的元素,流程如下图所示:
又因为Element元素的text值是将该元素下的所有文本值都返回,这里会返回“3月”、“4月”,创建一个新的方法提取出数字部分并转换成int型:
def extract_month_number(month_text):
"""
从包含月份的文本中提取月份数字
:param month_text: 包含月份的文本,如 "3月"
:return: 提取的月份数字
"""
# 去除非数字字符
month_number = ''.join(filter(str.isdigit, month_text))
if month_number:
return int(month_number)
return None
最终选择日期代码如下:
def select_date(driver, month, day):
# 首先获取页面上的月份是否和传入参数的月份相同
try:
date_selector = WebDriverWait(driver, 10).until(
EC.presence_of_element_located(
(By.CSS_SELECTOR, 'div.date-components input[aria-label="请选择日期"][type="text"][placeholder="yyyy-mm-dd"][readonly]'))
)
# 展开日期框
date_selector.click()
# 获取当前月份
current_months = WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, 'span.month'))
)
left_month = extract_month_number(current_months[0].text) # 左边月份3
right_month = extract_month_number(current_months[1].text) # 右边月份4
# 判断是否需要点击下月按钮
while month not in [left_month, right_month]:
# 点击下一个月
next_month_buttons = WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, 'span.in-date-picker.icon.next-ico.iconf-right'))
)
next_month_button = next_month_buttons[1]
next_month_button.click()
# 更新当前显示的两个月份
current_months = WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, 'span.month'))
)
left_month = extract_month_number(current_months[0].text)
right_month = extract_month_number(current_months[1].text)
# 选择月份框框
month_div = WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, 'div.date-month'))
)
left_month_div = month_div[0]
right_month_div = month_div[1]
# 选择日期,确认目标月份在左边还是在右边
if month == left_month:
# 定位日期元素
target_day = left_month_div.find_elements(
By.XPATH, f"//span[@class='date-d' and text()='{day}']")
target_day[0].click()
else:
target_day = right_month_div.find_elements(
By.XPATH, f"//span[@class='date-d' and text()='{day}']")
target_day[1].click()
except Exception as e:
print(f"选择日期时出现异常{e}")
(5)点击搜索
这个方法就很简单了,找到搜索按钮的元素,单击即可,代码如下:
def click_search(driver):
try:
search_button = WebDriverWait(driver, 10).until(
EC.presence_of_element_located(
(By.CSS_SELECTOR, 'button.search-btn'))
)
search_button.click()
except Exception as e:
print(f"点击搜索时出现异常{e}")
(6)滚动屏幕
点击搜索后,进入航班页面会发现页面显示不完整,需要将屏幕一直往下滑滑到最底的时候才发现航班信息全部展示完毕,因此代码如下:
def scroll_to_bottom_by_mouse_wheel(driver, action: ActionChains):
"""
使用鼠标滚轮操作将网页滚动到最下方
:param driver: 浏览器驱动对象
"""
try:
retry_count = 0
max_retries = 10 # 最大尝试次数
# 等待航班信息出现
WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located((By.CSS_SELECTOR, 'div.flight-box')))
while retry_count < max_retries:
# 模拟鼠标滚轮向下滚动
action.scroll_by_amount(0, 500).perform()
time.sleep(0.5) # 等待页面加载
retry_count += 1
# 检查是否已经滚动到底部
if not driver.execute_script("return document.body.scrollHeight > window.innerHeight;"):
break
except Exception as e:
print(f"滚动页面时出现异常: {e}")
(7)获取航班信息
通过F12查看所有航班信息,发现所有航班信息的属性都是一样的,我们选中其中一行,观察每一行航班信息的数据结构是什么样的,从中提取所需的属性,这里是要获取航空公司名称、航班名称、出发时间、出发地、出发机场、到达时间、目的地、到达机场、价格信息并进行数据处理。
通过CSS选择器:"div.flight-box"可以获取到航班信息的列表,之后通过每行对应的标签,例如"div.airline-name"获取航空公司名称、"div.depart-box div.time"获取到达时间等,之后进行组装,最终代码如下:
def get_flight_info(driver):
try:
flight_info_list = []
flight_infos = WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, 'div.flight-box'))
)
for flight in flight_infos[1:]:
# print(flight_info.text)
if isinstance(flight, WebElement):
try:
# 提取所需标签的信息
airline_name = flight.find_element(
By.CSS_SELECTOR, 'div.airline-name').text
plane_name = flight.find_element(
By.CSS_SELECTOR, 'span.plane-No').text
depart_box = flight.find_element(
By.CSS_SELECTOR, 'div.depart-box')
depart_time = depart_box.find_element(
By.CSS_SELECTOR, 'div.time').text
depart_airports = depart_box.find_elements(
By.CSS_SELECTOR, 'div.airport span')
arrive_box = flight.find_element(
By.CSS_SELECTOR, 'div.arrive-box')
arrive_time = arrive_box.find_element(
By.CSS_SELECTOR, 'div.time').text
arrive_airports = arrive_box.find_elements(
By.CSS_SELECTOR, 'div.airport span')
price = flight.find_element(
By.ID, 'travelPackage_price_undefined').text # ¥690
# 组装信息
plane_info = airline_name+plane_name
fly_time = depart_time+'-'+arrive_time
fly_arr = depart_airports[0].text+depart_airports[1].text + \
'-'+arrive_airports[0].text+arrive_airports[1].text
flight_info = [plane_info, fly_time, fly_arr, price]
flight_info_list.append(flight_info)
except Exception as e:
print(f"提取航班部分信息时出现异常{e}")
return flight_info_list
except Exception as e:
print(f"获取航班信息时出现异常{e}")
最后导入的包至少如下图所示:
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
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
四、运行结果
首先需要创建一个主运行文件,ctrip_test.py,里面开始调用刚刚写好的函数,代码如下:
import functions.xie_cheng as xc
import config.config as config
import time
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
from selenium.webdriver.common.action_chains import ActionChains
import functions.fly_pig as fp
import time
from win10toast import ToastNotifier
import threading
def main(driver, dep_city, arr_city, month, date):
# 打开网站
xc.open_url(driver)
action = ActionChains(driver)
# 选择单程
xc.click_button(driver)
time.sleep(0.3)
# 选择出发城市
xc.select_city(action, driver, "owDCity", dep_city)
# 选择目的城市
xc.select_city(action, driver, "owACity", arr_city)
# 选择日期
xc.select_date(driver, month, date)
time.sleep(0.5)
# 点击搜索
xc.click_search(driver)
#print("请手动验证!!")
#time.sleep(15)
# 滚动
xc.scroll_to_bottom_by_mouse_wheel(driver, action)
time.sleep(0.3)
# 获取航班信息
flight_info_list = xc.get_flight_info(driver)
# 排序航班并打印
if isinstance(flight_info_list, list):
flight_info_list.sort(key=lambda x: float(x[-1].replace('¥', '')))
for print_flight in flight_info_list:
print(print_flight)
if __name__ == "__main__":
driver = config.driver
main(driver, "海口", "深圳", 6, 30)
driver.quit()
这里有个小问题,博主在刚开始做这个功能的时候,运行脚本是不会出现验证码弹窗的,可能是运行次数太频繁导致现在每次都需要手动验证一下,这个问题大家还是注意一下,不要同一时间运行太多次脚本,同一天内不要打开超过30次或许可以避免这个问题。
最终运行结果如下:
博主后续也是将每次查询出来的数据都添加到了数据库,以备日后可以添加每日低价提醒等新功能
那么这篇文章就到这里了,后续还会继续更新其他平台的教程,大家喜欢的话动动小手点个赞,谢谢大家!