Guides

Web Scraping Stock Prices with Python and SOCKS5 Proxies

Learn web scraping stock prices with Python: parse HTML, validate quotes, export CSV, and configure Socks5.IO proxies for authorized collection workflows.

Gideon ReedGideon Reed18 min read

Quick answer: Web scraping stock prices with Python involves retrieving an authorized page, extracting quote fields, and checking the record before export. This tutorial uses Requests, Beautiful Soup and a synthetic HTML example. For jobs requiring a specific network route or session behavior, it also shows how to configure a Socks5.IO SOCKS5 proxy. Record the source timestamp, currency, exchange and market session; routing does not determine quote freshness or data-use rights.

Copy the complete HTML and Python examples below into local files. The DEMO instrument is fictional; reference-site images illustrate page structure rather than current market quotes.

Example scope: Documentation reviewed October 19, 2026. The reproducible example uses synthetic data, not a financial-site or Socks5.IO performance test.

Choose the right source before scraping stock prices

A number on a financial page is not necessarily an executable price. Decide whether you need a delayed displayed quote, a previous close, an intraday series or licensed real-time data before writing a collector.

What you need Appropriate starting point Important check
A small research snapshot from an approved page Static HTML with stable fields Permission, quote time, currency and session
Daily historical prices for backtesting Documented download or market-data API Adjustments, corporate actions and historical coverage
Quotes that appear only after rendering An authorized rendered page or documented API Raw response versus rendered DOM; source permissions
Low-latency trading or broad redistribution A feed licensed for that use Entitlements, latency, exchange coverage and redistribution rights

An API and an HTML scraper are different interfaces. For example, Alpha Vantage’s quote documentation distinguishes its quote endpoint and data entitlements. Calling an API with Requests is not DOM scraping, and its freshness still depends on the service and subscription.

Review the source’s terms, data license, robots rules and permitted request rate. Public visibility alone does not establish reuse rights. Google Finance’s disclaimer is a concrete example of a provider specifying data delays and restrictions on copying or commercial use. Do not assume that permission to view a page includes permission to build a commercial feed from it.

Define a useful stock-market record

To scrape stock market data reliably, identify the instrument and the meaning of the price before storing it. Keep at least these fields:

Field Why it matters
Symbol and exchange A ticker alone may be ambiguous across venues
Currency A numeric amount without its unit cannot be compared safely
Price and volume in shares Preserve decimal prices and verify whether volume is raw or abbreviated
Source quote time When the source says the quote applies
Retrieval time in UTC When your collector obtained the page
Market session Regular, pre-market, after-hours or unknown
Delay status Source-declared delay, unknown, or synthetic for a fixture
Source URL and source kind Provenance; keeps demonstrations out of production data

Retrieving a page at 16:10 does not turn a 16:00 quote into a 16:10 trade. A weekend fetch may contain Friday’s last price. Do not replace missing data with zero or substitute the previous close when the latest-price selector disappears.

reference-quote-fields

How to scrape stock market data with Python, step by step

Create an empty local folder for the tutorial. Save the HTML in step 2 and the complete Python block in step 3 into that folder.

1. Install the dependencies

Use Python 3.10 or later. Open a terminal in your tutorial folder and create an isolated environment:

powershellpowershell
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install "requests[socks]>=2.32,<3" "beautifulsoup4>=4.13,<5"

On macOS or Linux, use python3 -m venv .venv and .venv/bin/python instead. Subsequent commands below use python; run them with your environment’s executable, or activate the environment first.

The dependencies are requests[socks] and beautifulsoup4. Python’s built-in csv, decimal and datetime modules handle export, decimal arithmetic and timestamps. Pandas is optional for later analysis, not a prerequisite for extracting one quote.

2. Inspect the HTML you will parse

Save the complete block below as quote-demo.html, with UTF-8 encoding:

htmlhtml
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Fictional stock quote</title></head>
<body>
  <h1>DEMO Corporation — synthetic test instrument</h1>
  <p>Not a real security or a market quote.</p>
  <span class="quote-price" data-value="123.45">123.45</span>
  <span class="quote-volume" data-value="1234500">1,234,500</span>
  <time class="quote-time" datetime="2026-10-15T16:00:00-04:00">
    Synthetic timestamp
  </time>
