Skip to content

Commit 6f06c97

Browse files
committed
Add Step 2: ChessBase MegaDatabase indexer and query
megabase/indexer.py: streams a PGN export into a SQLite index with indexes on white/black player name columns. Handles multi-GB files without loading into memory; batches inserts; skips games with no player names. megabase/query.py: case-insensitive partial-name lookup across both colour columns; returns PGN or JSON; supports --limit; read-only DB connection for safety. 12 new tests covering indexing, schema, field storage, white/black lookup, partial match, case insensitivity, limit, date ordering, and PGN presence in results. README updated with Step 2 usage docs and Python API example. https://claude.ai/code/session_01VQfqug9MDEyRFydkmES4n2
1 parent be4a2ce commit 6f06c97

6 files changed

Lines changed: 438 additions & 4 deletions

File tree

README.md

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,46 @@ inspect the HTML to identify the right selector to add:
9898
python scraper.py Challenge34 --save-html page.html
9999
```
100100

101+
## Step 2 — ChessBase MegaDatabase
102+
103+
Export the MegaDatabase from ChessBase once (**File → Export → Export Database as PGN**),
104+
then build a local SQLite index for fast per-player lookups.
105+
106+
### Build the index (once)
107+
108+
```bash
109+
python -m megabase.indexer mega.pgn
110+
python -m megabase.indexer mega.pgn --db /data/megabase.db # custom path
111+
```
112+
113+
Streams the PGN — never loads the whole file into memory. Progress is printed every 10,000 games.
114+
115+
### Query by player name
116+
117+
```bash
118+
python -m megabase.query "Kasparov, Garry"
119+
python -m megabase.query "Kasparov, Garry" --output json
120+
python -m megabase.query "Kasparov" --limit 50 # partial name match
121+
python -m megabase.query "Kasparov, Garry" --db /data/megabase.db
122+
```
123+
124+
Returns PGN (default) or JSON. Matching is case-insensitive and covers both White and Black.
125+
126+
### Python API
127+
128+
```python
129+
from megabase.query import get_player_games
130+
131+
games = get_player_games("Kasparov, Garry", db_path="megabase.db")
132+
for game in games:
133+
print(game["event"], game["date"], game["result"])
134+
print(game["pgn"])
135+
```
136+
101137
## Roadmap
102138

103139
- [x] Step 1 — Scrape tournament entry lists (kingregistration, chessaction)
104-
- [ ] Step 2 — Index ChessBase MegaDatabase for fast player lookups
105-
- 2a. Export MegaDatabase to PGN from ChessBase (File → Export → Export Database as PGN)
106-
- 2b. Stream PGN into a local SQLite index keyed on player name
107-
- 2c. Query index by name to retrieve all games as PGN
140+
- [x] Step 2 — Index ChessBase MegaDatabase for fast player lookups
108141
- [ ] Step 3 — Look up each player on USCF / chess.com / Lichess
109142
- [ ] Step 4 — Analyse openings and tendencies
110143
- [ ] Step 5 — Generate per-opponent dossier report

megabase/__init__.py

Whitespace-only changes.

megabase/indexer.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""
2+
Build a SQLite index from a ChessBase MegaDatabase PGN export.
3+
4+
Run once after exporting from ChessBase (File → Export → Export Database as PGN).
5+
6+
Usage:
7+
python -m megabase.indexer <pgn_file> [--db megabase.db] [--batch 1000]
8+
9+
Example:
10+
python -m megabase.indexer mega.pgn
11+
python -m megabase.indexer mega.pgn --db /data/megabase.db
12+
"""
13+
14+
import io
15+
import sys
16+
import argparse
17+
import sqlite3
18+
19+
import chess.pgn
20+
21+
22+
DEFAULT_DB = "megabase.db"
23+
DEFAULT_BATCH = 1_000
24+
PROGRESS_EVERY = 10_000
25+
26+
27+
def create_schema(conn: sqlite3.Connection) -> None:
28+
conn.executescript("""
29+
CREATE TABLE IF NOT EXISTS games (
30+
id INTEGER PRIMARY KEY,
31+
white TEXT NOT NULL,
32+
black TEXT NOT NULL,
33+
date TEXT,
34+
event TEXT,
35+
result TEXT,
36+
pgn TEXT NOT NULL
37+
);
38+
CREATE INDEX IF NOT EXISTS idx_white ON games (white COLLATE NOCASE);
39+
CREATE INDEX IF NOT EXISTS idx_black ON games (black COLLATE NOCASE);
40+
""")
41+
42+
43+
def _pgn_text(game: chess.pgn.Game) -> str:
44+
buf = io.StringIO()
45+
exporter = chess.pgn.FileExporter(buf)
46+
game.accept(exporter)
47+
return buf.getvalue().strip()
48+
49+
50+
def build_index(pgn_path: str, db_path: str = DEFAULT_DB,
51+
batch_size: int = DEFAULT_BATCH) -> int:
52+
"""Stream pgn_path into db_path. Returns total games indexed."""
53+
conn = sqlite3.connect(db_path)
54+
create_schema(conn)
55+
56+
total = 0
57+
batch: list[tuple] = []
58+
59+
print(f"Indexing {pgn_path}{db_path}", file=sys.stderr)
60+
61+
with open(pgn_path, encoding="utf-8", errors="replace") as fh:
62+
while True:
63+
try:
64+
game = chess.pgn.read_game(fh)
65+
except Exception:
66+
continue
67+
if game is None:
68+
break
69+
70+
headers = game.headers
71+
white = headers.get("White", "?")
72+
black = headers.get("Black", "?")
73+
if white == "?" and black == "?":
74+
continue
75+
76+
batch.append((
77+
white,
78+
black,
79+
headers.get("Date"),
80+
headers.get("Event"),
81+
headers.get("Result"),
82+
_pgn_text(game),
83+
))
84+
85+
if len(batch) >= batch_size:
86+
_flush(conn, batch)
87+
total += len(batch)
88+
batch = []
89+
if total % PROGRESS_EVERY == 0:
90+
print(f" {total:,} games indexed…", file=sys.stderr)
91+
92+
if batch:
93+
_flush(conn, batch)
94+
total += len(batch)
95+
96+
conn.close()
97+
print(f"Done. {total:,} games indexed into {db_path}.", file=sys.stderr)
98+
return total
99+
100+
101+
def _flush(conn: sqlite3.Connection, batch: list[tuple]) -> None:
102+
conn.executemany(
103+
"INSERT INTO games (white, black, date, event, result, pgn) "
104+
"VALUES (?, ?, ?, ?, ?, ?)",
105+
batch,
106+
)
107+
conn.commit()
108+
109+
110+
def main() -> None:
111+
parser = argparse.ArgumentParser(description="Index a MegaDatabase PGN export into SQLite.")
112+
parser.add_argument("pgn", help="Path to exported PGN file")
113+
parser.add_argument("--db", default=DEFAULT_DB, help=f"SQLite database path (default: {DEFAULT_DB})")
114+
parser.add_argument("--batch", type=int, default=DEFAULT_BATCH,
115+
help=f"Insert batch size (default: {DEFAULT_BATCH})")
116+
args = parser.parse_args()
117+
118+
build_index(args.pgn, db_path=args.db, batch_size=args.batch)
119+
120+
121+
if __name__ == "__main__":
122+
main()

megabase/query.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""
2+
Query a MegaDatabase SQLite index for a player's games.
3+
4+
Usage:
5+
python -m megabase.query <player_name> [--db megabase.db] [--output pgn|json] [--limit N]
6+
7+
Examples:
8+
python -m megabase.query "Kasparov, Garry"
9+
python -m megabase.query "Kasparov, Garry" --output json
10+
python -m megabase.query "Kasparov" --limit 50 --output json
11+
"""
12+
13+
import sys
14+
import json
15+
import argparse
16+
import sqlite3
17+
18+
DEFAULT_DB = "megabase.db"
19+
20+
21+
def get_player_games(name: str, db_path: str = DEFAULT_DB,
22+
limit: int | None = None) -> list[dict]:
23+
"""
24+
Return all games where player name appears as White or Black.
25+
Matching is case-insensitive and supports partial names.
26+
"""
27+
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
28+
conn.row_factory = sqlite3.Row
29+
30+
pattern = f"%{name}%"
31+
sql = """
32+
SELECT white, black, date, event, result, pgn
33+
FROM games
34+
WHERE white LIKE ? COLLATE NOCASE
35+
OR black LIKE ? COLLATE NOCASE
36+
ORDER BY date DESC
37+
"""
38+
params: tuple = (pattern, pattern)
39+
if limit is not None:
40+
sql += " LIMIT ?"
41+
params = (pattern, pattern, limit)
42+
43+
rows = conn.execute(sql, params).fetchall()
44+
conn.close()
45+
46+
return [dict(row) for row in rows]
47+
48+
49+
def output_pgn(games: list[dict]) -> None:
50+
for game in games:
51+
print(game["pgn"])
52+
print()
53+
54+
55+
def output_json(games: list[dict]) -> None:
56+
# Exclude raw pgn from JSON summary by default for readability;
57+
# keep all fields since callers may need the pgn too.
58+
print(json.dumps(games, indent=2, ensure_ascii=False))
59+
60+
61+
def main() -> None:
62+
parser = argparse.ArgumentParser(description="Query a MegaDatabase SQLite index by player name.")
63+
parser.add_argument("player", help="Player name (full or partial, case-insensitive)")
64+
parser.add_argument("--db", default=DEFAULT_DB, help=f"SQLite database path (default: {DEFAULT_DB})")
65+
parser.add_argument("--output", choices=["pgn", "json"], default="pgn",
66+
help="Output format (default: pgn)")
67+
parser.add_argument("--limit", type=int, default=None,
68+
help="Maximum number of games to return")
69+
args = parser.parse_args()
70+
71+
games = get_player_games(args.player, db_path=args.db, limit=args.limit)
72+
print(f"Found {len(games)} game(s) for '{args.player}'.", file=sys.stderr)
73+
74+
if not games:
75+
return
76+
77+
if args.output == "json":
78+
output_json(games)
79+
else:
80+
output_pgn(games)
81+
82+
83+
if __name__ == "__main__":
84+
main()

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
requests>=2.31.0
22
beautifulsoup4>=4.12.0
3+
python-chess>=1.10.0
34
pytest>=8.0.0

0 commit comments

Comments
 (0)