-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscraper.py
More file actions
205 lines (159 loc) · 5.99 KB
/
Copy pathscraper.py
File metadata and controls
205 lines (159 loc) · 5.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
"""
Scrape tournament entry lists from kingregistration.com or chessaction.com.
Usage:
python scraper.py <tournament> [--site kingregistration|chessaction] [--output csv|json]
Tournament can be:
- A tournament ID shorthand (resolved using --site, default: kingregistration)
- A full URL (site auto-detected; --site flag ignored)
Examples:
python scraper.py Challenge34
python scraper.py Challenge34 --output json
python scraper.py nKGioA== --site chessaction
python scraper.py https://www.kingregistration.com/entrylist/Challenge34
python scraper.py "https://chessaction.com/tournaments/advance_entry_list.php?tid=nKGioA=="
python scraper.py Challenge34 --save-html page.html
"""
import sys
import json
import argparse
import requests
from bs4 import BeautifulSoup
# --- Site URL templates ---------------------------------------------------
_SITES = {
"kingregistration": "https://www.kingregistration.com/entrylist/{tid}",
"chessaction": "https://chessaction.com/tournaments/advance_entry_list.php?tid={tid}",
}
_HEADERS = {
"User-Agent": (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
}
# Normalise common column header variants to canonical field names
_HEADER_MAP = {
"name": "name",
"player": "name",
"player name": "name",
"full name": "name",
"last, first": "name",
"rating": "rating",
"rtng": "rating",
"uscf rating": "rating",
"pre-rating": "rating",
"pre rating": "rating",
"uscf": "uscf_id",
"uscf id": "uscf_id",
"uscf#": "uscf_id",
"id": "uscf_id",
"section": "section",
"division": "section",
"club": "club",
"team": "club",
"state": "state",
"grade": "grade",
"school": "school",
"city": "city",
}
# --- URL resolution -------------------------------------------------------
def _detect_site(url: str) -> str | None:
for site in _SITES:
if site in url:
return site
return None
def resolve_url(tournament: str, site: str = "kingregistration") -> str:
if tournament.startswith("http"):
return tournament
tid = tournament.rstrip("/").split("/")[-1]
return _SITES[site].format(tid=tid)
# --- Fetching & parsing ---------------------------------------------------
def fetch_html(url: str) -> str:
resp = requests.get(url, headers=_HEADERS, timeout=15)
resp.raise_for_status()
return resp.text
def parse_entry_list(html: str) -> list[dict]:
soup = BeautifulSoup(html, "html.parser")
for table in soup.find_all("table"):
rows = table.find_all("tr")
if len(rows) < 2:
continue
headers = [th.get_text(strip=True) for th in rows[0].find_all(["th", "td"])]
if not headers:
continue
# Skip tables with no recognised chess columns (e.g. nav/layout tables)
known = {_HEADER_MAP.get(h.lower().strip()) for h in headers} - {None}
if not known:
continue
players = []
for row in rows[1:]:
cells = [td.get_text(strip=True) for td in row.find_all(["th", "td"])]
if not cells or all(c == "" for c in cells):
continue
players.append(_normalize(dict(zip(headers, cells))))
if players:
return players
print("No player table found. Page text preview:", file=sys.stderr)
print(soup.get_text(separator="\n", strip=True)[:2000], file=sys.stderr)
return []
def _normalize(entry: dict) -> dict:
return {
_HEADER_MAP.get(k.lower().strip(), k.lower().strip()): v
for k, v in entry.items()
}
# --- Main entry point -----------------------------------------------------
def scrape_entry_list(tournament: str, site: str = "kingregistration",
save_html: str | None = None) -> list[dict]:
# Auto-detect site from full URLs so --site flag is optional
if tournament.startswith("http"):
detected = _detect_site(tournament)
if detected:
site = detected
url = resolve_url(tournament, site)
print(f"[{site}] Fetching: {url}", file=sys.stderr)
html = fetch_html(url)
if save_html:
with open(save_html, "w", encoding="utf-8") as fh:
fh.write(html)
print(f"HTML saved to {save_html}", file=sys.stderr)
players = parse_entry_list(html)
print(f"Found {len(players)} player(s).", file=sys.stderr)
return players
# --- Output helpers -------------------------------------------------------
def output_csv(players: list[dict]) -> None:
if not players:
print("No data.")
return
headers = list(players[0].keys())
print(",".join(f'"{h}"' for h in headers))
for player in players:
print(",".join(f'"{player.get(h, "")}"' for h in headers))
def output_json(players: list[dict]) -> None:
print(json.dumps(players, indent=2, ensure_ascii=False))
def main():
parser = argparse.ArgumentParser(
description="Scrape a chess tournament entry list.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("tournament", help="Tournament ID shorthand or full URL")
parser.add_argument(
"--site", choices=list(_SITES), default="kingregistration",
help="Site to scrape (default: kingregistration). Ignored when a full URL is given.",
)
parser.add_argument(
"--output", choices=["csv", "json"], default="csv",
help="Output format (default: csv)",
)
parser.add_argument(
"--save-html", metavar="FILE",
help="Save raw HTML to FILE for debugging",
)
args = parser.parse_args()
players = scrape_entry_list(args.tournament, site=args.site, save_html=args.save_html)
if args.output == "json":
output_json(players)
else:
output_csv(players)
if __name__ == "__main__":
main()