</body>
</html>

Its symbol is DEMO, exchange is TEST and currency is USD. All values are fabricated training inputs, not a quote for a real company. The timestamp has an explicit offset, which lets the parser convert it to UTC without guessing the exchange’s timezone.

On an approved real page, inspect the raw HTML response as well as the rendered browser view. A browser can display data that a Requests response does not contain. Prefer a stable semantic attribute over a generated CSS class, and verify whether a numeric value lives in text, content or another attribute.

reference-view-source

3. Run the complete Python stock-price scraper

Save the entire Python block as quote_scraper.py, next to the HTML. It is complete, not an excerpt: imports, proxy setup, parsing, fetching, command-line options and CSV output are included.

pythonpython
import argparse
import csv
import ipaddress
import os
import re
import sys
from datetime import datetime, timezone
from decimal import Decimal
from pathlib import Path
from urllib.parse import quote, urlsplit

import requests
from bs4 import BeautifulSoup


def build_session(use_proxy=False):
    session = requests.Session()
    session.trust_env = False
    session.headers['User-Agent'] = 'QuoteResearchTutorial/1.0'
    if use_proxy:
        host = os.environ['SOCKS5IO_HOST']
        port = int(os.environ['SOCKS5IO_PORT'])
        if not re.fullmatch(r'[A-Za-z0-9.-]+', host) or not 1 <= port <= 65535:
            raise ValueError('Invalid proxy host or port')
        user = quote(os.environ['SOCKS5IO_USER'], safe='')
        password = quote(os.environ['SOCKS5IO_PASSWORD'], safe='')
        proxy = f'socks5h://{user}:{password}@{host}:{port}'
        session.proxies.update({'http': proxy, 'https': proxy})
    return session


def is_local_url(url):
    host = (urlsplit(url).hostname or '').lower().rstrip('.')
    if host == 'localhost' or host.endswith('.localhost'):
        return True
    try:
        address = ipaddress.ip_address(host)
    except ValueError:
        return False
    mapped = getattr(address, 'ipv4_mapped', None)
    return address.is_loopback or address.is_unspecified or bool(mapped and mapped.is_loopback)


def fetch_html(session, url):
    parsed = urlsplit(url)
    if parsed.username or parsed.password or parsed.query or parsed.fragment:
        raise ValueError('Use a public quote URL without credentials, query or fragment')
    local = is_local_url(url)
    if local and any(session.proxies.values()):
        raise ValueError('Local sources cannot use a remote proxy')
    if parsed.scheme != 'https' and not (local and parsed.scheme == 'http'):
        raise ValueError('Use HTTPS, or HTTP for the local tutorial server only')
    with session.get(url, timeout=(5, 20), allow_redirects=False) as response:
        if response.status_code == 429:
            raise ValueError('HTTP 429: stop; review Retry-After and the source quota')
        if response.status_code in {401, 403}:
            raise ValueError(f'HTTP {response.status_code}: stop; check access rights')
        if response.status_code != 200:
            raise ValueError(f'HTTP {response.status_code}: inspect before continuing')
        if 'text/html' not in response.headers.get('Content-Type', '').lower():
            raise ValueError('Expected HTML, not an API response or file download')
        return response.content


def field(soup, selector):
    matches = soup.select(selector)
    if len(matches) != 1:
        raise ValueError('Each selector must match exactly one node')
    node = matches[0]
    for attr in ('data-value', 'content', 'datetime'):
        if node.get(attr) is not None:
            return node[attr].strip()
    return node.get_text(' ', strip=True)


def number(raw, integer=False):
    pattern = r'(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?'
    if not re.fullmatch(pattern, raw):
        raise ValueError('Unsupported number format or missing value')
    value = Decimal(raw.replace(',', ''))
    if integer and value != value.to_integral_value():
        raise ValueError('Volume must be a whole number of shares')
    if not integer and value <= 0:
        raise ValueError('Price must be positive')
    return str(int(value)) if integer else format(value, 'f')


