Skip to main content

urlgenie

🧞 URL Genie 🧞

URL extraction, generalization, validation, and filtration made easy.

🚀 About URL Genie

A python package based on research involving over 2 million URLs, designed to handle URLs in a flexible manner for data-driven projects.

Version 2 is a rewrite with an RFC 3986 compliant core and small, explicit functions in place of one generalize() carrying a dozen flags. See Migrating from 1.x if you are upgrading.

✨ Features

  • RFC 3986 compliant parsing, normalization and validation.
  • Social profile recognition for Facebook, LinkedIn, Twitter/X, Instagram and YouTube.
  • Email, phone and social extraction from free text, with validation.
  • Domain-scoped email filtering, to drop contacts unrelated to a site.
  • Duplicate reduction: every spelling of a profile collapses to one canonical URL.
  • Public suffix validation against the bundled PSL snapshot -- no network access.
  • Pure functions: no hidden state, safe to use across threads and processes.

⚙️ Installation

python -m pip install urlgenie

Depends only on tldextract, used offline against its bundled public suffix list.

♨️ Usage

from urlgenie import generalize, generalize_social, extract_social_handle

print(generalize("cnn.com/sports/about?a=b#c"))
>> https://cnn.com/sports/about

print(generalize("facebook.com.br/@Ahmed.Khatib"))
>> https://www.facebook.com/ahmedkhatib

print(generalize_social("x.com/elonmusk"))
>> https://twitter.com/elonmusk

print(extract_social_handle("x.com/elonmusk").handle)
>> elonmusk

Everything returns None on invalid input rather than a magic string, so use generalize(url) or "Bad Url" if you want a sentinel:

df["clean"] = df["url"].apply(lambda u: generalize(u) or "Bad Url")

For a one-off batch, generalize_many keeps every entry in place -- an invalid URL comes back as None rather than being silently dropped, so nothing shifts out of alignment with your input:

from urlgenie import generalize_many

for original, result in generalize_many("facebook.com,facebook/com,dummy.com,instagram.com/ahmed"):
    print(f"{original:22} -> {result}")

>> facebook.com           -> https://facebook.com
>> facebook/com           -> None
>> dummy.com              -> https://dummy.com
>> instagram.com/ahmed    -> https://www.instagram.com/ahmed

For a one-off run where you only want the URLs that came out clean and do not need to know which input produced which, pass drop_invalid=True:

generalize_many("facebook.com,facebook/com,dummy.com,instagram.com/ahmed", drop_invalid=True)
>> [('facebook.com', 'https://facebook.com'), ('dummy.com', 'https://dummy.com'), ('instagram.com/ahmed', 'https://www.instagram.com/ahmed')]
print(generalize("random.haz/somePath") or "Bad Url")
>> Bad Url

Validating a social URL against an expected platform

A form field asking for a Facebook profile has to check two different things: is this a Facebook URL at all, and does it actually point at a profile?

from urlgenie import validate_social_platform, validate_social_profile

url = "facebook.com/profile.php"

print(validate_social_platform(url, "facebook"))
>> True

print(validate_social_profile(url, "facebook"))
>> False

print(validate_social_profile("facebook.com/profile.php?id=123123123", "facebook"))
>> True

profile.php alone belongs to Facebook but isn't a profile -- it's a reserved path with no id. Adding ?id=... is what makes it one.

Extracting contacts from a page

from urlgenie import extract_contacts, validate_contacts

result = extract_contacts(scraped_text)

print(result.emails)
>> {'sales@example.com', 'someone@gmail.com'}

print(result.facebook)
>> {'https://www.facebook.com/example'}

print(result.phones)
>> {'+14155552671'}

#-Keep only emails belonging to the site being scraped-#
validated = validate_contacts(result, url="https://www.example.com/contact")

print(validated.emails)
>> {'sales@example.com'}

📋 API

Function Purpose
generalize(url, social=True, ...) Canonical URL, collapsing recognized social links to their profile
generalize_url(url, ...) Canonical URL only; keep_path / keep_query / keep_fragment / keep_userinfo / force_https / lower
generalize_many(urls, drop_invalid=False, ...) Same for a delimited string or iterable; returns [(original, result), ...] in order, so an invalid entry is None in place rather than dropped. Pass drop_invalid=True to omit those pairs entirely when you only want the clean ones
generalize_social(url) Canonical social profile URL, or None
extract_social_handle(url) SocialHandle(platform, handle, url, original_handle, rule), or None
detect_platform(url) / is_social_url(url) Platform lookup and membership test
parse_url(url) RFC 3986 parse + normalize into a ParsedUrl, or None
validate_url(url) Syntactic validity, public-suffix check, scheme allowlist
validate_email(email, url=None) Syntax, RFC 5321 length limits, optional site-domain match
validate_phone(phone) / normalize_phone(phone) Pragmatic digit-count validation
validate_social(url) Whether a URL is a recognized profile, on any platform
validate_social_platform(url, platform) Whether a URL's domain belongs to platform (e.g. "facebook"), regardless of whether the path resolves to a profile
validate_social_profile(url, platform) Whether a URL resolves to an actual profile/handle on platform. facebook.com/profile.php passes the platform check but not this one
extract_contacts(text, include=, exclude=) Emails, phones and socials from free text
validate_contacts(result, url=None) Filter an ExtractResult; scope emails to a site

