Python es un asistente para encontrar vuelos baratos para quienes les gusta viajar

La autora del artículo, cuya traducción publicamos hoy, dice que su objetivo es hablar sobre el desarrollo de un raspador web en Python usando Selenium, que busca los precios de las tarifas aéreas. Al buscar boletos, se utilizan fechas flexibles (+ - 3 días en relación con las fechas especificadas). Scraper guarda los resultados de la búsqueda en un archivo de Excel y envía a la persona que lo lanzó un correo electrónico con información general sobre lo que logró encontrar. El objetivo de este proyecto es ayudar a los viajeros a encontrar las mejores ofertas.



Si al tratar con el material siente que está perdido, eche un vistazo a este artículo.

Que estamos buscando


Usted es libre de usar el sistema descrito aquí de la manera que desee. Por ejemplo, lo usé para buscar recorridos de fin de semana y boletos para mi ciudad natal. Si se toma en serio la búsqueda de tickets rentables, puede ejecutar el script en el servidor (un servidor simple, por 130 rublos al mes, es bastante adecuado para esto) y hacerlo funcionar una o dos veces al día. Los resultados de la búsqueda le serán enviados por correo electrónico. Además, le recomiendo que configure todo para que el script guarde el archivo de Excel con los resultados de búsqueda en la carpeta de Dropbox, que le permite ver dichos archivos desde cualquier lugar y en cualquier momento.


Todavía no he encontrado tarifas con errores, pero creo que esto es posible

Al buscar, como ya se ha dicho, se utiliza una "fecha flexible", el script encuentra ofertas que se encuentran dentro de los tres días posteriores a las fechas indicadas. Aunque al iniciar el script, busca ofertas en una sola dirección, es fácil refinarlo para que pueda recopilar datos en varias direcciones de vuelos. Con su ayuda, incluso puede buscar tarifas erróneas, tales hallazgos pueden ser muy interesantes.

¿Por qué necesito otro raspador web?


Cuando comencé a raspar la web, para ser honesto, no fue particularmente interesante. Quería hacer más proyectos en el campo del modelado predictivo, el análisis financiero y, posiblemente, en el campo del análisis del color emocional de los textos. Pero resultó que era muy interesante: descubrir cómo crear un programa que recopile datos de sitios web. Al profundizar en este tema, me di cuenta de que el raspado web es el "motor" de Internet.

Puede decidir que esta es una declaración demasiado audaz. Pero piense en cómo Google comenzó con un raspador web que Larry Page creó usando Java y Python. Googlebots ha estado investigando y explorando Internet, tratando de proporcionar a sus usuarios las mejores respuestas posibles a sus preguntas. El raspado web tiene un número infinito de aplicaciones, e incluso si usted, en el campo de la ciencia de datos, está interesado en otra cosa, para obtener datos para el análisis, necesitará algunas habilidades de raspado.

Algunos de los trucos utilizados aquí los encontré en un libro maravilloso sobre el raspado web, que recientemente adquirí. En él puedes encontrar muchos ejemplos e ideas simples sobre la aplicación práctica de lo estudiado. Además, hay un capítulo muy interesante sobre el bypass de prueba reCaptcha. Para mí, esto era una novedad, ya que no sabía que había herramientas especiales e incluso servicios completos para resolver tales problemas.

¿Te gusta viajar?


A la pregunta simple y bastante inofensiva formulada en el encabezado de esta sección, a menudo se puede escuchar una respuesta positiva, con un par de historias de viaje de la persona a quien se le preguntó. La mayoría de nosotros estará de acuerdo en que viajar es una excelente manera de sumergirse en nuevos entornos culturales y expandir nuestros horizontes. Sin embargo, si le preguntas a alguien si le gusta buscar boletos aéreos, estoy seguro de que la respuesta estará lejos de ser tan positiva. De hecho, aquí Python viene al rescate.