def parse_quote(html, price_selector, volume_selector, time_selector):
    soup = BeautifulSoup(html, 'html.parser')
    raw_time = field(soup, time_selector)
    source_time = datetime.fromisoformat(raw_time.replace('Z', '+00:00'))
    if source_time.tzinfo is None:
        raise ValueError('Quote timestamp needs a verified timezone offset')
    return {'price': number(field(soup, price_selector)),
            'volume_shares': number(field(soup, volume_selector), integer=True),
            'quote_time_raw': raw_time,
            'quote_time_utc': source_time.astimezone(timezone.utc).isoformat()}


def main():
    parser = argparse.ArgumentParser()
    source = parser.add_mutually_exclusive_group(required=True)
    source.add_argument('--fixture', type=Path)
    source.add_argument('--url')
    parser.add_argument('--approved', action='store_true')
    parser.add_argument('--proxy', action='store_true')
    parser.add_argument('--demo-http', action='store_true')
    for name in ('symbol', 'exchange', 'currency', 'market-session', 'delay-status'):
        parser.add_argument('--' + name, required=True)
    parser.add_argument('--price-selector', default='.quote-price')
    parser.add_argument('--volume-selector', default='.quote-volume')
    parser.add_argument('--time-selector', default='.quote-time')
    parser.add_argument('--out', type=Path, required=True)
    args = parser.parse_args()
    if args.fixture:
        if args.proxy or args.demo_http:
            parser.error('Fixture mode does not use a proxy or HTTP')
        html, source_url, kind = args.fixture.read_bytes(), 'local:quote-demo.html', 'synthetic_fixture'
    else:
        if not args.approved:
            parser.error('Review source permissions, terms and robots rules before --approved')
        local_source = is_local_url(args.url)
        if args.demo_http and not local_source:
            parser.error('--demo-http is reserved for the local tutorial server')
        if local_source and args.proxy:
            parser.error('Local sources cannot use a remote proxy')
        with build_session(args.proxy) as session:
            html = fetch_html(session, args.url)
        source_url = args.url
        kind = 'synthetic_http_fixture' if local_source else 'user_approved_html'
    row = {'symbol': args.symbol, 'exchange': args.exchange, 'currency': args.currency,
           **parse_quote(html, args.price_selector, args.volume_selector, args.time_selector),
           'market_session': args.market_session, 'delay_status': args.delay_status,
           'retrieved_at_utc': datetime.now(timezone.utc).isoformat(),
           'source_url': source_url, 'source_kind': kind}
    with args.out.open('x', encoding='utf-8', newline='') as handle:
        writer = csv.DictWriter(handle, fieldnames=list(row))
        writer.writeheader()
        writer.writerow(row)
    print('Saved one validated record. Source kind:', kind)


if __name__ == '__main__':
    try:
        main()
    except ValueError as error:
        print('Stopped:', error, file=sys.stderr)
        sys.exit(1)
    except (KeyError, OSError, requests.RequestException):
        print('Stopped: check HTTP/access status, selectors, fields, environment or output filename.', file=sys.stderr)
        sys.exit(1)

Run this command from the same folder, using your virtual environment’s Python:

powershellpowershell
python quote_scraper.py --fixture quote-demo.html --symbol DEMO --exchange TEST --currency USD --market-session regular --delay-status synthetic --out demo-quotes.csv

The parser requires one matching node per selected field, checks numeric formatting and rejects source timestamps without timezone offsets. It does not verify that the page matches your supplied symbol, exchange or currency, or that the quote is fresh and belongs to the intended session. Verify those facts and your acceptable quote age separately. Existing output filenames are refused, so choose a new filename on repeat runs.

Beautiful Soup’s selector documentation explains the selection mechanism. The image below highlights the parsing functions; copy the complete text above to run the program.

python-quote-parser

Requiring exactly one match prevents a layout change from silently selecting an unrelated price. The number helper accepts US decimal formatting and grouped thousands; it rejects blanks, N/A, currency symbols, 1.2M and comma-decimal formats. Add a source-specific normalizer only after confirming that format and its units.

4. Inspect the exported record

The sample should produce these deterministic values:

texttext
symbol: DEMO
exchange: TEST
currency: USD
price: 123.45
volume_shares: 1234500
quote_time_utc: 2026-10-15T20:00:00+00:00
delay_status: synthetic
source_kind: synthetic_fixture

