TL;DR: Web scraping JavaScript tables in Python rarely needs a headless browser. Open DevTools, find the JSON endpoint that hydrates the grid, replay it with requests, paginate it, and fall back to Playwright only when the network call is signed, encrypted, or otherwise sealed shut.You wrote the obvious code. requests.get(url), hand the HTML to BeautifulSoup, pull the rows out of the <table>. The script runs, the file lands on disk, and the CSV is empty. Welcome to web scraping JavaScript tables, where the rows you see in your browser do not exist in the document the server actually returned.
Static tables ship the data inside the initial HTML. Dynamic tables (also called AJAX or JavaScript-rendered tables) ship a near-empty shell, then a script in the page calls a JSON endpoint and injects rows into the DOM after load. If you do not execute that script, you do not see those rows. Spinning up a full browser to fix this is a heavy answer to what is usually a small problem.
This guide takes the shorter route. We will start with a decision ladder so you stop guessing whether to reach for requests or a browser engine, then walk through finding the underlying JSON endpoint in DevTools, replaying it in Python with pagination and authentication, parsing it into clean rows, and exporting to CSV, JSON Lines, or SQLite. Playwright is here as a real fallback for sites that hide the network call, not as the default tool. By the end you will have a script you can rerun next quarter without rewriting it from scratch.