🔀 Migrating from 1.x

Version 2 removes the UrlGenie class. Import the functions directly.

# 1.x
from urlgenie import UrlGenie
genie = UrlGenie(bad_url = "Bad Url", proper_tlds = True)
print(genie.generalize("fb.com/@ahmedkhatib"))
>> https://www.facebook.com/ahmedkhatib

# 2.x
from urlgenie import generalize
print(generalize("fb.com/@ahmedkhatib") or "Bad Url")
>> https://www.facebook.com/ahmedkhatib
1.x 2.x
UrlGenie(bad_url=..., bad_social=...) functions return None; use or "Bad Url"
proper_tlds=True always on, via the public suffix list
generalize(url) generalize(url)
generalize(url, get_handle=True) extract_social_handle(url).handle
generalize(url, get_domain=True) parse_url(url).domain
generalize(url, get_domain_with_tld=True) parse_url(url).registrable_domain
generalize(url, comma_separated=True) generalize_many(url) -- note this returns (original, result) pairs, not a flat list
generalize(url, social_rectification=False) generalize(url, social=False)
generalize(url, keep_periods=False) always on for Facebook, which ignores periods
extract_from_text(text) extract_contacts(text)
validate_result_dict(d, url=...) validate_contacts(result, url=...)
update_subdomains() / get_subdomains() removed; generalize() keeps no state

Behaviour changes worth knowing:

  • URLs 1.x silently rejected now parse: TLDs longer than four characters, subdomain labels longer than ten, ports, IDN hosts, IP literals, and example.com?a=b. Measured against the Tranco top 1M, 1.x rejected 4.72% of real domains; 2.x rejects 0.
  • Twitter handles follow Twitter's actual rules (15 characters, no periods), so some strings 1.x accepted are now rejected.
  • Facebook handles have periods stripped, since Facebook ignores them when resolving profiles. LinkedIn and Instagram periods are significant and preserved.

🧩 Adding a social pattern

Every pattern is a separate named Rule in urlgenie/config.py. Rules are tried in order and the first yielding a non-reserved handle wins, so supporting a new URL shape is one line and touches nothing else:

FACEBOOK_RULES = (
    Rule("fb_query_id",  re.compile(r"[?&](?:id|gid|__user)=(?P<id>\d{5,})", _I)),
    Rule("fb_media_set", re.compile(r"[?&]set=a\.(?:\d+\.)*(?P<id>\d{5,})", _I)),
    ...
)

A rule exposes an id or handle named group; an optional subdir group or attribute sets the canonical path prefix. transform= handles the rare case needing real logic, such as LinkedIn's legacy /pub/ to /in/ slug conversion.

📐 RFC 3986

urlgenie/uri.py implements scheme and host case normalization (§3.1, §3.2.2), IDNA punycoding, default-port removal (§3.2.3), dot-segment resolution (§5.2.4) and percent-encoding normalization (§2.1, §6.2.2.2). The last is idempotent, unlike quote(unquote(url)), which corrupts %2520 and literal %.

⚡ Performance

Roughly 60,000 URLs per second single-threaded (~17s for one million), measured over a corpus of one million distinct URLs. Every function is pure, so multiprocessing.Pool scales it linearly.

python tools/benchmark.py 1000000

🧪 Tests

python -m pytest tests -q      # 287 tests

sample.csv at the repo root is the reference sheet and is treated as the spec, read directly by the test suite: every row is asserted for both the generalized URL and the extracted handle.

📖 Resources

⭐ Love It? Star It!

Just a simple click but would help me out ;)

Metadata

Release files for urlgenie 2.0.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for urlgenie 2.0.4
File Size Uploaded
urlgenie-2.0.4.tar.gz 30.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for urlgenie 2.0.4
File Interpreter ABI Platform
urlgenie-2.0.4-py3-none-any.whl Python 3 none any Details

Total release size: 54.3 kB

Release files / urlgenie-2.0.4.tar.gz

Download URL urlgenie-2.0.4.tar.gz
Size 30.9 kB
Tags Source
SHA-256 checksum
How to use checksums
f8c38f9a8c139e1f2e5da2e4eb5bd7161282eb988ed5b54dee7826b8f588ead3
BLAKE2b-256 checksum
How to use checksums
03db6b5899f1378233e3c6224987f87dc412fdcc98190d0613a3b472af1fcd8f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / urlgenie-2.0.4-py3-none-any.whl

Download URL urlgenie-2.0.4-py3-none-any.whl
Size 23.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e63165a2f80763eda3bd84452615697400a56b6cf9afaf5fd4ee20067348ada8
BLAKE2b-256 checksum
How to use checksums
097670988e6163a122f794c90d61448814e18c55779f5373c42d42521917d75e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

2.1.0

2 release files

2.0.5

2 release files

This release

2.0.4 This release

2 release files

2.0.3

2 release files

2.0.1

2 release files

2.0.0

2 release files

1.2.1

2 release files

1.2.0

1 release file

1.1.5

1 release file

1.1.4

1 release file

1.1.3

1 release file

1.1.2

1 release file

1.1.1

1 release file

1.1.0

1 release file

1.0.0

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page