Web Scraping avec Selenium et Python
C'est mon tout premier code de programme et cela fonctionne réellement. Mon objectif est de récupérer des informations sur le site Web et de les stocker dans une base de données. C'est un site qui contient des données historiques sur les événements sportifs et les cotes.
https://www.oddsportal.com/hockey/sweden/shl-2019-2020/results/
Par exemple, il y a 50 correspondances sur cette page et le programme entre chacune d'elles et gratte ces données.
Cependant, je ne pense pas que cela ressemble vraiment au vrai code. Je suis intéressé par ce que je peux faire pour l'améliorer et l'optimiser. À quoi est-ce que je prête le plus d'attention, quelles choses est-ce que je fais de mal?
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
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.common.exceptions import NoSuchElementException, TimeoutException
from selenium.webdriver.common.keys import Keys
import sqlite3 as sql
import time
# I disable image display to get up to speed
options = webdriver.ChromeOptions()
prefs = {"profile.managed_default_content_settings.images": 2}
options.add_experimental_option("prefs", prefs)
driver = webdriver.Chrome(options=options)
# Database connection
conn = sql.connect('Hockey_data.db')
c = conn.cursor()
driver.get("https://www.oddsportal.com/hockey/sweden/shl-2019-2020/results/")
driver.maximize_window()
cur_win = driver.current_window_handle
list_of_links = driver.find_elements_by_xpath("//td[2]/a")[0:-2] # because 2 more elements have the same xpath and they are always at the end
for index, link in enumerate(list_of_links):
link.send_keys(Keys.CONTROL + Keys.RETURN)
driver.switch_to.window([win for win in driver.window_handles if win !=cur_win][0])
# Basic game info
league = WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.XPATH, "/html/body/div[1]/div/div[2]/div[6]/div[1]/div/div[1]/div[1]/a[4]"))).text
game = WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.XPATH, "/html/body/div[1]/div/div[2]/div[6]/div[1]/div/div[1]/div[2]/div[1]/h1"))).text
game_split = game.split('- ')
home_team, away_team = game_split[0], game_split[1]
time = WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.XPATH, "/html/body/div[1]/div/div[2]/div[6]/div[1]/div/div[1]/div[2]/div[1]/p[1]"))).text
time_split = time.split(', ')
day_in_week = time_split[0]
date = time_split[1]
day = int(date[0:2])
month = date[3:7]
year = int(date[7:11])
hour = time_split[2]
# Result and goals by periods
try:
result = WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "event-status"))).text
except NoSuchElementException:
result = "0000000000000000000000000000000000000000000000000000000000000000000000"
if len(result) == 32:
home_goals = int(result[13:14])
home_first = int(result[18:19])
home_second = int(result[23:24])
home_third = int(result[28:29])
home_overtime = 0
home_penalties = 0
away_goals = int(result[15:16])
away_first = int(result[20:21])
away_second = int(result[25:26])
away_third = int(result[30:31])
away_overtime = 0
away_penalties = 0
elif len(result) == 33:
home_goals = int(result[13:15])
home_first = int(result[19:20])
home_second = int(result[24:25])
home_third = int(result[29:30])
home_overtime = 0
home_penalties = 0
away_goals = int(result[16:17])
away_first = int(result[21:22])
away_second = int(result[26:27])
away_third = int(result[31:32])
away_overtime = 0
elif len(result) == 40:
home_goals = int(result[13:14])
home_first = int(result[21:22])
home_second = int(result[26:27])
home_third = int(result[31:32])
home_overtime = int(result[36:37])
home_penalties = 0
away_goals = int(result[15:16])
away_first = int(result[23:24])
away_second = int(result[28:29])
away_third = int(result[33:34])
away_overtime = int(result[38:39])
away_penalties = 0
elif len(result) == 52:
home_goals = int(result[13:14])
home_first = int(result[28:29])
home_second = int(result[33:34])
home_third = int(result[38:39])
home_overtime = int(result[43:44])
home_penalties = int(result[48:49])
away_goals = int(result[15:16])
away_first = int(result[30:31])
away_second = int(result[35:36])
away_third = int(result[40:41])
away_overtime = int(result[45:46])
away_penalties = int(result[50:51])
else:
home_goals = 0
home_first = 0
home_second = 0
home_third = 0
home_overtime = 0
home_penalties = 0
away_goals = 0
away_first = 0
away_second = 0
away_third = 0
away_overtime = 0
away_penalties = 0
# 1x2 odds
pinnacle = "Pinnacle"
try:
pinnacle_home = driver.find_element_by_xpath("//div[a[contains(text(), 'Pinnacle')]]/following::td[1]")
hov_pinnacle_home = ActionChains(driver).move_to_element(pinnacle_home)
hov_pinnacle_home.perform()
pinnacle_home_closing = driver.find_element_by_xpath("//*[@id='tooltiptext']/strong[1]").text
try:
pinnacle_home_opening = driver.find_element_by_xpath("//*[@id='tooltiptext']/strong[2]").text
except (NoSuchElementException, TimeoutException):
pinnacle_home_opening = pinnacle_home_closing
except (NoSuchElementException, TimeoutException):
pinnacle_home = "0000000000"
pinnacle_home_closing = 0.00
pinnacle_home_opening = 0.00
try:
pinnacle_draw = driver.find_element_by_xpath("//div[a[contains(text(), 'Pinnacle')]]/following::td[2]")
hov_pinnacle_draw = ActionChains(driver).move_to_element(pinnacle_draw)
hov_pinnacle_draw.perform()
pinnacle_draw_closing = driver.find_element_by_xpath("//*[@id='tooltiptext']/strong[1]").text
try:
pinnacle_draw_opening = driver.find_element_by_xpath("//*[@id='tooltiptext']/strong[2]").text
except (NoSuchElementException, TimeoutException):
pinnacle_draw_opening = pinnacle_draw_closing
except (NoSuchElementException, TimeoutException):
pinnacle_draw = "0000000000"
pinnacle_draw_closing = 0.00
pinnacle_draw_opening = 0.00
try:
pinnacle_away = driver.find_element_by_xpath("//div[a[contains(text(), 'Pinnacle')]]/following::td[3]")
hov_pinnacle_away = ActionChains(driver).move_to_element(pinnacle_away)
hov_pinnacle_away.perform()
pinnacle_away_closing = driver.find_element_by_xpath("//*[@id='tooltiptext']/strong[1]").text
try:
pinnacle_away_opening = driver.find_element_by_xpath("//*[@id='tooltiptext']/strong[2]").text
except (NoSuchElementException, TimeoutException):
pinnacle_away_opening = pinnacle_away_closing
except (NoSuchElementException, TimeoutException):
pinnacle_away = "0000000000"
pinnacle_away_closing = 0.00
pinnacle_away_opening = 0.00
average = "Average"
try:
average_home = driver.find_element_by_xpath("//td[strong[contains(text(), 'Average')]]/following::td[1]").text
except NoSuchElementException:
average_home = 0.00
try:
average_draw = driver.find_element_by_xpath("//td[strong[contains(text(), 'Average')]]/following::td[2]").text
except NoSuchElementException:
average_draw = 0.00
try:
average_away = driver.find_element_by_xpath("//td[strong[contains(text(), 'Average')]]/following::td[3]").text
except NoSuchElementException:
average_away = 0.00
highest = "Highest"
try:
highest_home = driver.find_element_by_xpath("//td[strong[contains(text(), 'Highest')]]/following::td[1]").text
except NoSuchElementException:
highest_home = 0.00
try:
highest_draw = driver.find_element_by_xpath("//td[strong[contains(text(), 'Highest')]]/following::td[2]").text
except NoSuchElementException:
highest_draw = 0.00
try:
highest_away = driver.find_element_by_xpath("//td[strong[contains(text(), 'Highest')]]/following::td[3]").text
except NoSuchElementException:
highest_away = 0.00
# Over-Under tab
OU_link = WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.XPATH, "/html/body/div[1]/div/div[2]/div[6]/div[1]/div/div[1]/div[2]/div[1]/div[5]/div[1]/ul/li[5]/a/span"))).click()
pinnacle_over_under = "Pinnacle 5.5"
# 5.5 over-under tab
try:
OU_55 = WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.XPATH, "//div/div/strong/a[contains(text(), 'Over/Under +5.5')]"))).click()
try:
pinnacle_over = driver.find_element_by_xpath("//div[a[contains(text(), 'Pinnacle')]]/following::td[2]")
hov_pinnacle_over = ActionChains(driver).move_to_element(pinnacle_over)
hov_pinnacle_over.perform()
pinnacle_over_closing = driver.find_element_by_xpath("//*[@id='tooltiptext']/strong[1]").text
try:
pinnacle_over_opening = driver.find_element_by_xpath("//*[@id='tooltiptext']/strong[2]").text
except (NoSuchElementException, TimeoutException):
pinnacle_over_opening = pinnacle_over_closing
except (NoSuchElementException, TimeoutException):
pinnacle_over = "0000000000"
pinnacle_over_opening = 0.00
pinnacle_over_closing = 0.00
try:
pinnacle_under = driver.find_element_by_xpath("//div[a[contains(text(), 'Pinnacle')]]/following::td[3]")
hov_pinnacle_under = ActionChains(driver).move_to_element(pinnacle_under)
hov_pinnacle_under.perform()
pinnacle_under_closing = driver.find_element_by_xpath("//*[@id='tooltiptext']/strong[1]").text
try:
pinnacle_under_opening = driver.find_element_by_xpath("//*[@id='tooltiptext']/strong[2]").text
except (NoSuchElementException, TimeoutException):
pinnacle_under_opening = pinnacle_over_closing
except (NoSuchElementException, TimeoutException):
pinnacle_under = "0000000000"
pinnacle_under_opening = 0.00
pinnacle_under_closing = 0.00
except (NoSuchElementException, TimeoutException):
OU_55 = "0000000000"
pinnacle_under_opening = 0.00
pinnacle_under_closing = 0.00
average_over_under = "Average 5.5"
try:
average_over = driver.find_element_by_xpath("//td[strong[contains(text(), 'Average')]]/following::td[2]").text
except NoSuchElementException:
average_over = 0.00
try:
average_under = driver.find_element_by_xpath("//td[strong[contains(text(), 'Average')]]/following::td[3]").text
except NoSuchElementException:
average_under = 0.00
highest_over_under = "Highest 5.5"
try:
highest_over = driver.find_element_by_xpath("//td[strong[contains(text(), 'Highest')]]/following::td[2]").text
except NoSuchElementException:
highest_over = 0.00
try:
highest_under = driver.find_element_by_xpath("//td[strong[contains(text(), 'Highest')]]/following::td[3]").text
except NoSuchElementException:
highest_under = 0.00
c.execute('insert into Model values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
(league, home_team, away_team, day_in_week, day, month, year, hour,
home_goals, away_goals, home_first, away_first, home_second,away_second, home_third, away_third,home_overtime, away_overtime, home_penalties, away_penalties,
pinnacle, pinnacle_home_closing, pinnacle_draw_closing, pinnacle_away_closing, pinnacle_home_opening, pinnacle_draw_opening, pinnacle_away_opening,
average, average_home, average_draw, average_away, highest, highest_home, highest_draw, highest_away,
pinnacle_over_under, pinnacle_over_closing, pinnacle_under_closing, pinnacle_over_opening, pinnacle_under_opening,
average_over_under, average_over, average_under, highest_over_under, highest_over, highest_under,))
conn.commit()
print("Number: ", index)
driver.close()
driver.switch_to.window(cur_win)
Réponses
Répétition 1
Vous avez plusieurs XPATH qui sont en grande partie les mêmes. Surtout quand ils partagent un préfixe commun, comme ceux-ci
"/html/body/div[1]/div/div[2]/div[6]/div[1]/div/div[1]/div[1]/a[4]"
Je déclarerais une variable
xpath_base = "/html/body/div[1]/div/div[2]/div[6]/div[1]/div/div[1]"
puis réutilisez cela pour chaque xpath qui a la même base. Cela rend le code plus lisible et plus facile à modifier en cas de changement de base.
Votre premier xpath devient alors xpath_base + "/div[1]/a[4]", votre second devient xpath_base + "/div[2]/div[1]/h1"et ainsi de suite.
Reptition 2 / Cartographie
Je préférerais utiliser une structure plus compacte pour la lecture des objectifs. Je ne sais pas si vous le trouvez plus lisible, mais il est plus court.
Vous pouvez placer vos index dans une combinaison de dict et de tableaux comme celui-ci.
Dans ce cas, je n'utilise que les index pour la maison et l'extérieur goals, first, second, thirdcar ils sont utilisés avec des index dans les 4 cas.
result_mapping = {
32: [13, 14, 18, 19, 23, 24, 28, 29, 15, 16, 20, 21, 25, 26, 30, 31],
33: [13, 15, 19, 20, 24, 25, 29, 39, 16, 17, 21, 22, 26, 27, 31, 32],
40: [...],
52: [...]
}
Avec cette cartographie, vous pouvez alors faire
if len(result) in result_mapping.keys():
indexes = result_mapping[len(result)]
home_goals = int(result[indexes[0] : indexes[1]])
home_first = int(result[indexes[2] : indexes[3]])
home_second = int(result[indexes[4] : indexes[5]])
etc. Il y a plus de place pour raccourcir ce code, comme pour créer une fonction int(result[])puisque cela est répété sur chaque ligne.
De plus, les index sont toujours net n+1sauf pour home_goals = int(result[13:15])(est-ce une erreur d'indexation?). Si le 13:15est une erreur et est censé l'être, 13:14vous pouvez supprimer tous les index de fin des tableaux ci-dessus, donc
32: [13, 14, 18, 19, 23, 24, 28, 29, 15, 16, 20, 21, 25, 26, 30, 31]
devient
32: [13, 18, 23, 28, 15, 20, 25, 30]
et vous ajoutez simplement à la +1place de l'index de fin, dans chaque cas.
Répétition 3 / Aliasing
Les choses que vous répétez beaucoup, je préférerais faire un alias plus court. Vous pouvez définir quelque chose comme au xpath = driver.find_element_by_xpathdébut du code, puis utiliser le nouveau nom pour appeler cette fonction.
Répétition 4 / Cas par défaut
Dans la longue pièce if-elif-else, vous avez votre cas par défaut en dernier après le else.
Mais certains des cas ici sont partagés avec plusieurs des cas du if-else. Pour cette raison, il serait plus judicieux de définir d'abord vos valeurs par défaut, avant le if-else. Comme ils sont tous nuls, vous pouvez également les raccourcir un peu comme ça.
home_goals = home_first = home_second = home_third = home_overtime = home_penalties = 0
Maintenant, en faisant cela avant le if-else, vous n'avez pas besoin de mentionner à nouveau ces variables dans les cas où elles devraient toujours être 0telles que home_penaltiesdans 3 cas sur 4.
Commentaire final
Je ne pense pas que ça ressemble vraiment au vrai code
Je pense qu'une des raisons pour lesquelles il ne ressemble pas à du "vrai code" est qu'il a tellement d'essais - à l'exception des cas spéciaux et ainsi de suite. Mais c'est difficile à éviter lors de l'écriture d'un grattoir pour un site Web particulier. Vous devez écrire un code personnalisé pour la structure de ce site Web, et vous ne pouvez pas faire beaucoup de refactorisations / raccourcis et de réutiliser des choses que vous pouvez normalement lors de l'écriture d'un programme normal. Je pense donc que la plupart du code semble bien pour ce cas d'utilisation.
Encore un ajout après la publication
Vous avez beaucoup de cas suivant le même schéma, comme celui-ci
try:
average_under = driver.find_element_by_xpath("//td[strong[contains(text(), 'Average')]]/following::td[3]").text
except NoSuchElementException:
average_under = 0.00
Vous pouvez refactoriser ceci et le rendre beaucoup moins répétitif en créant une fonction.
average_under = try_or_default("//td[strong[contains(text(), 'Average')]]/following::td[3]", 0.00)
et votre fonction sera
def try_or_default(xpath, default_value):
try:
return driver.find_element_by_xpath(xpath).text
except NoSuchElementException:
return default_value
(Je n'ai pas essayé de l'exécuter, il peut donc y avoir une erreur de syntaxe).
Vous pouvez le raccourcir encore plus en définissant une valeur par défaut pour default_value
def try_or_default(xpath, default_value=0.00):
Ce qui vous permet de laisser de côté 0.00lors de l'appel de la fonction dans la plupart des cas.