Scrape News Headlines from Google News

Google News is a good source when you want a fast topic monitor without building your own publisher-by-publisher crawler first.

In this tutorial we will build a lightweight Python scraper that:

  • queries Google News by topic
  • collects headlines, publishers, timestamps, and article links
  • deduplicates overlapping stories
  • exports a CSV you can schedule every hour or every morning

Mandatory screenshot of the target site:

Google News topic page

Scale topic monitoring cleanly with ProxiesAPI

Google News scraping stays simple when volume is low. Once you monitor many topics, ProxiesAPI helps keep the fetch layer predictable without changing the parser.


What we are scraping

Google News has several public surfaces. The browser UI is highly dynamic, but Google also exposes stable RSS endpoints that map to the same news product.

For topic searches, the URL pattern is:

https://news.google.com/rss/search?q=YOUR_QUERY&hl=en-US&gl=US&ceid=US:en

That is the surface we will scrape because it is:

  • public
  • fast to fetch
  • easy to parse
  • more stable than reverse-engineering changing client-side markup

You still get what you need for a headline monitor:

  • headline
  • canonical link
  • publisher
  • publication timestamp

If you later need screenshots, DOM validation, or article-card metadata from the rendered page, you can layer a browser on top. For the actual data pipeline, RSS is the better default.


Setup

python3 -m venv .venv
source .venv/bin/activate
pip install requests beautifulsoup4 lxml pandas python-dateutil

Step 1: Build the topic URL

Google News lets you control language and region with query parameters. Keeping them fixed makes the output more reproducible.

from urllib.parse import quote_plus


def google_news_rss_url(query: str, *, hl: str = "en-US", gl: str = "US", ceid: str = "US:en") -> str:
    return (
        "https://news.google.com/rss/search?q="
        + quote_plus(query)
        + f"&hl={quote_plus(hl)}&gl={quote_plus(gl)}&ceid={quote_plus(ceid)}"
    )

Example:

print(google_news_rss_url("artificial intelligence"))

Step 2: Add a fetch layer with optional ProxiesAPI

from __future__ import annotations

import os
import random
import time
from urllib.parse import quote_plus

import requests

TIMEOUT = (10, 30)
PROXIESAPI_KEY = os.getenv("PROXIESAPI_KEY", "")

session = requests.Session()
session.headers.update({
    "User-Agent": (
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/124.0.0.0 Safari/537.36"
    ),
    "Accept-Language": "en-US,en;q=0.9",
})


def proxiesapi_url(target_url: str) -> str:
    if not PROXIESAPI_KEY:
        raise RuntimeError("Set PROXIESAPI_KEY before enabling ProxiesAPI")
    return (
        "https://api.proxiesapi.com/?auth_key="
        + quote_plus(PROXIESAPI_KEY)
        + "&url="
        + quote_plus(target_url)
    )


def fetch_xml(url: str, *, use_proxiesapi: bool = False, attempts: int = 4) -> str:
    target = proxiesapi_url(url) if use_proxiesapi else url
    last_error = None

    for attempt in range(1, attempts + 1):
        try:
            response = session.get(target, timeout=TIMEOUT)
            response.raise_for_status()
            xml = response.text
            if "<rss" not in xml.lower():
                raise RuntimeError("response is not RSS/XML")
            return xml
        except Exception as exc:
            last_error = exc
            time.sleep(min(10, 2 ** (attempt - 1)) + random.random())

    raise RuntimeError(f"fetch failed: {last_error}")

Step 3: Parse the feed with real selectors

This parser uses the actual RSS structure:

  • channel > item
  • item > title
  • item > link
  • item > pubDate
  • item > source
from bs4 import BeautifulSoup
from dateutil import parser as date_parser


def parse_google_news_rss(xml: str, *, topic: str) -> list[dict]:
    soup = BeautifulSoup(xml, "xml")
    rows: list[dict] = []

    for item in soup.select("channel > item"):
        title = item.select_one("title")
        link = item.select_one("link")
        pub_date = item.select_one("pubDate")
        source = item.select_one("source")

        rows.append({
            "topic": topic,
            "title": title.get_text(" ", strip=True) if title else None,
            "url": link.get_text(" ", strip=True) if link else None,
            "source": source.get_text(" ", strip=True) if source else None,
            "published_at": date_parser.parse(pub_date.get_text(" ", strip=True)).isoformat() if pub_date else None,
        })

    return rows

