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 Real Estate Listings from Realtor.com
Show how to collect listing cards, prices, beds, baths, and links into a clean housing-market dataset with Python and screenshots.
tutorial#python#real-estate#realtor
Scrape Rightmove Sold Prices
Build a sold-price dataset with Rightmove property cards, detail pages, sale dates, and historical prices using real selectors and a screenshot.
tutorial#python#rightmove#real-estate
Scrape Google Scholar Papers with Python
Pull paper titles, authors, snippets, and result links into a reusable research dataset from public Google Scholar result pages.
tutorial#python#google-scholar#web-scraping
Scrape GitHub Repository Contributors and Commit Activity with Python
Turn GitHub's public contributors and commit charts into a lightweight engineering-health dataset with Python, CSV export, and practical retry patterns.
tutorial#python#github#web-scraping