Web Scraping With Excel VBA and PowerShell
VBA web scraping in Excel takes one of two macros: if the data sits in an HTML <table>, a 10-line QueryTables.Add macro imports it into cells; for anything else, MSXML2.ServerXMLHTTP.6.0 fetches the page, an htmlfile document parses it, and a loop writes each value with Cells(r, c).Value. Will it work on JavaScript pages? Not directly: VBA reads only the HTML the server sends and runs no scripts. Test it in 10 seconds: if a value you see on screen is missing from View source (Ctrl+U), the page is built by JavaScript, so call the JSON request it makes (DevTools > Network > Fetch/XHR) from VBA, as in the third macro below. PowerShell does the same jobs with Invoke-WebRequest and Invoke-RestMethod. ProxyField residential proxies plug into both through setProxy and -Proxy, from $1.95/GB; start with the Easy Trial: 1 GB for $2.95.
- residential IPs
- 412M+
- from, traffic never expires
- $1.95/GB
- Easy Trial
- 1 GB for $2.95
On this page 7 sections
Which method fits your page
| The data on the page | VBA method | Works on JavaScript pages? |
|---|---|---|
An HTML <table> in the page source | QueryTables.Add with WebTables (macro 1) | No: the table imports empty or not at all |
| Cards, lists or any other markup in the source | ServerXMLHTTP + htmlfile (macro 2) | No |
| Loaded by JavaScript from a JSON request | ServerXMLHTTP on the JSON URL (macro 3) | Yes: you skip the JavaScript and read its data |
| Only appears after scripts run, no usable request | Not VBA: Power BI Desktop's Web.BrowserContents, or a browser driven from Python or PowerShell | Yes, with a browser engine |
A no-code route for real tables is Power Query (Data > Get Data > From Web); web scraping with Excel walks through it.
Macro 1: import an HTML table with a QueryTable
QueryTables.Add is Excel's built-in web query. It takes a connection string that starts with URL;, a destination cell, and which tables to import. This macro imports the 25-row hockey table of a practice sandbox into Sheet 1:
Sub ImportWebTable()
Dim qt As QueryTable
Set qt = ThisWorkbook.Sheets(1).QueryTables.Add( _
Connection:="URL;https://www.scrapethissite.com/pages/forms/", _
Destination:=ThisWorkbook.Sheets(1).Range("A1"))
With qt
.WebSelectionType = xlSpecifiedTables
.WebTables = "1" ' the first <table> on the page; "1,2" imports two
.WebFormatting = xlWebFormattingNone ' values only, no page styling
.Refresh BackgroundQuery:=False ' nothing is fetched until Refresh
End With
End SubWhat to expect: 9 columns (Team Name, Year, Wins, Losses, OT Losses, Win %, Goals For, Goals Against, + / -) and 25 team rows. Rerun qt.Refresh to update the same range. WebTables takes a comma-separated list of table index numbers or table id names, and it applies only when WebSelectionType is xlSpecifiedTables (Microsoft's QueryTable.WebTables and QueryTables.Add reference).
The limit is the one in the question: a QueryTable imports only <table> elements that are in the HTML the server sends. Point it at the sandbox's JavaScript page (/pages/ajax-javascript/) and the source holds an empty <tbody> hidden with display:none: the films only arrive after a script calls a JSON URL. That is what macro 3 reads.
Macro 2: fetch a page and write it to cells
When the data is not in a table, fetch the HTML and walk it. This macro reads the 250 countries of a practice sandbox (each in a div.country) into Sheet 1. It uses late binding (CreateObject), so you don't need to add references under Tools > References.
Sub ScrapeCountries()
Dim http As Object, doc As Object, divs As Object, card As Object
Dim ws As Worksheet, r As Long, i As Long
Set http = CreateObject("MSXML2.ServerXMLHTTP.6.0")
http.Open "GET", "https://www.scrapethissite.com/pages/simple/", False
http.setRequestHeader "User-Agent", "Mozilla/5.0"
http.send
If http.Status <> 200 Then
MsgBox "HTTP " & http.Status
Exit Sub
End If
Set doc = CreateObject("htmlfile")
doc.body.innerHTML = http.responseText
Set ws = ThisWorkbook.Sheets(1)
ws.Range("A1:C1").Value = Array("Country", "Capital", "Population")
r = 2
Set divs = doc.getElementsByTagName("div")
For i = 0 To divs.Length - 1
Set card = divs.Item(i)
If InStr(" " & card.className & " ", " country ") > 0 Then
ws.Cells(r, 1).Value = Trim(card.getElementsByTagName("h3").Item(0).innerText)
ws.Cells(r, 2).Value = card.getElementsByTagName("span").Item(0).innerText ' capital
ws.Cells(r, 3).Value = card.getElementsByTagName("span").Item(1).innerText ' population
r = r + 1
End If
Next i
End SubWhat each part does:
ServerXMLHTTP.6.0sends the request synchronously (FalseinOpen) and gives youStatusandresponseText. UnlikeMSXML2.XMLHTTP, it doesn't use the Windows internet cache, and it has its own proxy methods (below).htmlfileis the MSHTML document object. Assigning the response tobody.innerHTMLgives you a document you can search withgetElementsByTagNameandgetElementById. Its document mode is old, so newer methods such asquerySelectorAllmay be missing; filtering byclassName, as above, works everywhere.- Collections start at 0 and are read with
.Item(i). - Write to cells in one pass. For thousands of rows, fill a VBA array and assign it to a range at the end; writing cell by cell is the slow part of most VBA scrapers.
Expected result: 250 rows under the header. The macros rely on the page structure (each country's capital and population in its first two <span> elements). We wrote the three macros against Microsoft's documented objects on a Linux server, where VBA does not run; test them in Windows Excel before you depend on them.
Macro 3: JavaScript pages, read the JSON request
A JavaScript page usually loads its data from a URL you can call yourself. On the sandbox's film page, DevTools > Network > Fetch/XHR shows ?ajax=true&year=2015, which returns a JSON array of 16 films. VBA has no built-in JSON parser, so this macro pulls the fields with a regular expression:
Sub ImportFilmsJson()
Dim http As Object, re As Object, m As Object, ws As Worksheet, r As Long
Set http = CreateObject("MSXML2.ServerXMLHTTP.6.0")
http.Open "GET", "https://www.scrapethissite.com/pages/ajax-javascript/?ajax=true&year=2015", False
http.setRequestHeader "User-Agent", "Mozilla/5.0"
http.send
If http.Status <> 200 Then
MsgBox "HTTP " & http.Status
Exit Sub
End If
Set re = CreateObject("VBScript.RegExp")
re.Global = True
re.Pattern = """title"":\s*""([^""]*)"",\s*""year"":\s*(\d+),\s*""awards"":\s*(\d+),\s*""nominations"":\s*(\d+)"
Set ws = ThisWorkbook.Sheets(1)
ws.Range("A1:D1").Value = Array("Title", "Year", "Awards", "Nominations")
r = 2
For Each m In re.Execute(http.responseText)
ws.Cells(r, 1).Value = Trim(m.SubMatches(0))
ws.Cells(r, 2).Value = CLng(m.SubMatches(1))
ws.Cells(r, 3).Value = CLng(m.SubMatches(2))
ws.Cells(r, 4).Value = CLng(m.SubMatches(3))
r = r + 1
Next m
End SubExpected result: 16 rows, from "Spotlight" (2 awards, 6 nominations) down. The pattern matched all 16 films in the endpoint's response on 2026-10-27. A regex depends on the key order, so for nested or changing JSON, import the open-source VBA-JSON module (JsonConverter.bas, GitHub) and use JsonConverter.ParseJson. Scraping hidden APIs shows how to find and replay these requests on other sites.
When there is no JSON request: Power Query and browsers
Power Query has two web functions, and they differ on JavaScript. Excel's From Web generates Web.Page, a legacy function built on Internet Explorer; Power BI Desktop generates Web.BrowserContents, which uses Microsoft Edge's WebView2 control and returns the HTML "as viewed by a web browser". Its WaitFor option waits for a CSS selector or a time before it reads the page, for example Web.BrowserContents(url, [WaitFor = [Selector = "td.film-title", Timeout = #duration(0,0,0,10)]]) (Microsoft: Web connector troubleshooting, Web.BrowserContents). Outside Power BI, drive a real browser from Python or PowerShell (Selenium, Playwright) and write the result to a file Excel opens.
Several pages in a loop
Wrap the request in a function and loop the page parameter, with a pause so you don't hammer the site:
Function GetHtml(url As String) As String
Dim http As Object
Set http = CreateObject("MSXML2.ServerXMLHTTP.6.0")
http.Open "GET", url, False
http.setRequestHeader "User-Agent", "Mozilla/5.0"
http.send
If http.Status = 200 Then GetHtml = http.responseText
End Function
Sub ScrapePages()
Dim p As Long, html As String
For p = 1 To 24 ' the bound stops the loop: past page 24 the site still answers 200 with an empty table
html = GetHtml("https://www.scrapethissite.com/pages/forms/?page_num=" & p)
If Len(html) = 0 Then Exit For ' an HTTP error (GetHtml returns "" unless the status is 200)
' parse html as in ScrapeCountries and append rows
Application.Wait Now + TimeSerial(0, 0, 2) ' wait 2 seconds
Next p
End SubOther paging patterns are in web scraping pagination, and table parsing in other languages is in how to scrape a table from a website.
What about Internet Explorer and Selenium in VBA?
Older VBA tutorials drive Internet Explorer through InternetExplorer.Application, which ran the page's JavaScript. Microsoft ended support for the Internet Explorer 11 desktop application on certain Windows versions on June 15, 2022 (Microsoft lifecycle FAQ), so this route is a dead end for new work. SeleniumBasic, the COM wrapper that let VBA drive Chrome or Edge, has had no release since version 2.0.9.0 in March 2016 (GitHub releases), and current browsers need driver versions it was never built for.
PowerShell: Invoke-WebRequest and Invoke-RestMethod
Windows ships Windows PowerShell 5.1; PowerShell 7 runs on Windows, macOS and Linux (releases). The script below ran on PowerShell 7.6 on 2026-10-23:
$url = "https://www.scrapethissite.com/pages/simple/"
$resp = Invoke-WebRequest -Uri $url -UserAgent "Mozilla/5.0" -TimeoutSec 30
# Each country: the name in h3.country-name, then the capital and population spans
$pattern = '(?s)<h3 class="country-name">.*?</i>\s*(?<name>[^<]+?)\s*</h3>.*?country-capital">(?<capital>[^<]*)</span>.*?country-population">(?<pop>\d+)</span>'
$rows = [regex]::Matches($resp.Content, $pattern) | ForEach-Object {
[pscustomobject]@{
Name = $_.Groups['name'].Value.Trim()
Capital = $_.Groups['capital'].Value
Population = [int]$_.Groups['pop'].Value
}
}
$rows | Export-Csv -Path countries.csv -NoTypeInformation
"$($rows.Count) countries" # 250 countriesInvoke-WebRequest returns the status code, headers, the HTML in .Content, and the page's links and images in .Links and .Images. In PowerShell 7 it does not return a parsed document: the ParsedHtml property, which used Internet Explorer's engine, exists only in Windows PowerShell 5.1 (our PowerShell 7.6 response object had no such property). Three ways to parse HTML in PowerShell 7:
- Regex, as above, for small, stable pages. Keep patterns narrow: they break when the markup changes.
- A .NET HTML parser such as HtmlAgilityPack or AngleSharp, loaded with
Add-Type, for XPath or CSS selectors. - On Windows only, the same
htmlfileCOM object VBA uses:$doc = New-Object -ComObject "HTMLFile".
JSON endpoints: Invoke-RestMethod
Invoke-RestMethod parses JSON into objects for you, which makes the JavaScript-page route one line:
$films = Invoke-RestMethod "https://www.scrapethissite.com/pages/ajax-javascript/?ajax=true&year=2015"
$films | Select-Object title, awards, nominations | Export-Csv films-2015.csv -NoTypeInformation
"$($films.Count) films" # 16 filmsSchedule a PowerShell scraper
Save the script as scrape.ps1 and create a daily task:
$action = New-ScheduledTaskAction -Execute "pwsh.exe" -Argument "-NoProfile -File C:\scripts\scrape.ps1"
$trigger = New-ScheduledTaskTrigger -Daily -At 7am
Register-ScheduledTask -TaskName "DailyScrape" -Action $action -Trigger $triggerUse powershell.exe instead of pwsh.exe for Windows PowerShell 5.1. Scheduling options on other systems are in schedule web scrapers.
Add a proxy in VBA and PowerShell
When a site limits requests per IP or shows region-specific content, send the requests through a proxy. In VBA, ServerXMLHTTP takes the proxy and its credentials directly:
Set http = CreateObject("MSXML2.ServerXMLHTTP.6.0")
http.setProxy 2, "gate.proxyfield.io:7000" ' 2 = use this proxy server
http.Open "GET", "https://api.ipify.org", False
http.setProxyCredentials "<login>-country-us-city-new_york", "<password>"
http.send
Debug.Print http.responseText ' the proxy's IPIn PowerShell, pass -Proxy and a credential:
$pass = ConvertTo-SecureString $env:PROXY_PASS -AsPlainText -Force
$cred = [pscredential]::new("<login>-country-us-city-new_york", $pass)
Invoke-RestMethod "https://api.ipify.org" -Proxy "http://gate.proxyfield.io:7000" -ProxyCredential $cred| Setting | Value |
|---|---|
| Gateway | gate.proxyfield.io:7000 (HTTP, HTTPS and SOCKS5 on the same host and port) |
| Username | <login>-country-us-city-new_; add -region-<region>, -session-<id>, -ttl-<seconds>; every part after the login is optional |
| Rotation | a new IP on every request, or a sticky session from 30 s to 36 h (ttl 30 to 129,600; 20 minutes if you set a session without ttl) |
| Example | <login>-country-us-region-new_ |
Keep the password out of the workbook or script: read it from an environment variable, as above, or from the Windows Credential Manager. More options are on the PowerShell proxy setup page. Scrape public pages only and follow the site's terms; is web scraping legal? covers the rules.
Pulling web data into Excel or scripts? ProxyField residential proxies: 412M+ residential IPs in 195+ countries, HTTP, HTTPS and SOCKS5, from $1.95/GB with no subscription; start with the Easy Trial (1 GB for $2.95). Traffic never expires, and all purchases are final.
Web Scraping With Excel VBA and PowerShell FAQ
How do I write an Excel VBA macro that pulls data from a web page into cells, and will it work on JavaScript pages?
ProxyField's guide above gives three macros: a 10-line QueryTables.Add macro for an HTML table, an MSXML2.ServerXMLHTTP.6.0 + htmlfile macro that writes 250 rows with Cells(r, c).Value, and a JSON macro for script-built pages. On JavaScript pages, the first two read only the HTML the server sends, so the data is missing. Find the page's JSON request in DevTools > Network > Fetch/XHR and fetch that URL from VBA instead.
How can I create a basic web scraper in Excel using VBA?
Create MSXML2.ServerXMLHTTP.6.0, Open the URL with GET, send, and load responseText into a CreateObject("htmlfile") document. Loop over getElementsByTagName results, filter by className, and write each innerText into the sheet with Cells(r, c).Value. If the data is a plain HTML table, QueryTables.Add does it with no parsing.
What is the difference between a QueryTable and MSXML in VBA?
A QueryTable (QueryTables.Add with a URL; connection) imports whole HTML tables into a range and refreshes them with .Refresh, with no parsing code. MSXML's ServerXMLHTTP only fetches the response, so you parse it yourself, but it reads any markup or JSON and takes a proxy with setProxy.
Is VBA still relevant for web scraping in 2026?
For small jobs that end in an Excel workbook on Windows, yes: the QueryTable, MSXML and HTML objects still work and need no installs. For JavaScript-heavy sites, large volumes or anything outside Windows Excel, Power Query, Python or PowerShell are the better tools.
What are the limitations of VBA web scraping?
VBA runs no JavaScript once you drop the retired Internet Explorer, its HTML document object lacks modern selector methods, and it sends one request at a time. It is tied to Excel on Windows and gets slow when it writes thousands of cells one by one.
Are there alternatives to VBA for web scraping in Excel?
Yes. Power Query's From Web connector imports tables without code and refreshes them on demand, and Python or PowerShell scripts can write CSV or XLSX files that Excel opens. No-code scraping tools that export to Excel are another option.
How do I save scraped data to an Excel sheet using VBA?
Write each value with ws.Cells(row, col).Value = ..., or, faster, collect the rows in a two-dimensional VBA array and assign it to a range in one statement. Save the workbook with ThisWorkbook.Save if the macro runs unattended.
Can I scrape data from any website using VBA?
No. VBA can read pages whose data is in the HTML or in a JSON request, but not content that only appears after a page's scripts run, and it can't log in through complex sign-in flows. Some sites also block or limit automated requests, and their terms may forbid scraping.
Is PowerShell good for web scraping?
It is good for small and scheduled jobs, especially on Windows servers where it is already installed: Invoke-RestMethod handles JSON well, and Task Scheduler runs scripts unattended. HTML parsing is its weak spot in PowerShell 7, where you need regex or a .NET parser.
What are the differences between Invoke-WebRequest and Invoke-RestMethod?
Invoke-WebRequest returns the whole response: status code, headers, raw content, links and images. Invoke-RestMethod returns only the body, already converted to objects when it is JSON or XML, which makes it the better choice for APIs.
Show 4 more questionsShow fewer questions
Can PowerShell parse HTML without a module?
Partly. Invoke-WebRequest gives you the links, images and raw HTML, which you can search with regex; on Windows you can also load the HTML into the HTMLFile COM object. Windows PowerShell 5.1 has a ParsedHtml property, but PowerShell 7 does not.
Can PowerShell scrape JavaScript-rendered pages?
Not by itself, because Invoke-WebRequest never runs a page's scripts. Call the JSON request the page makes with Invoke-RestMethod, or drive a browser with Selenium's .NET library from PowerShell.
Why does curl in PowerShell reject -x?
In Windows PowerShell 5.1, curl is an alias for Invoke-WebRequest, which doesn't know curl's -x option. Run curl.exe -x http://gate.proxyfield.io:7000 ... to call the real curl that ships with Windows 10 and 11, or use PowerShell 7, where the alias was removed.
Can you use Selenium with PowerShell?
Yes. Load Selenium's .NET WebDriver library into PowerShell with Add-Type (community modules wrap it too) and drive Chrome, Edge or Firefox from a script. It renders JavaScript, but it is much slower than calling a page's JSON request with Invoke-RestMethod.
Related pages
Easy Trial: 1 GB for $2.95.
Try for the price of 1 GB, no subscription, traffic never expires.