Hi,
This python script that I’ve made web scrapes data and produces an excel file. However it seems to have a repeating set of data which I don’t want. Here is the code in question, more specifically, the async def generate_clan_war_stats function.
aftermath_militia_scraper.py
import http.client
import urllib
import asyncio
import aiohttp
import re
import httpx
import logging
from bs4 import BeautifulSoup
from openpyxl import Workbook
import openpyxl
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from datetime import datetime
import pandas as pd
from player_war_scraper import scrape_clan_wars_info # Import the scrape_clan_wars_info function from player_war_scraper.py
from selenium.webdriver.remote.remote_connection import LOGGER
import tqdm
# Set the logging level to suppress messages
logging.getLogger('urllib3').setLevel(logging.WARNING)
LOGGER.setLevel(logging.WARNING)
# List of clan URLs with corresponding names
clan_urls = [
# ('https://royaleapi.com/clan/8CYR2V', 'Aftermath'),
# ('https://royaleapi.com/clan/L2RRRGVG', 'Afterlife'),
# ('https://royaleapi.com/clan/YC8R0RJ0', 'Aftershock'),
# ('https://royaleapi.com/clan/LUYLLQJR', 'Afterglow'),
# ('https://royaleapi.com/clan/Q0JRGC22', 'Afterparty'),
# ('https://royaleapi.com/clan/QY920UQR', 'Afterburn'),
('https://royaleapi.com/clan/G22P0PUG', 'Aftertaste')
]
# Maximum number of player tags to process
MAX_PLAYER_TAGS = 1 # Set this to any number up to 50
async def fetch_clan_data(url, clan_name):
try:
async with httpx.AsyncClient() as client:
response = await client.get(url)
response.raise_for_status()
return clan_name, response.text
except Exception as e:
print(f"Error fetching data from {url}: {e}")
return clan_name, ""
async def fetch_all_clan_data():
tasks = [fetch_clan_data(url, clan_name) for url, clan_name in clan_urls]
return await asyncio.gather(*tasks)
async def generate_main_spreadsheet(clan_data):
try:
# Create a new workbook for the main spreadsheet
main_wb = openpyxl.Workbook()
# Delete the default sheet
main_wb.remove(main_wb.active)
clan_players_data = {}
# Iterate over each clan's data
for clan_name, content in clan_data:
if not content:
print(f"No content for {clan_name}, skipping...")
continue
soup = BeautifulSoup(content, 'html.parser')
# Find the roster table
roster_table = soup.find('table', id='roster')
if roster_table:
# Create a new worksheet for the current clan with the clan name
ws = main_wb.create_sheet(title=clan_name)
# Extract headers from the table
headers = ['Player Tag', 'Name']
# Write headers to the worksheet
ws.append(headers)
# Extract rows from the table
rows = roster_table.find('tbody').find_all('tr')
clan_players = []
for row in rows:
try:
# Extract player tag
player_tag_div = row.find('div', class_='mobile-hide muted')
player_tag = player_tag_div.text.strip().replace('#', '') if player_tag_div else ''
# Extract player name
name_a = row.find('a', class_='block member_link')
name = name_a.contents[0].strip() if name_a else ''
# Write row data to the worksheet
ws.append([player_tag, name])
# Collect player data for war stats generation
clan_players.append((player_tag, name))
except AttributeError as e:
print(f"AttributeError: {str(e)}")
continue
# Store clan players data
clan_players_data[clan_name] = clan_players
# Apply formatting to headers
for row in ws.iter_rows(min_row=1, max_row=1):
for cell in row:
cell.font = Font(size=9)
cell.alignment = Alignment(horizontal='center', vertical='center')
# Adjust column widths
for col in ws.columns:
max_length = 0
column = col[0].column_letter
for cell in col:
try:
if len(str(cell.value)) > max_length:
max_length = len(cell.value)
except:
pass
adjusted_width = (max_length + 2) * 1.2
ws.column_dimensions[column].width = adjusted_width
# Apply borders to all cells
border = Border(left=Side(border_style='thin', color='BFBFBF'),
right=Side(border_style='thin', color='BFBFBF'),
top=Side(border_style='thin', color='BFBFBF'),
bottom=Side(border_style='thin', color='BFBFBF'))
for row in ws.iter_rows():
for cell in row:
cell.border = border
# Display clan information
members_count = len(rows)
last_updated = datetime.now().strftime("%d/%m/%Y | %H:%M:%S")
ws['C2'] = f'Clan: '
ws['D2'] = f'{clan_name}'
ws['C3'] = f'Members: '
ws['D3'] = f'{members_count}'
ws['C4'] = f'Last Updated: '
ws['D4'] = f'{last_updated}'
# Apply formatting to clan information
info_cells = ['C2', 'D2', 'C3', 'D3', 'C4', 'D4']
for cell in info_cells:
ws[cell].font = Font(size=11)
ws[cell].alignment = Alignment(horizontal='right', vertical='center')
# Apply fill colors to clan information
ws['C2'].fill = PatternFill(start_color='FFFFFF', end_color='FFFFFF', fill_type='solid')
ws['D2'].fill = PatternFill(start_color='FFFFFF', end_color='FFFFFF', fill_type='solid')
ws['C3'].fill = PatternFill(start_color='D9D9D9', end_color='D9D9D9', fill_type='solid')
ws['D3'].fill = PatternFill(start_color='D9D9D9', end_color='D9D9D9', fill_type='solid')
ws['C4'].fill = PatternFill(start_color='FFFFFF', end_color='FFFFFF', fill_type='solid')
ws['D4'].fill = PatternFill(start_color='FFFFFF', end_color='FFFFFF', fill_type='solid')
# Apply borders to clan information cells
for cell in info_cells:
ws[cell].border = border
# Align text in Column C to the right and text in Column D to the left within the clan information
for row in ws.iter_rows(min_row=2, max_row=4, min_col=3, max_col=4):
for cell in row:
if cell.column == 3:
cell.alignment = Alignment(horizontal='right', vertical='center')
else:
cell.alignment = Alignment(horizontal='left', vertical='center')
# Save the main spreadsheet
main_wb.save("Aftermath_Militia_Player_Data.xlsx")
print("Main spreadsheet generation completed successfully.")
return clan_players_data
except Exception as e:
print(f"An error occurred: {str(e)}")
return {}
async def generate_clan_war_stats(clan_data, max_player_tags=50):
try:
for clan_name, players_data in clan_data.items():
print(f"Processing war stats for clan {clan_name}...")
# Create a new workbook for the clan's war stats
clan_wb = openpyxl.Workbook()
# Limit the number of players to process based on max_player_tags
limited_players_data = players_data[:max_player_tags]
for idx, (player_tag, player_name) in enumerate(tqdm.tqdm(limited_players_data, desc=f'Clan: {clan_name}'), start=1):
print(f"Gathering war information for {player_tag} | #{idx}...")
# Skip invalid player tags
if not re.match(r'^#?[a-zA-Z0-9]+$', player_tag):
print(f"Invalid player tag '{player_tag}', skipping...")
continue
try:
war_stats = await scrape_clan_wars_info(player_tag) # Scrape war stats for the player
if war_stats:
player_war_stats = war_stats[0]['War Statistics'] # Extract war stats for the player
# Create a new worksheet for the player within the clan's workbook
ws = clan_wb.create_sheet(title=player_tag)
# Write player's war stats to the worksheet with Win Count and Win Percentage in the header
headers = ['You', 'Opp', 'Season ID', 'Date', 'Clan Name', 'Decks Used', 'Fame', 'Win Count', 'Win Percentage']
ws.append(headers)
# Write rows of player's war stats
for stat in player_war_stats:
row = list(stat.values())
ws.append(row)
print(f"War information for {player_tag} gathered successfully.")
else:
print(f"No war information found for {player_tag}.")
except Exception as e:
print(f"An error occurred while gathering war information for {player_tag}: {str(e)}")
# Save the clan's war stats to a single file after processing all players
clan_stats_filename = f"{clan_name}_war_stats.xlsx"
# Remove the blank sheet created at the beginning
clan_wb.remove(clan_wb.active)
# Save the edited workbook
clan_wb.save(clan_stats_filename)
print(f"War stats for clan {clan_name} generated successfully.")
except Exception as e:
print(f"An error occurred while generating war stats: {str(e)}")
async def main():
try:
clan_data = await fetch_all_clan_data()
clan_players_data = await generate_main_spreadsheet(clan_data)
await generate_clan_war_stats(clan_players_data, max_player_tags=MAX_PLAYER_TAGS)
except Exception as e:
print(f"An error occurred in main: {str(e)}")
if __name__ == "__main__":
asyncio.run(main())
That is the main block of code, and then there is this code which is needed for the main script to run:
player_war_scraper.py
import re
import sys
import asyncio
import pandas as pd
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import httpx
from bs4 import BeautifulSoup
from io import StringIO
# Placeholder value for the number of rows of information to generate
num_rows = 12
async def scrape_clan_wars_info(player_tag):
try:
# Scrape card levels
card_levels_df = await scrape_card_levels(player_tag)
# Scrape war stats
war_stats_df = await scrape_war_stats(player_tag)
# Scrape win percentage
count, win_percentage = await scrape_win_percentage(player_tag)
# Merge card levels and war stats into one DataFrame
combined_df = pd.concat([card_levels_df, war_stats_df], axis=1)
# Add win statistics columns
combined_df['Count'] = count
combined_df['Percentage'] = win_percentage
# Convert the DataFrame to a list of dictionaries
combined_data = combined_df.to_dict(orient='records')
return [{'War Statistics': combined_data}]
except Exception as e:
print(f"An error occurred while scraping war information for player {player_tag}: {str(e)}")
return []
async def scrape_card_levels(player_tag):
urls = [
f"https://royaleapi.com/player/{player_tag}/battles/history?battle_type=warAny",
f"https://royaleapi.com/player/{player_tag}/battles/history?before=1716772359000&&battle_type=warAny"
]
all_data = []
unique_battles = set() # To keep track of unique battles
for idx, url in enumerate(urls):
data = await scrape_data(url)
for battle_data in data:
if tuple(battle_data['You']) not in unique_battles: # Check if battle data already seen
all_data.append(battle_data)
unique_battles.add(tuple(battle_data['You'])) # Add battle data to the set of unique battles
if idx < len(urls) - 1: # Add an empty row between data from each URL
all_data.append({})
# Combine all battles into one DataFrame
df = pd.DataFrame(columns=['You', 'Opp'])
for battle_data in all_data:
if not battle_data: # If empty row, skip
continue
temp_df = pd.DataFrame(battle_data)
df = pd.concat([df, temp_df], ignore_index=True)
# Convert 'You' and 'Opp' columns to numeric
df['You'] = df['You'].apply(lambda x: pd.to_numeric(x, errors='coerce'))
df['Opp'] = df['Opp'].apply(lambda x: pd.to_numeric(x, errors='coerce'))
return df
async def scrape_data(url):
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
}
async with httpx.AsyncClient() as client:
response = await client.get(url, headers=headers)
response.raise_for_status()
soup = BeautifulSoup(response.content, 'html.parser')
all_battles = []
segments_seen = set() # To keep track of segments already seen
for battle in soup.find_all('div', class_=re.compile(r'\bbattle_\w+')):
segments = battle.find_all('div', class_='ui basic segment team-segment')
segment_ids = tuple(id(seg) for seg in segments) # Get unique IDs for segments
if segment_ids not in segments_seen: # Check if segments already seen
you_cards = []
opp_cards = []
for i, segment in enumerate(segments):
card_levels = segment.find_all('div', class_='ui basic center card-level')
if i % 2 == 0: # Player's segment
# Extract only the numbers from card levels and convert to integers
you_cards.extend([int(re.search(r'\d+', card.get_text()).group()) for card in card_levels])
else: # Opponent's segment
# Extract only the numbers from card levels and convert to integers
opp_cards.extend([int(re.search(r'\d+', card.get_text()).group()) for card in card_levels])
# Ensure the length of the lists is the same
max_length = max(len(you_cards), len(opp_cards))
you_cards.extend([None] * (max_length - len(you_cards)))
opp_cards.extend([None] * (max_length - len(opp_cards)))
all_battles.append({'You': you_cards, 'Opp': opp_cards})
segments_seen.add(segment_ids) # Add segment IDs to the set
return all_battles
async def scrape_war_stats(player_tag):
war_stats = []
url = f"https://royaleapi.com/player/{player_tag}"
async with httpx.AsyncClient() as client:
response = await client.get(url)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'lxml')
# Configure Chrome options for headless mode and disable image loading
chrome_options = Options()
chrome_options.add_argument("--headless") # Run Chrome in headless mode
chrome_options.add_argument("--disable-gpu") # Disable GPU acceleration
chrome_options.add_argument("--disable-infobars") # Disable info bars
chrome_options.add_argument("--disable-dev-shm-usage") # Disable /dev/shm usage
chrome_options.add_argument("--no-sandbox") # Disable sandbox mode
chrome_options.add_argument("--disable-extensions") # Disable extensions
chrome_options.add_argument("--disable-browser-side-navigation") # Disable browser side navigation
chrome_options.add_argument("--disable-features=VizDisplayCompositor") # Disable viz display compositor
chrome_options.add_argument("--start-maximized") # Start maximized
chrome_options.add_experimental_option("prefs", {"profile.managed_default_content_settings.images": 2}) # Disable image loading
chrome_options.add_argument("--log-level=3") # Suppress logging to the console
# Initialize Chrome WebDriver with configured options
driver = webdriver.Chrome(options=chrome_options)
load_time = 1200
# Set implicit wait time
driver.implicitly_wait(load_time)
# Open the webpage
driver.get(url)
try:
accept_cookies(driver)
# Click the button to load war history
load_war_history(driver)
# Wait for the table to be loaded
table = WebDriverWait(driver, load_time).until(EC.visibility_of_element_located((By.CLASS_NAME, "player__cw2_history_table")))
# Get the table headers with their respective data content
headers = [th.get_attribute("data-content") for th in table.find_elements(By.TAG_NAME, "th")]
# Specify the columns to select
selected_columns = ['Season ID', 'Date', 'Clan Name', 'Decks Used', 'Fame']
# Get the table data
table_html = table.get_attribute('outerHTML')
# Read the HTML into a DataFrame
df = pd.read_html(StringIO(table_html))[0]
# Rename the columns with their respective data content
df.columns = headers
# Select only the specified columns
df_selected_columns = df[selected_columns]
# Select only the specified number of rows
df_selected_rows = df_selected_columns.head(num_rows)
# Convert the DataFrame to a dictionary
player_war_stats = df_selected_rows.to_dict(orient='records')
# Create a dictionary to hold all war stats for the player
consolidated_stats = {'Player Tag': player_tag, 'War Statistics': player_war_stats}
# Append consolidated stats to the war_stats list
war_stats.append(consolidated_stats)
finally:
# Close the WebDriver
driver.quit()
return pd.concat([pd.DataFrame(stats['War Statistics']) for stats in war_stats])
async def scrape_win_percentage(player_tag):
try:
url = f"https://royaleapi.com/player/{player_tag}"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
}
async with httpx.AsyncClient() as client:
response = await client.get(url, headers=headers)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
# Initialize variables to store win statistics
win_percentage = None
count = None
# Find all tables in the page
tables = soup.find_all('table')
# Look for the table containing "Percentage" and "Count"
for table in tables:
if "Percentage" in table.get_text() and "Count" in table.get_text():
# Find the tbody element within the table
tbody = table.find('tbody')
# Extract the first row (index 0) from tbody
if tbody:
first_row = tbody.find('tr')
# Extract text content from the first row
if first_row:
cells = first_row.find_all('td')
if len(cells) >= 3:
count = cells[1].text.strip()
win_percentage = cells[2].text.strip()
break
# Check if both count and win_percentage were found
if count is not None and win_percentage is not None:
return count, win_percentage # Return count and win_percentage separately
else:
print("Count or Percentage table not found.")
return None, None # Return None for both values if not found
except Exception as e:
print(f"An error occurred while scraping win percentage for player {player_tag}: {str(e)}")
return None, None # Return None for both values in case of any error
def accept_cookies(driver):
try:
# Switch to the iframe
driver.switch_to.frame("sp_message_iframe_1169948")
# Switch to the document within the iframe
iframe_document = driver.find_element(By.TAG_NAME, "html")
driver.switch_to.frame(iframe_document)
# Locate the "Accept" button within the dropdown
accept_button = driver.find_element(By.XPATH, "//button[contains(text(), 'Accept')]")
# Click the "Accept" button
accept_button.click()
# Switch back to the default content
driver.switch_to.default_content()
except:
pass # No cookie prompt found, continue without accepting
def load_war_history(driver):
# Click the button using JavaScript
load_button = driver.find_element(By.CLASS_NAME, "cw2_history_button")
driver.execute_script("arguments[0].click();", load_button)
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Please provide the player tag:")
sys.exit(1)
player_tag = sys.argv[1]
asyncio.run(scrape_clan_wars_info(player_tag))
I’ve also added the image of the excel file in question. The “You” and “Opp” columns are meant to be like that, however as you can see, the Win Count and Win Percentage are mimicking the column in terms of how many rows its occupying. I only want the Win Count and Win Percentage to displaying as 1 row, not several.