Spaces:
Runtime error
Runtime error
File size: 5,237 Bytes
0b5960f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | #!/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()
|