The CSV also includes the raw source timestamp, source URL, market session and actual retrieval timestamp. Your retrieval time will differ. Check the file as text first; spreadsheet software can otherwise reformat timestamps or identifiers without asking.

synthetic-csv-output

For a reproducible failure, run with a selector that does not exist:

powershellpowershell
python quote_scraper.py --fixture quote-demo.html --symbol DEMO --exchange TEST --currency USD --market-session regular --delay-status synthetic --price-selector ".missing-price" --out must-not-exist.csv

Expect Stopped: Each selector must match exactly one node, a nonzero exit code and no new output file. This checks missing-field handling, not the accuracy of real market data.

5. Exercise the HTTP path locally

To test fetching without sending traffic to a financial website, serve the same fictional page. In a second terminal, from your tutorial folder, run:

powershellpowershell
python -m http.server 8765 --bind 127.0.0.1

Leave that terminal open, then run the collector in your first terminal:

powershellpowershell
python quote_scraper.py --url http://127.0.0.1:8765/quote-demo.html --approved --demo-http --symbol DEMO --exchange TEST --currency USD --market-session regular --delay-status synthetic --out demo-http-quotes.csv

Expect source_kind to be synthetic_http_fixture. Stop the server with Ctrl+C when finished. Recognized localhost and loopback sources keep this label even if --demo-http is omitted. Their URLs are rejected with --proxy before proxy setup or any fetch; fetch_html also rejects them when given an explicitly proxied session. This local request checks the HTTP/CSV workflow, not a remote proxy. The URL check covers explicit local names and IP literals, not arbitrary DNS aliases or rebinding.

localhost-route-guard

6. Adapt the selectors to an authorized market page

For a real collection job, replace the source and all instrument metadata with verified values. Set --price-selector, --volume-selector and --time-selector to selectors observed in that source’s returned HTML. The fixture defaults are not universal Yahoo, Bloomberg or TradingView selectors.

reference-dom-inspection

Run one approved HTTPS URL with --approved and a fresh output filename. That flag records your deliberate decision to run the request; it does not inspect a license or confer permission. The starter rejects query strings and embedded credentials to keep token-bearing URLs out of CSV exports. If your approved source needs query parameters, extend request construction and URL redaction deliberately before using it.

For example, a source you control might use #last-price, #volume-shares and time.quote-asof. Supply those exact selectors only if the page contains them. If the source exposes only a human-readable time such as “4:00 PM,” implement and test its date, timezone and daylight-saving rules before importing records. The starter instead fails on ambiguous timestamps.

Here is a complete parameter template, not a verified financial website. Replace the reserved .invalid address and every REPLACE_... value with source-verified information before running it:

powershellpowershell
python quote_scraper.py --url "https://quotes.example.invalid/quote/REPLACE_SYMBOL" --approved --symbol "REPLACE_SYMBOL" --exchange "REPLACE_EXCHANGE" --currency "REPLACE_CURRENCY" --market-session "REPLACE_SESSION" --delay-status "REPLACE_DELAY" --price-selector "REPLACE_PRICE_CSS" --volume-selector "REPLACE_VOLUME_CSS" --time-selector "REPLACE_TIME_CSS" --out authorized-direct.csv

The time selector must return an ISO timestamp with an offset, through text or an attribute supported by field. A real source needing different time/number formats requires an adapter before this command can work correctly.

If the fields exist only after JavaScript execution, use a documented data interface or permitted browser rendering. Beautiful Soup does not execute scripts. Do not treat a consent page, challenge screen or HTTP 200 login page as a valid quote response.

Which proxy setup fits a stock-price scraping workflow?

Choose an IP type and a session policy separately. A residential route need not rotate on every request, and a sticky session is not a permanently assigned address.

Workflow requirement Setup to evaluate What to verify
A small permitted job with no routing requirement Direct connection Source request budget and valid fields
Allowlisting or long-lived exit continuity Static residential/ISP or another genuinely fixed endpoint Rental period, replacement policy and exit after reconnect
Several related requests in a short interval Sticky residential session Session expiry, idle timeout and peer availability
Independent, permitted localized requests Location-targeted rotating residential endpoints Observed exit and location; shared source-wide quota
A source that accepts hosting-network traffic Datacenter proxy Valid-record rate, latency and cost on that source

