Web Scraping sử dụng Selenium và Python

Sep 12 2020

Đây là mã chương trình đầu tiên của tôi và nó thực sự hoạt động. Mục tiêu của tôi là thu thập thông tin từ trang web và lưu trữ trong cơ sở dữ liệu. Đây là một trang web có dữ liệu lịch sử về các sự kiện thể thao và tỷ lệ cược.

https://www.oddsportal.com/hockey/sweden/shl-2019-2020/results/

Ví dụ: có 50 kết quả phù hợp trên trang này và chương trình sẽ nhập từng kết quả và loại bỏ dữ liệu này.

Tuy nhiên, tôi không nghĩ nó thực sự giống mã thật. Tôi quan tâm đến những gì tôi có thể làm để cải thiện và tối ưu hóa nó. Điều gì tôi chú ý nhất, những điều tôi làm sai?

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)

Trả lời

3 user985366 Sep 12 2020 at 23:41

Lặp lại 1

Bạn có một số XPATH phần lớn giống nhau. Đặc biệt là khi chúng chia sẻ một tiền tố chung, như

"/html/body/div[1]/div/div[2]/div[6]/div[1]/div/div[1]/div[1]/a[4]"

Tôi sẽ khai báo một biến

xpath_base = "/html/body/div[1]/div/div[2]/div[6]/div[1]/div/div[1]"

và sau đó sử dụng lại nó cho mọi xpath có cùng cơ sở. Điều này làm cho mã dễ đọc hơn và dễ sửa đổi hơn trong trường hợp cơ sở thay đổi.

Sau đó xpath đầu tiên của bạn trở thành xpath_base + "/div[1]/a[4]", thứ hai của bạn sẽ trở thành xpath_base + "/div[2]/div[1]/h1", v.v.

Bản sao 2 / Ánh xạ

Tôi muốn sử dụng một số cấu trúc nhỏ gọn hơn cho phần đọc mục tiêu. Không chắc bạn có thấy nó dễ đọc hơn không, nhưng nó ngắn hơn.

Bạn có thể đặt các chỉ mục của mình trong sự kết hợp của dict và mảng như thế này.

Trong trường hợp này, tôi chỉ sử dụng các chỉ mục cho nhà và sân khách goals, first, second, thirdvì chúng được sử dụng với các chỉ mục trong cả 4 trường hợp.

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: [...]
}

Với ánh xạ này, bạn có thể làm

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

và như thế. Có nhiều chỗ hơn để làm cho mã này ngắn hơn, giống như tạo một hàm int(result[])vì nó được lặp lại trên mỗi dòng.

Ngoài ra, các chỉ mục luôn luôn nvà n+1ngoại trừ home_goals = int(result[13:15])(đó có phải là một lỗi trong việc lập chỉ mục?). Nếu đó 13:15là một lỗi và phải là lỗi 13:14thì bạn có thể xóa tất cả các chỉ mục cuối trong các mảng ở trên, vì vậy

32: [13, 14, 18, 19, 23, 24, 28, 29, 15, 16, 20, 21, 25, 26, 30, 31]

trở thành

32: [13, 18, 23, 28, 15, 20, 25, 30]

và bạn chỉ thêm +1thay vì chỉ mục kết thúc, trong mỗi trường hợp.

Lặp lại 3 / Bí danh

Những điều mà bạn lặp lại nhiều, tôi muốn đặt một bí danh ngắn hơn cho. Bạn có thể xác định một cái gì đó giống như xpath = driver.find_element_by_xpathban đầu trong mã và sau đó sử dụng tên mới để gọi hàm đó.

Lặp lại 4 / Các trường hợp mặc định

Trong đoạn if-elif-else dài, bạn có trường hợp mặc định cuối cùng sau else.

Nhưng một số trường hợp ở đây được chia sẻ với một số trường hợp trong if-else. Vì lý do đó, sẽ có ý nghĩa hơn nếu xác định các mặc định của bạn trước, trước if-else. Vì chúng đều bằng 0, bạn cũng có thể làm cho chúng ngắn hơn một chút như thế này.

home_goals = home_first = home_second = home_third = home_overtime = home_penalties = 0

Bây giờ bằng cách thực hiện điều này trước if-else, bạn không cần phải đề cập đến các biến này một lần nữa trong các trường hợp mà chúng vẫn nên 0như vậy home_penaltiestrong 3 trong số 4 trường hợp.

Nhận xét cuối cùng

Tôi không nghĩ nó thực sự giống mã thật

Tôi nghĩ một lý do khiến nó không giống "mã thực" là nó có quá nhiều lần thử - ngoại trừ và các trường hợp đặc biệt, v.v. Nhưng điều đó khó có thể tránh khỏi khi viết một bản nháp cho một trang web cụ thể. Bạn cần viết mã tùy chỉnh cho cấu trúc của trang web đó và bạn không thể thực hiện nhiều thao tác tái cấu trúc / phím tắt và sử dụng lại những thứ mà bạn thường có thể làm khi viết một chương trình bình thường. Vì vậy, tôi nghĩ rằng hầu hết các mã có vẻ ổn cho trường hợp sử dụng này.

Thêm một bổ sung sau khi đăng

Bạn có rất nhiều trường hợp theo cùng một mẫu, như thế này

try:
    average_under = driver.find_element_by_xpath("//td[strong[contains(text(), 'Average')]]/following::td[3]").text
except NoSuchElementException:
        average_under = 0.00

Bạn có thể cấu trúc lại điều này và làm cho nó ít lặp lại hơn nhiều bằng cách tạo một hàm.

average_under = try_or_default("//td[strong[contains(text(), 'Average')]]/following::td[3]", 0.00)

và chức năng của bạn sẽ là

def try_or_default(xpath, default_value):
    try:
        return driver.find_element_by_xpath(xpath).text
    except NoSuchElementException:
        return default_value 

(Tôi chưa thử chạy nó nên có thể có một số lỗi cú pháp).

Bạn có thể làm cho nó ngắn hơn nữa bằng cách đặt giá trị mặc định cho default_value

def try_or_default(xpath, default_value=0.00):

Điều này cho phép bạn bỏ qua 0.00khi gọi hàm trong hầu hết các trường hợp.