La primera tarea que debemos resolver sobre la forma de crear un sistema de búsqueda de información sobre boletos aéreos será la selección de una plataforma adecuada con la cual tomaremos información. La solución a este problema no fue fácil para mí, pero al final elegí el servicio de Kayak. Probé los servicios de Momondo, Skyscanner, Expedia y algunos más, pero los mecanismos de protección contra los robots en estos recursos eran impenetrables. Después de varios intentos, durante los cuales, al tratar de convencer a los sistemas de que era humano, tuve que lidiar con semáforos, pasos de peatones y bicicletas, decidí que Kayak me quedaba bien, aunque aquí también, si carga demasiadas páginas en poco tiempo, también comienzan las verificaciones. Logré hacer que el bot enviara solicitudes al sitio a intervalos de 4 a 6 horas, y todo funcionó bien. Las dificultades también surgen periódicamente cuando se trabaja con Kayak, pero si comienzas a molestarte con los cheques, entonces debes tratarlos manualmente, luego iniciar el bot o esperar unas horas, y los controles deberían detenerse. Si es necesario, puede adaptar el código para otra plataforma, y ​​si lo hace, puede informarlo en los comentarios.

Si recién está comenzando con el raspado web y no sabe por qué algunos sitios web están luchando con él, antes de comenzar su primer proyecto en esta área, hágase un favor y busque palabras en Google "Etiqueta de raspado web". Sus experimentos pueden terminar antes de lo que piensa si está involucrado sin motivo en el raspado de la web.

Empezando


Aquí hay una descripción general de lo que sucederá en el código de nuestro raspador web:

  • Importar bibliotecas requeridas.
  • Abre la pestaña Google Chrome.
  • Llamando a la función que inicia el bot, pasándole la ciudad y la fecha, que se utilizará al buscar boletos.
  • Esta función recibe los primeros resultados de búsqueda, ordenados según los criterios de los más atractivos (los mejores), y presiona el botón para cargar resultados adicionales.
  • Otra función recopila datos de toda la página y devuelve un marco de datos.
  • Los dos pasos anteriores se realizan utilizando tipos de clasificación por precio de billete (barato) y por velocidad de vuelo (más rápido).
  • Se envía un correo electrónico al usuario del script que contiene un breve resumen de los precios de los boletos (los boletos más baratos y el precio promedio), y el marco de datos con la información ordenada por los tres indicadores antes mencionados se guarda como un archivo Excel.
  • Todas las acciones anteriores se realizan en un ciclo después de un período de tiempo especificado.

Cabe señalar que cada proyecto Selenium comienza con un controlador web. Utilizo Chromedriver , trabajo con Google Chrome, pero hay otras opciones. También son populares PhantomJS y Firefox. Después de cargar el controlador, debe colocarlo en la carpeta correspondiente, esto completa la preparación para su uso. Las primeras líneas de nuestro script abren una nueva pestaña de Chrome.

Tenga en cuenta que, en mi historia, no estoy tratando de abrir nuevos horizontes para encontrar ofertas rentables en boletos aéreos. Existen técnicas mucho más avanzadas para encontrar tales ofertas. Solo quiero ofrecer a los lectores de este material una forma simple pero práctica de resolver este problema.

Aquí está el código del que hablamos anteriormente.

from time import sleep, strftime from random import randint import pandas as pd from selenium import webdriver from selenium.webdriver.common.keys import Keys import smtplib from email.mime.multipart import MIMEMultipart #      chromedriver! chromedriver_path = 'C:/{YOUR PATH HERE}/chromedriver_win32/chromedriver.exe' driver = webdriver.Chrome(executable_path=chromedriver_path) #     Chrome sleep(2) 

Al comienzo del código, puede ver los comandos de importación de paquetes que se utilizan en todo nuestro proyecto. Por lo tanto, randint se usa para que el bot se "duerma" durante un número aleatorio de segundos antes de comenzar una nueva operación de búsqueda. Por lo general, ni un solo bot puede prescindir de él. Si ejecuta el código anterior, se abrirá una ventana de Chrome, que el bot utilizará para trabajar con sitios.

Hagamos un pequeño experimento y abra el sitio web kayak.com en una ventana separada. Elija la ciudad desde la que vamos a volar y la ciudad a la que queremos llegar, así como las fechas de los vuelos. Al elegir las fechas, verificaremos que el rango sea de + -3 días. Escribí el código teniendo en cuenta lo que produce el sitio en respuesta a tales solicitudes. Si, por ejemplo, necesita buscar tickets solo para fechas determinadas, es muy probable que tenga que modificar el código bot. Hablando sobre el código, hago explicaciones apropiadas, pero si sientes que estás confundido, házmelo saber.

