goworker / scripts /normalize.py
ChartDB Admin
Deploy GDELT Engine v3.0.0 - Timestamp processing API with parallel workers
0b5960f
Raw
History Blame Contribute Delete
5.24 kB
#!/usr/bin/env python3
"""
URL Normalization Script for GDELT Engine
This isolated Python script normalizes URLs for deduplication.
It can be called from Go via exec.Command or used standalone.
Usage:
python normalize.py <url>
echo "url1\nurl2" | python normalize.py --stdin
"""
import sys
import re
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
# Tracking parameters to remove
TRACKING_PARAMS = {
# UTM parameters
'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
'utm_id', 'utm_cid',
# Social media tracking
'fbclid', 'gclid', 'dclid', 'msclkid', 'twclid', 'igshid',
# Analytics
'ref', 'source', 'campaign', 'mc_cid', 'mc_eid',
'_ga', '_gl', '_hsenc', '_hsmi',
# News specific
'ncid', 'ocid', 'cmpid', 'ns_mchannel', 'ns_source', 'ns_campaign',
# General
'share', 'action', 'module', 'feed', 'rss',
}
# URL shortener domains that should not be normalized
SHORTENERS = {
'bit.ly', 't.co', 'goo.gl', 'ow.ly', 'tinyurl.com', 'is.gd',
'buff.ly', 'dlvr.it', 'j.mp', 'spr.ly', 'ht.ly',
}
def normalize_url(url: str) -> str:
"""
Normalize a URL for deduplication.
Steps:
1. Parse URL
2. Ensure scheme (default http)
3. Lowercase host
4. Remove www prefix
5. Remove tracking parameters
6. Remove fragment
7. Clean path (remove trailing slash, normalize)
"""
if not url:
return ''
# Ensure scheme
if not url.startswith(('http://', 'https://')):
url = 'http://' + url
try:
parsed = urlparse(url)
except Exception:
return ''
# Skip shorteners - return as-is
host = parsed.netloc.lower()
if any(short in host for short in SHORTENERS):
return url
# Lowercase and clean host
host = host.lower()
if host.startswith('www.'):
host = host[4:]
# Remove tracking parameters
query_params = parse_qs(parsed.query, keep_blank_values=True)
clean_params = {
k: v for k, v in query_params.items()
if k.lower() not in TRACKING_PARAMS
}
clean_query = urlencode(clean_params, doseq=True) if clean_params else ''
# Clean path
path = parsed.path
# Remove trailing slash (except for root)
if path != '/' and path.endswith('/'):
path = path.rstrip('/')
# Normalize double slashes
path = re.sub(r'/+', '/', path)
# Reconstruct URL without fragment
normalized = urlunparse((
parsed.scheme,
host,
path,
parsed.params,
clean_query,
'' # No fragment
))
return normalized
def extract_domain(url: str) -> str:
"""Extract the domain from a URL."""
try:
parsed = urlparse(url)
host = parsed.netloc.lower()
if host.startswith('www.'):
host = host[4:]
return host
except Exception:
return ''
def is_valid_news_url(url: str) -> bool:
"""Check if URL is likely a news article (not homepage, category, etc)."""
if not url:
return False
try:
parsed = urlparse(url)
path = parsed.path
# Must have a path beyond just /
if not path or path == '/':
return False
# Common non-article patterns
non_article_patterns = [
r'^/category/',
r'^/tag/',
r'^/author/',
r'^/page/',
r'^/search',
r'^/about',
r'^/contact',
r'^/privacy',
r'^/terms',
r'/feed/?$',
r'/rss/?$',
]
for pattern in non_article_patterns:
if re.match(pattern, path, re.IGNORECASE):
return False
# Likely an article if has date-like pattern or enough path depth
if re.search(r'/\d{4}/\d{2}/', path): # Date in URL
return True
if path.count('/') >= 2: # Multiple path segments
return True
if re.search(r'\.\w{3,4}$', path): # Has file extension
return True
if len(path) > 20: # Long path likely an article
return True
return True # Default to true
except Exception:
return False
def main():
"""Main entry point."""
if len(sys.argv) > 1:
if sys.argv[1] == '--stdin':
# Read URLs from stdin
for line in sys.stdin:
url = line.strip()
if url:
normalized = normalize_url(url)
print(normalized)
elif sys.argv[1] == '--validate':
# Validate URLs from stdin
for line in sys.stdin:
url = line.strip()
if url and is_valid_news_url(url):
print(normalize_url(url))
else:
# Single URL argument
print(normalize_url(sys.argv[1]))
else:
print("Usage: normalize.py <url>", file=sys.stderr)
print(" normalize.py --stdin", file=sys.stderr)
print(" normalize.py --validate", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()