For Socks5.IO residential routing, the session-control documentation distinguishes sticky and rotating modes. At this review, its standard sticky example describes a 10-minute session and reassignment after expiry or over 60 seconds idle; continuity also depends on the node remaining available. Confirm the generated configuration for your product rather than treating this example as a fixed-IP lease. Its rotating example uses -f-1 for per-request rotation; do not add or remove parameters without checking the selected mode.

Keep a stable exit for permitted multi-request comparisons that must share a session. Use rotation for independent requests only when consistent with the source’s conditions—not to reset a publisher’s quota or evade a refusal. For longer continuity, evaluate a fixed endpoint instead of repeatedly extending a sticky assumption.

Add Socks5.IO to an approved collection workflow

Socks5.IO’s rotating residential proxies are an option for approved collection or localized QA workflows that need residential routing and session controls. Apply the selection criteria above before choosing a product.

Use the account-and-password configuration guide to obtain your selected product’s issued host, port and proxy-user credentials. Keep generated location or session parameters exactly as documented. Do not invent a rotating-port range, substitute the website dashboard password, or alter a session parameter by guessing.

socks5io-endpoint-fields

Configure authenticated SOCKS5 in Python

The complete script’s build_session reads issued endpoint fields from the environment and applies the proxy to both destination schemes. The excerpt below highlights this configuration; the complete program is in step 3.

python-socks5-session

The dictionary keys describe destination URL schemes; both can use the same SOCKS proxy. Requests documents the requests[socks] dependency and proxy-side DNS with socks5h. This HTTPS collection uses TCP; optional UDP features do not make it a faster market-data feed.

Set the issued values in your process environment or secret manager. For an interactive PowerShell session, these prompts keep the password out of the typed command history:

powershellpowershell
$env:SOCKS5IO_HOST = Read-Host 'Issued proxy host'
$env:SOCKS5IO_PORT = Read-Host 'Issued proxy port'
$env:SOCKS5IO_USER = Read-Host 'Issued proxy username'
$proxySecret = Read-Host 'Issued proxy password' -AsSecureString
$env:SOCKS5IO_PASSWORD = [System.Net.NetworkCredential]::new('', $proxySecret).Password

Environment variables are not an encrypted secret store. Use a dedicated process, do not print them, and remove them when finished. The script percent-encodes credentials when constructing the URL and suppresses raw network exception text that might contain it. Never publish a credential-bearing proxy URL in code, screenshots or logs.

After setting the issued environment fields, use this complete proxy template. Again, replace all placeholders first; .invalid is deliberately not a live target:

powershellpowershell
python quote_scraper.py --url "https://quotes.example.invalid/quote/REPLACE_SYMBOL" --approved --proxy --symbol "REPLACE_SYMBOL" --exchange "REPLACE_EXCHANGE" --currency "REPLACE_CURRENCY" --market-session "REPLACE_SESSION" --delay-status "REPLACE_DELAY" --price-selector "REPLACE_PRICE_CSS" --volume-selector "REPLACE_VOLUME_CSS" --time-selector "REPLACE_TIME_CSS" --out authorized-proxy.csv

Change only routing between the direct and proxy runs; keep the approved URL, metadata, selectors and collection window comparable. The script disables ambient proxy and .netrc settings, does not retry directly on a proxy failure, and keeps TLS verification enabled. Managed environments needing a custom CA must configure an approved trust bundle explicitly.

Verify routing separately from quote correctness

First compare direct and proxy requests from this Python client against a controlled endpoint you are authorized to use. Record the route, observation time and exit privately. A browser IP display or a standalone credential test does not prove this Python session used the proxy.

Then request the same approved quote URL using unchanged selectors and metadata. A successful routing check does not establish that the returned page contains a valid quote. Count a record as usable only after checking its instrument, fields, timestamp and source status.

Observation Next action
Proxy connection or authentication fails Recheck the issued host, port, protocol, credentials and active service
Exit observed but expected fields are missing Inspect source layout, response type and authorization state
Quote parses but is older than expected Check the source’s timestamp, session and delay policy
Direct and proxy pages differ Preserve time, request conditions and content differences before attributing a cause

Measure usable records, not just successful requests