Ahora haga clic en el botón de inicio de búsqueda y mire el enlace en la barra de direcciones. Debería parecerse al enlace que uso en el ejemplo a continuación, donde se declara la variable de kayak que almacena la URL y se usa el método de get del controlador web. Después de hacer clic en el botón de búsqueda, los resultados deberían aparecer en la página.


Cuando utilicé el comando get más de dos o tres veces en unos minutos, me pidieron que pasara una prueba usando reCaptcha. Puede realizar esta verificación manualmente y continuar los experimentos hasta que el sistema decida organizar una nueva verificación. Cuando probé el script, tuve la sensación de que la primera sesión de búsqueda siempre funciona sin problemas, por lo que si desea experimentar con el código, solo tiene que verificarlo periódicamente y dejar que el código se ejecute utilizando largos intervalos entre las sesiones de búsqueda. Sí, y si lo piensa, es poco probable que una persona necesite información sobre los precios de los boletos recibidos en intervalos de 10 minutos entre operaciones de búsqueda.

Trabajando con una página usando XPath


Entonces, abrimos la ventana y cargamos el sitio. Para obtener precios y otra información, necesitamos usar la tecnología XPath o los selectores CSS. Decidí detenerme en XPath y no sentí la necesidad de usar selectores CSS, pero es muy posible trabajar así. Moverse por una página usando XPath puede ser una tarea desalentadora, e incluso si usa los métodos que describí en este artículo, que copiaron los identificadores correspondientes del código de la página, me di cuenta de que, de hecho, esta no es la mejor manera de acceder elementos necesarios Por cierto, en este libro puede encontrar una excelente descripción de los conceptos básicos del trabajo con páginas que utilizan selectores XPath y CSS. Así es como se ve el método del controlador web correspondiente.


Entonces, continuamos trabajando en el bot. Aproveche el programa para seleccionar los boletos más baratos. En la siguiente imagen, el código del selector XPath se resalta en rojo. Para ver el código, debe hacer clic derecho en el elemento de la página que le interesa y seleccionar el comando Inspeccionar en el menú que aparece. Se puede llamar a este comando para diferentes elementos de página, cuyo código se mostrará y resaltará en la ventana de visualización de código.


Ver código de página

Para encontrar la confirmación de mi razonamiento sobre las desventajas de copiar selectores del código, preste atención a las siguientes características.

Esto es lo que obtienes al copiar código:

 //*[@id="wtKI-price_aTab"]/div[1]/div/div/div[1]/div/span/span 

Para copiar algo similar, debe hacer clic con el botón derecho en la parte del código que le interesa y seleccionar Copiar> Copiar XPath en el menú que aparece.

Esto es lo que solía definir el botón más barato:

 cheap_results = '//a[@data-code = "price"]' 


Copiar> Copiar comando XPath

Es bastante obvio que la segunda opción parece mucho más simple. Cuando lo usa, busca el elemento a, que tiene el atributo de data-code igual al price . Usando la primera opción, se busca un elemento id que es wtKI-price_aTab , y la ruta XPath al elemento se ve como /div[1]/div/div/div[1]/div/span/span . Una solicitud XPath similar a una página hará el truco, pero solo una vez. Puedo decir ahora que la id cambiará la próxima vez que se cargue la página. La wtKI caracteres wtKI cambia dinámicamente cada vez que se carga la página, como resultado, el código en el que se usa será inútil después de la próxima recarga de la página. Así que tómate un tiempo para descubrir XPath. Este conocimiento te servirá bien.

Sin embargo, debe tenerse en cuenta que copiar los selectores XPath puede ser útil cuando se trabaja con sitios bastante simples, y si esto le conviene, no hay nada de malo en eso.

Ahora pensemos qué hacer si necesita obtener todos los resultados de búsqueda en varias líneas, dentro de la lista. Muy simple Cada resultado está dentro de un objeto con la clase resultWrapper . La descarga de todos los resultados se puede realizar en un bucle similar al que se muestra a continuación.

Debe tenerse en cuenta que si comprende lo anterior, debe comprender fácilmente la mayor parte del código que analizaremos. En el curso del trabajo de este código, pasamos a lo que necesitamos (de hecho, este es el elemento en el que se envuelve el resultado) usando algún mecanismo para indicar la ruta (XPath). Esto se hace para obtener el texto del elemento y colocarlo en un objeto desde el que se puedan leer los datos (primero use flight_containers , luego flights_list ).


