Apify Public Data MCP

One of the biggest headaches when building automated job aggregators or market compensation trackers is posting age.

Job portals like Glassdoor don’t expose clean ISO timestamps (2026-09-10T12:00:00Z) in the listing feed HTML. Instead, they present relative, human-readable strings like:

If you are dumping these raw strings into Postgres, BigQuery, or Pandas, running temporal queries (e.g. “Show only jobs posted within the last 7 days”) becomes messy.

Here is how to cleanly normalize relative posting ages into deterministic ISO dates and precision flags in Python.


The Mathematical Parser

We can parse relative durations using regular expressions and calculate estimated timestamps relative to run execution (scraped_at), tagging the calculation with a precision enum (exact_day, estimated, or lower_bound):

import re
from datetime import datetime, timezone, timedelta

def parse_posting_age(age_str: str, reference_time: datetime = None):
    if not reference_time:
        reference_time = datetime.now(timezone.utc)
        
    if not age_str:
        return {
            "posting_age": None,
            "posting_date_estimated": None,
            "posting_date_precision": None
        }

    clean_str = age_str.strip().lower()

    # Matches "24h", "5h", "1h"
    hour_match = re.match(r"^(\d+)\s*h(?:ours?)?$", clean_str)
    if hour_match:
        hours = int(hour_match.group(1))
        est_date = reference_time - timedelta(hours=hours)
        return {
            "posting_age": clean_str,
            "posting_date_estimated": est_date.strftime("%Y-%m-%d"),
            "posting_date_precision": "estimated"
        }

    # Matches "30d+", "60d+" (Lower bound)
    plus_match = re.match(r"^(\d+)\s*d(?:ays?)?\+$", clean_str)
    if plus_match:
        days = int(plus_match.group(1))
        est_date = reference_time - timedelta(days=days)
        return {
            "posting_age": clean_str,
            "posting_date_estimated": est_date.strftime("%Y-%m-%d"),
            "posting_date_precision": "lower_bound"
        }

    # Matches "3d", "7d", "14 days"
    day_match = re.match(r"^(\d+)\s*d(?:ays?)?$", clean_str)
    if day_match:
        days = int(day_match.group(1))
        est_date = reference_time - timedelta(days=days)
        return {
            "posting_age": clean_str,
            "posting_date_estimated": est_date.strftime("%Y-%m-%d"),
            "posting_date_precision": "estimated"
        }

    # Fallback for unexpected formats
    return {
        "posting_age": clean_str,
        "posting_date_estimated": None,
        "posting_date_precision": "unknown"
    }

Glassdoor URLs from search grids are loaded with dynamic tracking telemetry, session IDs, and redirection wrappers:

https://www.glassdoor.com/partner/jobListing.htm?pos=101&ao=1136043&s=58&guid=00000191...&jobListingId=100987654321

To prevent database duplicates, extract the pure numeric job_id and construct a clean canonical permalink:

from urllib.parse import urlparse, parse_qs

def canonicalize_glassdoor_url(raw_url: str):
    parsed = urlparse(raw_url)
    qs = parse_qs(parsed.query)
    
    # Try finding the jobListingId or jl parameter
    job_id = qs.get("jobListingId", [None])[0] or qs.get("jl", [None])[0]
    
    if not job_id:
        # Regex search for digits in path
        id_match = re.search(r"-jl_(\d+)\.htm", raw_url) or re.search(r"/(\d{8,12})\.htm", raw_url)
        if id_match:
            job_id = id_match.group(1)

    if job_id:
        canonical_url = f"https://www.glassdoor.com/job-listing/-jl_{job_id}.htm"
        return job_id, canonical_url

    return None, raw_url

Production Cloud Endpoint

If you need this pipeline running with zero server maintenance, rotating proxies, and automated retries, we run this continuous enrichment pipeline on Apify:

from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("captainhandsome/glassdoor-jobs-scraper").call(run_input={
    "keyword": "Software Engineer",
    "location": "Austin, TX",
    "max_items": 30
})

jobs = list(client.dataset(run["defaultDatasetId"]).iterate_items())
for job in jobs[:3]:
    print(job["job_title"], "|", job["employer"], "|", job["posting_date_estimated"], f"({job['posting_age']})")