Those selectors are boring, and that is exactly why they are good.


Step 4: Dedupe stories across topics

The same article often appears under multiple topic queries. Deduping before export keeps your monitor readable.

def dedupe_rows(rows: list[dict]) -> list[dict]:
    seen = set()
    out = []

    for row in rows:
        key = row.get("url") or row.get("title")
        if not key or key in seen:
            continue
        seen.add(key)
        out.append(row)

    return out

Step 5: Scrape multiple topics into one dataset

import pandas as pd


def scrape_topics(topics: list[str], *, use_proxiesapi: bool = False) -> pd.DataFrame:
    all_rows: list[dict] = []

    for topic in topics:
        url = google_news_rss_url(topic)
        xml = fetch_xml(url, use_proxiesapi=use_proxiesapi)
        all_rows.extend(parse_google_news_rss(xml, topic=topic))
        time.sleep(random.uniform(0.5, 1.3))

    rows = dedupe_rows(all_rows)
    df = pd.DataFrame(rows)
    if not df.empty:
        df = df.sort_values("published_at", ascending=False)
    return df

Main entry point:

def main() -> None:
    topics = [
        "artificial intelligence",
        "semiconductor industry",
        "enterprise software",
    ]
    df = scrape_topics(topics, use_proxiesapi=bool(PROXIESAPI_KEY))
    df.to_csv("google-news-headlines.csv", index=False)
    print(df.head(10).to_string(index=False))


if __name__ == "__main__":
    main()

Why this beats scraping the UI first

For a headline monitor, RSS is usually the right starting point.

ApproachGood forTradeoff
Google News RSSheadlines, links, publisher, timefewer page-level details
Browser UI scrapingscreenshots, visible card metadata, DOM validationheavier and more fragile
Publisher-by-publisher scrapingdeeper article fieldsmore engineering effort

This is the main practical lesson: start from the most stable public surface that gives you the data you actually need.


Common cleanup steps

Once you have the CSV, the next useful upgrades are:

  • strip publisher names from titles if the feed includes them inline
  • normalize redirects if you want publisher URLs instead of Google News URLs
  • bucket articles by topic or publisher
  • keep a small SQLite history so you can alert only on new headlines

You do not need those on day one. A flat CSV plus dedupe is already useful.


When ProxiesAPI matters here

For three topics once per day, it probably does not.

It starts to matter when you:

  • monitor dozens of topics
  • refresh frequently
  • pull multiple region/language variants
  • combine Google News with other news surfaces in one job

In that setup, the parser is not the headache. The request layer becomes the headache. That is where ProxiesAPI helps.


Final thoughts

If your goal is “get me a clean list of fresh headlines by topic,” Google News RSS is the fastest honest path.

You still validate the topic in the real Google News UI, which is why the screenshot matters. But for the data pipeline itself, stable XML selectors beat brittle client-side HTML every time.

That is a good scraping instinct to build early: prefer the simple public surface first, and only escalate to browser automation when the simpler surface stops giving you the fields you need.

Scale topic monitoring cleanly with ProxiesAPI

Google News scraping stays simple when volume is low. Once you monitor many topics, ProxiesAPI helps keep the fetch layer predictable without changing the parser.

Related guides

Scrape Live Stock Data from Yahoo Finance
Build a Yahoo Finance watchlist scraper in Python: pull current quote snapshots, day ranges, and volume from quote pages, then export a clean CSV using a ProxiesAPI-ready fetch layer.
tutorial#python#yahoo-finance#stocks
Scrape Numbeo Quality of Life Index by City with Python
Extract Numbeo's city-level quality-of-life scores, safety, traffic, pollution, and climate indicators into a clean dataset with Python and ProxiesAPI.
tutorial#python#numbeo#web-scraping
Scrape GitHub Topic Pages with Python + ProxiesAPI
Collect repository cards, stars, languages, repo URLs, and update timestamps from GitHub topic pages into a niche-watch dataset.
tutorial#python#github#web-scraping
Scrape Numbeo Crime Index by City with Python + ProxiesAPI
Extract city crime rankings, safety scores, and comparison-ready rows from Numbeo's public rankings table into JSON and CSV.
tutorial#python#numbeo#web-scraping