Se muestran las primeras tres líneas y podemos ver claramente todo lo que necesitamos. Sin embargo, tenemos formas más interesantes de obtener información. Necesitamos tomar datos de cada elemento por separado.

A trabajar!


Es más fácil escribir una función para cargar resultados adicionales, así que comencemos con ella. Me gustaría maximizar la cantidad de vuelos sobre los que el programa recibe información y, al mismo tiempo, no causar sospechas en el servicio que conduzcan a la verificación, por lo que hago clic en el botón Cargar más resultados una vez cada vez que se muestra la página. En este código, debe prestar atención al bloque try , que agregué debido al hecho de que a veces el botón no se carga normalmente. Si también encuentra esto, comente las llamadas a esta función en el código de la función start_kayak , que discutiremos a continuación.

 #      ,      def load_more():   try:       more_results = '//a[@class = "moreButton"]'       driver.find_element_by_xpath(more_results).click()       #            ,          print('sleeping.....')       sleep(randint(45,60))   except:       pass 

Ahora, después de un largo análisis de esta función (a veces puedo dejarme llevar), estamos listos para declarar una función que se ocupará del raspado de página.

Ya he recopilado la mayor parte de lo que se necesita en la próxima función llamada page_scrape . A veces, los datos devueltos sobre las etapas de la ruta se combinan, para su separación utilizo un método simple. Por ejemplo, la primera vez que uso las variables section_a_list y section_b_list . Nuestra función devuelve el marco de datos flights_df , esto nos permite separar los resultados obtenidos utilizando diferentes métodos de clasificación de datos y luego combinarlos.

 def page_scrape():   """This function takes care of the scraping part"""     xp_sections = '//*[@class="section duration"]'   sections = driver.find_elements_by_xpath(xp_sections)   sections_list = [value.text for value in sections]   section_a_list = sections_list[::2] #          section_b_list = sections_list[1::2]     #     reCaptcha,    - .   #  ,  -   ,     ,       #   if        -   #    ,           #    SystemExit           if section_a_list == []:       raise SystemExit     #     A     B     a_duration = []   a_section_names = []   for n in section_a_list:       #         a_section_names.append(''.join(n.split()[2:5]))       a_duration.append(''.join(n.split()[0:2]))   b_duration = []   b_section_names = []   for n in section_b_list:       #         b_section_names.append(''.join(n.split()[2:5]))       b_duration.append(''.join(n.split()[0:2]))   xp_dates = '//div[@class="section date"]'   dates = driver.find_elements_by_xpath(xp_dates)   dates_list = [value.text for value in dates]   a_date_list = dates_list[::2]   b_date_list = dates_list[1::2]   #      a_day = [value.split()[0] for value in a_date_list]   a_weekday = [value.split()[1] for value in a_date_list]   b_day = [value.split()[0] for value in b_date_list]   b_weekday = [value.split()[1] for value in b_date_list]     #     xp_prices = '//a[@class="booking-link"]/span[@class="price option-text"]'   prices = driver.find_elements_by_xpath(xp_prices)   prices_list = [price.text.replace('$','') for price in prices if price.text != '']   prices_list = list(map(int, prices_list))   # stops -   ,         ,   -     xp_stops = '//div[@class="section stops"]/div[1]'   stops = driver.find_elements_by_xpath(xp_stops)   stops_list = [stop.text[0].replace('n','0') for stop in stops]   a_stop_list = stops_list[::2]   b_stop_list = stops_list[1::2]   xp_stops_cities = '//div[@class="section stops"]/div[2]'   stops_cities = driver.find_elements_by_xpath(xp_stops_cities)   stops_cities_list = [stop.text for stop in stops_cities]   a_stop_name_list = stops_cities_list[::2]   b_stop_name_list = stops_cities_list[1::2]     #   -,          xp_schedule = '//div[@class="section times"]'   schedules = driver.find_elements_by_xpath(xp_schedule)   hours_list = []   carrier_list = []   for schedule in schedules:       hours_list.append(schedule.text.split('\n')[0])       carrier_list.append(schedule.text.split('\n')[1])   #          a  b   a_hours = hours_list[::2]   a_carrier = carrier_list[1::2]   b_hours = hours_list[::2]   b_carrier = carrier_list[1::2]     cols = (['Out Day', 'Out Time', 'Out Weekday', 'Out Airline', 'Out Cities', 'Out Duration', 'Out Stops', 'Out Stop Cities',           'Return Day', 'Return Time', 'Return Weekday', 'Return Airline', 'Return Cities', 'Return Duration', 'Return Stops', 'Return Stop Cities',           'Price'])   flights_df = pd.DataFrame({'Out Day': a_day,                              'Out Weekday': a_weekday,                              'Out Duration': a_duration,                              'Out Cities': a_section_names,                              'Return Day': b_day,                              'Return Weekday': b_weekday,                              'Return Duration': b_duration,                              'Return Cities': b_section_names,                              'Out Stops': a_stop_list,                              'Out Stop Cities': a_stop_name_list,                              'Return Stops': b_stop_list,                              'Return Stop Cities': b_stop_name_list,                              'Out Time': a_hours,                              'Out Airline': a_carrier,                              'Return Time': b_hours,                              'Return Airline': b_carrier,                                                     'Price': prices_list})[cols]     flights_df['timestamp'] = strftime("%Y%m%d-%H%M") #      return flights_df 