Keep a per-attempt record of HTTP classification, elapsed time, received content bytes, validation result and failure reason. Log source/instrument identity and session labels privately without passwords or full credential-bearing URLs. Compare direct and proxy routes under the same source-wide budget and near-identical request conditions.

  • Valid-record rate = records passing structural, instrument, session and freshness checks ÷ attempted quote requests. The program performs structural checks; apply the other checks separately before counting a usable record.
  • Request duration: measure the full fetch with a monotonic clock; report median and p95 together with failures and timeouts. Do not compare only the fastest successes.
  • Traffic: record received content bytes, but do not equate these with billable bytes. Compression, overhead, failed requests and provider accounting can differ.
  • Cost per valid record = attributable billed collection cost ÷ usable records. With zero usable records it is undefined, not zero. Label shared-cost allocation and use real billing, not a headline price alone.

Start with a small source-approved sample and no concurrency. Increase workers only within the agreed request budget. For broader trade-offs, see our guide to choosing proxies for scraping. No rate, latency or cost result is prefilled here.

Handle errors without corrupting your dataset

The starter makes one request, checks the response and stops on failure. Its Requests timeout settings are (5, 20) for connection and read operations, not a guaranteed total job deadline. It also stops on redirects, leaving you to review the destination before changing a collection URL.

Failure What to investigate What not to do
HTTP 401 or 403 Access conditions, credentials or source policy Automatically cycle IPs to evade the refusal
HTTP 429 Quota and Retry-After, if supplied Immediately retry through another exit
Timeout or 5xx Temporary availability; source-approved retry policy Retry indefinitely or write partial quotes
HTTP 200 challenge/consent page Response body and expected fields Count status 200 as extraction success
Zero or multiple selector matches Layout changes or ambiguous regular/after-hours fields Take the first number anywhere on the page
Missing/ambiguous time or units Source-specific parsing rules Guess an exchange timezone or volume multiplier

Production retries should be bounded, delayed and consistent with the source’s instructions. Apply a per-source request budget across all workers and exits. A larger proxy pool does not increase the quota a publisher grants your application.

Example business workflow: an end-of-day research dashboard

Consider a research team licensed to retain daily snapshots for 24 instruments. This is a design example, not a customer performance claim. The team records one approved close observation per instrument after its source’s expected publication time, validates currency and session, and sends missing or stale records to review.

Its collection target is 24 scheduled observations per run, not thousands of requests per minute. Historical split adjustments come from a documented historical source; yesterday’s HTML quote is not silently substituted. A fixed source URL, parser version and retrieval timestamp make it possible to investigate revisions later.

For an approved US-versus-UK page comparison, the team can generate two permitted location-targeted endpoints, assign private session labels and verify each observed exit from the Python client. Keep language, instrument, currency, cookies and collection times comparable. Use each sticky session only within its confirmed lifetime, and record a new observation after reconnect or expiry. A localized display difference is not an arbitrage opportunity by itself.

Acceptance still means correctly identified, timestamped records or logged failures—not 24 HTTP 200 responses. The team checks valid-record rate, time and attributable cost before choosing whether fixed, sticky or rotating routing is useful. A trading system needing licensed low-latency ticks should use an appropriate feed instead.

Conclusion

Web scraping stock prices starts with a permitted source and a precise data definition. Run the fictional HTML example, validate its CSV, test missing-field failures, and only then adapt a collector to an approved market page. Preserve source time, retrieval time, currency and session so a plausible number does not become misleading data.

If your authorized workflow needs a particular exit location or session behavior, choose the relevant Socks5.IO proxy type, use the product and authentication guidance above to generate an endpoint, and verify it from the same Python client. Run a small permitted collection test and compare usable records, latency and attributable cost before scaling. The next step is a measured configuration decision—not a promise of unblocked access or real-time trading data.

FAQs

No, public visibility does not establish permission for every collection or reuse. Review the source's terms, robots rules, data license and intended use before running a job. Commercial redistribution can require different rights from internal research, while exchange data may have additional conditions. A proxy does not supply those rights. If the permitted scope is unclear, obtain clarification from the source or use a documented service licensed for your application.

Latest articles

Global proxy network illustration

Plug into the global proxy network — every connection faster and more stable

Start for Free