Can Python Solve Sudoku in Seconds? Automate with Selenium & Backtracking
Learn how to automate a web-based Sudoku game using Python's Selenium for data extraction, implement an efficient backtracking and bitwise algorithm to solve the puzzle, and programmatically fill the solution back into the browser, achieving performance improvements from 17 seconds to just over 3 seconds.
This article demonstrates how to use Python and Selenium to automatically solve a Sudoku puzzle hosted on a web page, extract the board data, compute the solution with an optimized backtracking algorithm, and write the solved numbers back into the browser.
Sudoku Rules
Each digit 1‑9 must appear exactly once in every row.
Each digit 1‑9 must appear exactly once in every column.
Each digit 1‑9 must appear exactly once in each 3×3 sub‑grid.
Accessing the Target Site with Selenium
First install Selenium and then open a Chrome browser instance:
from selenium import webdriver
browser = webdriver.Chrome()Navigate to the Sudoku page:
url = "https://www.sudoku.name/index.php?ln=cn&puzzle_num=&play=1&difficult=4&timer=&time_limit=0"
browser.get(url)Extracting Sudoku Data
The puzzle is rendered inside a table element with id sudoku_main_board. Each cell is an input whose value attribute holds the digit or is empty.
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
wait = WebDriverWait(browser, 10)
table = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, 'table#sudoku_main_board')))
board = []
for tr in table.find_elements_by_xpath('.//tr'):
row = []
for input_e in tr.find_elements_by_xpath('.//input[@class="i3"]'):
cell = input_e.get_attribute('value')
row.append(cell if cell else '.')
board.append(row)
boardThe extracted board is a list of lists where empty cells are represented by ..
Solving Sudoku with Backtracking and Bitwise Optimization
The initial solver uses classic backtracking with bitwise masks to track used digits in rows, columns, and blocks:
def solveSudoku(board):
def flip(i, j, digit):
line[i] ^= (1 << digit)
column[j] ^= (1 << digit)
block[i // 3][j // 3] ^= (1 << digit)
def dfs(pos):
nonlocal valid
if pos == len(spaces):
valid = True
return
i, j = spaces[pos]
mask = ~(line[i] | column[j] | block[i // 3][j // 3]) & 0x1ff
while mask:
digitMask = mask & (-mask)
digit = bin(digitMask).count('0') - 1
flip(i, j, digit)
board[i][j] = str(digit + 1)
dfs(pos + 1)
flip(i, j, digit)
mask &= (mask - 1)
if valid:
return
line = [0] * 9
column = [0] * 9
block = [[0] * 3 for _ in range(3)]
valid = False
spaces = []
for i in range(9):
for j in range(9):
if board[i][j] == '.':
spaces.append((i, j))
else:
digit = int(board[i][j]) - 1
flip(i, j, digit)
dfs(0)Running this version on the sample board took about 17 seconds.
Optimization idea: if a blank cell has only one possible digit (its mask contains a single bit), fill it immediately instead of waiting for recursion.
Optimized Solver
The refined implementation adds a deterministic filling pass before recursion, reducing runtime to roughly 3.2 seconds:
def solveSudoku(board: list) -> None:
def flip(i: int, j: int, digit: int):
line[i] ^= (1 << digit)
column[j] ^= (1 << digit)
block[i // 3][j // 3] ^= (1 << digit)
def dfs(pos: int):
nonlocal valid
if pos == len(spaces):
valid = True
return
i, j = spaces[pos]
mask = ~(line[i] | column[j] | block[i // 3][j // 3]) & 0x1ff
while mask:
digitMask = mask & (-mask)
digit = bin(digitMask).count('0') - 1
flip(i, j, digit)
board[i][j] = str(digit + 1)
dfs(pos + 1)
flip(i, j, digit)
mask &= (mask - 1)
if valid:
return
line = [0] * 9
column = [0] * 9
block = [[0] * 3 for _ in range(3)]
valid = False
spaces = []
for i in range(9):
for j in range(9):
if board[i][j] != '.':
digit = int(board[i][j]) - 1
flip(i, j, digit)
while True:
modified = False
for i in range(9):
for j in range(9):
if board[i][j] == '.':
mask = ~(line[i] | column[j] | block[i // 3][j // 3]) & 0x1ff
if not (mask & (mask - 1)):
digit = bin(mask).count('0') - 1
flip(i, j, digit)
board[i][j] = str(digit + 1)
modified = True
if not modified:
break
for i in range(9):
for j in range(9):
if board[i][j] == '.':
spaces.append((i, j))
dfs(0)Writing the Solution Back to the Web Page
After solving, the script iterates over the same table cells and uses Selenium to click, clear, and send the solved digit:
for i, tr in enumerate(table.find_elements_by_xpath('.//tr')):
for j, input_e in enumerate(tr.find_elements_by_xpath('.//input[@class="i3"]')):
if input_e.get_attribute('readonly') == 'true':
continue
input_e.click()
input_e.clear()
input_e.send_keys(board[i][j])The automation fills the entire puzzle in a few seconds, turning a manual 14‑minute effort into a 10‑second completion.
Python Crawling & Data Mining
Life's short, I code in Python. This channel shares Python web crawling, data mining, analysis, processing, visualization, automated testing, DevOps, big data, AI, cloud computing, machine learning tools, resources, news, technical articles, tutorial videos and learning materials. Join us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