Traté de nombrar las variables para que el código fuera claro. Recuerde que las variables que comienzan con a refieren al primer paso de la ruta b al segundo. Ir a la siguiente función.

Mecanismos auxiliares


Ahora tenemos una función que le permite cargar resultados de búsqueda adicionales y una función para procesar estos resultados. Este artículo podría completarse sobre esto, ya que estas dos funciones proporcionan todo lo necesario para raspar páginas que se pueden abrir de forma independiente. Pero todavía no hemos considerado algunos de los mecanismos auxiliares discutidos anteriormente. Por ejemplo, este es un código para enviar correos electrónicos y algunas otras cosas. Todo esto se puede encontrar en la función start_kayak , que ahora consideramos.

Para usar esta función, necesita información sobre ciudades y fechas. Con esta información, forma un enlace en la variable kayak , que se utiliza para ir a una página que contendrá los resultados de búsqueda ordenados por su mejor coincidencia. Después de la primera sesión de scraping, trabajaremos con los precios en la tabla en la parte superior de la página. A saber, encontramos el precio mínimo del boleto y el precio promedio. Todo esto, junto con la predicción emitida por el sitio, se enviará por correo electrónico. En la página, la tabla correspondiente debe estar en la esquina superior izquierda. Trabajar con esta tabla, por cierto, puede causar un error al buscar usando fechas exactas, ya que en este caso la tabla no se muestra en la página.

 def start_kayak(city_from, city_to, date_start, date_end):   """City codes - it's the IATA codes!   Date format -  YYYY-MM-DD"""     kayak = ('https://www.kayak.com/flights/' + city_from + '-' + city_to +            '/' + date_start + '-flexible/' + date_end + '-flexible?sort=bestflight_a')   driver.get(kayak)   sleep(randint(8,10))     #    ,           try   try:       xp_popup_close = '//button[contains(@id,"dialog-close") and contains(@class,"Button-No-Standard-Style close ")]'       driver.find_elements_by_xpath(xp_popup_close)[5].click()   except Exception as e:       pass   sleep(randint(60,95))   print('loading more.....')  #     load_more()     print('starting first scrape.....')   df_flights_best = page_scrape()   df_flights_best['sort'] = 'best'   sleep(randint(60,80))     #      ,        matrix = driver.find_elements_by_xpath('//*[contains(@id,"FlexMatrixCell")]')   matrix_prices = [price.text.replace('$','') for price in matrix]   matrix_prices = list(map(int, matrix_prices))   matrix_min = min(matrix_prices)   matrix_avg = sum(matrix_prices)/len(matrix_prices)     print('switching to cheapest results.....')   cheap_results = '//a[@data-code = "price"]'   driver.find_element_by_xpath(cheap_results).click()   sleep(randint(60,90))   print('loading more.....')  #     load_more()     print('starting second scrape.....')   df_flights_cheap = page_scrape()   df_flights_cheap['sort'] = 'cheap'   sleep(randint(60,80))     print('switching to quickest results.....')   quick_results = '//a[@data-code = "duration"]'   driver.find_element_by_xpath(quick_results).click()    sleep(randint(60,90))   print('loading more.....')  #     load_more()     print('starting third scrape.....')   df_flights_fast = page_scrape()   df_flights_fast['sort'] = 'fast'   sleep(randint(60,80))     #     Excel-,         final_df = df_flights_cheap.append(df_flights_best).append(df_flights_fast)   final_df.to_excel('search_backups//{}_flights_{}-{}_from_{}_to_{}.xlsx'.format(strftime("%Y%m%d-%H%M"),                                                                                  city_from, city_to,                                                                                  date_start, date_end), index=False)   print('saved df.....')     #    ,  ,  ,      xp_loading = '//div[contains(@id,"advice")]'   loading = driver.find_element_by_xpath(xp_loading).text   xp_prediction = '//span[@class="info-text"]'   prediction = driver.find_element_by_xpath(xp_prediction).text   print(loading+'\n'+prediction)     #    loading   , , ,        #    -    "Not Sure"   weird = '¯\\_(ツ)_/¯'   if loading == weird:       loading = 'Not sure'     username = 'YOUREMAIL@hotmail.com'   password = 'YOUR PASSWORD'   server = smtplib.SMTP('smtp.outlook.com', 587)   server.ehlo()   server.starttls()   server.login(username, password)   msg = ('Subject: Flight Scraper\n\n\ Cheapest Flight: {}\nAverage Price: {}\n\nRecommendation: {}\n\nEnd of message'.format(matrix_min, matrix_avg, (loading+'\n'+prediction)))   message = MIMEMultipart()   message['From'] = 'YOUREMAIL@hotmail.com'   message['to'] = 'YOUROTHEREMAIL@domain.com'   server.sendmail('YOUREMAIL@hotmail.com', 'YOUROTHEREMAIL@domain.com', msg)   print('sent email.....') 

Probé este script usando una cuenta de Outlook (hotmail.com). No verifiqué el funcionamiento correcto de la cuenta de Gmail, este sistema de correo es muy popular, pero hay muchas opciones posibles. Si usa una cuenta de Hotmail, para que todo funcione, solo necesita ingresar sus datos en el código.

Si desea comprender qué se realiza exactamente en secciones separadas del código de esta función, puede copiarlas y experimentar con ellas. Los experimentos de código son la única forma de entenderlo.

Sistema listo


Ahora que todo lo que hablamos está hecho, podemos crear un ciclo simple en el que se llaman nuestras funciones. El script solicita al usuario datos sobre ciudades y fechas. Al probar con un reinicio constante del script, es poco probable que desee ingresar estos datos manualmente cada vez, por lo que las líneas correspondientes, durante la duración de la prueba, se pueden comentar sin comentar aquellas debajo de ellas en las que los datos necesarios para el script están codificados.

 city_from = input('From which city? ') city_to = input('Where to? ') date_start = input('Search around which departure date? Please use YYYY-MM-DD format only ') date_end = input('Return when? Please use YYYY-MM-DD format only ') # city_from = 'LIS' # city_to = 'SIN' # date_start = '2019-08-21' # date_end = '2019-09-07' for n in range(0,5):   start_kayak(city_from, city_to, date_start, date_end)   print('iteration {} was complete @ {}'.format(n, strftime("%Y%m%d-%H%M")))     #  4    sleep(60*60*4)   print('sleep finished.....') 

Aquí está la ejecución de prueba del script.

Script de ejecución de prueba

Resumen


Si llegas a este punto, ¡felicidades! Ahora tiene un raspador web que funciona, aunque ya veo muchas formas de mejorarlo. Por ejemplo, se puede integrar con Twilio para que, en lugar de correos electrónicos, envíe mensajes de texto. Puede usar una VPN o algo más para recibir simultáneamente resultados de múltiples servidores. También hay un problema recurrente al verificar si el usuario del sitio es una persona, pero este problema también se puede resolver. En cualquier caso, ahora tiene una base que puede expandir si lo desea. Por ejemplo, para asegurarse de que el archivo de Excel se envíe al usuario como un archivo adjunto a un correo electrónico.

Source: https://habr.com/ru/post/451872/


All Articles