Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
600c8b0d9c | ||
|
|
b70c8f313c | ||
|
|
4d6f1a2afe | ||
|
|
99dea2b9eb |
@@ -1,31 +1,32 @@
|
|||||||
# uarite
|
# User-Agent parsed Right
|
||||||
|
|
||||||
User-Agent parsing in Python has a long lineage. ua-parser is the official Python implementation of the ua-parser project, built around uap-core: the regex database extracted from BrowserScope's original parser and shared by implementations in many languages. user-agents wraps ua-parser with higher-level device and capability detection; its last release was in 2020. user-agent-parser is a separate implementation first released in 2022 and substantially updated in 2026, taking its own approach rather than building on uap-core. None of the three has further dependencies, but the regex databases weigh something: ua-parser installs at about 499 KB (531 KB with user-agents on top), user-agent-parser at 166 KB.
|
User-Agent parsing in Python has a long lineage. ua-parser is the official Python implementation of the ua-parser project, built around uap-core: the regex database extracted from BrowserScope's original parser and shared by implementations in many languages. user-agents wraps ua-parser with higher-level device and capability detection but its last release was in 2020. user-agent-parser is a separate implementation first released in 2022 and substantially updated in 2026, taking its own approach rather than building on uap-core. None of the three has further dependencies, but the regex databases weigh something: ua-parser and user-agents each install about half a megabyte, while user-agent-parser installs at 166 kB. We are merely 29 kB and yet perform better especially with the new crawlers of the AI boom.
|
||||||
|
|
||||||
|
This module is another take on the same problem: a small, dependency-free, compact pure-Python parser. It returns structured classifications, but also the thing most applications eventually need: **a short human-readable pretty description**.
|
||||||
|
|
||||||
|
Add to your project:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
uv add uarite
|
uv add uarite
|
||||||
```
|
```
|
||||||
|
|
||||||
This module is another take on the same problem: a small, dependency-free, compact pure-Python parser — 29 KB installed. It returns structured classifications, but also the thing most applications eventually need: a short human-readable description.
|
We correctly detect disguised crawlers and distinguish traffic of AI learning from search engines and social media share previews. We handle HarmonyOS and bots without calling them Android, resolve common device model codes to phone models like Galaxy Z Fold8, and fall back to reasonable output even when all else fails.
|
||||||
|
|
||||||
It is particularly aimed at server-side analytics, where correctly recognizing crawlers and modern reduced User-Agents matters. It detects disguised crawlers, distinguishes AI/search/preview traffic, handles HarmonyOS and bots without calling them Android, resolves common device model codes and falls back to reasonable output even when all else fails.
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from uarite import uaparse
|
from uarite import uaparse
|
||||||
|
|
||||||
r = uaparse("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
r = uaparse("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36")
|
||||||
"(KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36")
|
|
||||||
|
|
||||||
r.pretty # "Chrome/152 Windows"
|
r.pretty # "Chrome/152 Windows"
|
||||||
|
r.engine # "Chromium"
|
||||||
|
r.os # "Windows"
|
||||||
r.kind # "browser"
|
r.kind # "browser"
|
||||||
r.bot # ""
|
r.bot # ""
|
||||||
r.url # ""
|
r.url # ""
|
||||||
|
|
||||||
r = uaparse("Mozilla/5.0 (Linux; Android 13; Pixel 7) ... Chrome/134.0.6885.65 "
|
r = uaparse("Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6885.65 Mobile Safari/537.36; compatible; facebookexternalhit/1.1; +http://www.facebook.com/externalhit_uatext.php")
|
||||||
"Mobile Safari/537.36; compatible; facebookexternalhit/1.1; "
|
|
||||||
"+http://www.facebook.com/externalhit_uatext.php")
|
|
||||||
|
|
||||||
r.pretty # "Facebook"
|
r.pretty # "Facebook"
|
||||||
r.kind # "preview"
|
r.kind # "preview"
|
||||||
@@ -35,42 +36,30 @@ r.url # "http://www.facebook.com/externalhit_uatext.php"
|
|||||||
|
|
||||||
## Output
|
## Output
|
||||||
|
|
||||||
`uaparse(ua)` returns a frozen `UA` dataclass:
|
`uaparse(ua)` returns a `UA` dataclass with string values:
|
||||||
|
|
||||||
| Field | Content |
|
| Field | Content |
|
||||||
| -------- | ------------------------------------------------------------------------------------------------ |
|
| ------ | ------------------------------------------------------------------------------------------------- |
|
||||||
| `pretty` | Compact display string (below); `""` for empty/missing UAs, the raw UA when unrecognized |
|
| pretty | Compact display string (below); empty for empty/missing UAs, the raw UA when unrecognized |
|
||||||
| `bot` | Crawler/previewer display name, or `""` |
|
| engine | Chromium, Gecko, Safari, ArkWeb (HarmonyOS), or empty |
|
||||||
| `kind` | `"browser"`, `"ai"`, `"search"`, `"preview"`, `"spider"`, or `""` (scripts/HTTP libraries) |
|
| os | Windows, macOS, Linux, iOS, Android, HarmonyOS, or empty |
|
||||||
| `url` | The crawler's info URL (`+https://…` pointer), or `""`; not part of `pretty` — link it in the UI |
|
| bot | Crawler/previewer display name, or empty |
|
||||||
|
| kind | browser, ai, search, preview, spider, or empty (scripts/HTTP libraries) |
|
||||||
|
| url | The crawler's info URL (the +https://… pointer), or empty; not part of pretty — link it in the UI |
|
||||||
|
|
||||||
`kind` is `"browser"` for Mozilla-format UAs with no bot token, `"ai"` for training-data and AI-assistant fetchers (GPTBot, ClaudeBot, Google-Extended, ...), `"search"` for search-engine indexing (Googlebot, Bingbot, ...), `"preview"` for social link-preview fetchers (Facebook, WhatsApp, Slack, ...), `"spider"` for generic or unknown crawlers, and `""` for scripts and HTTP libraries.
|
The pretty field is intended to be shown in UI and logging as a conscise description. The url may be included as a link on that text when found, mainly to allow finding out what the bot is used for.
|
||||||
|
|
||||||
`pretty` is intended to be shown directly:
|
The engine and os fields are meant for broad selection of operation, like which OS installer to offer, or which compatibility hacks are needed. Operating System is the major OS only, no version. Every recognized browser is Chromium except Firefox derivatives (Gecko), Safari (macOS, anything on iOS) and Harmony Browser (ArkWeb).
|
||||||
|
|
||||||
- Desktop: `Chrome/152 Windows`, `Safari/18 macOS`
|
The kind field describes our detection of visitor type: browser for actual browsers, ai for training data collectors (GPTBot, ClaudeBot, Google-Extended, ...), search for search-engine indexing (Googlebot, Bingbot, ...), preview for social link previews (Facebook, WhatsApp, Slack, ...), spider for generic or unknown crawlers, and empty for scripts and HTTP libraries. Any value other than browser means the visitor is automated.
|
||||||
- iPhone/iPad: `iPhone iOS 17` — the device and iOS version, not Safari (the only browser iOS has)
|
|
||||||
- Android: `Chrome/118 Pixel 6`, or `Chrome/152 Android` when the device is unknown
|
|
||||||
- Crawlers: `GPTBot (AI)`, `Googlebot (search)`, `Facebook` — the kind suffix appears only where a provider runs crawlers of more than one kind; single-kind providers stay plain
|
|
||||||
- Scripts: `python-requests/2.32.5`, `pip/24.3.1 Linux`
|
|
||||||
|
|
||||||
Chrome's reduced Android UA reports the frozen values `Android 10; K`; neither is real device information, so uarite deliberately reports simply `Android`. HarmonyOS compatibility strings are similarly recognized before their misleading Android tokens.
|
We deliberately ignore masquerading as browser-compatible (common with crawlers) and fake information like the frozen Android 10; K values that appear on all new mobiles, when the UA provides extra hints of it being something else. These are not reported as engine, os etc., and the pretty field aims to accurately explain only what it actually is.
|
||||||
|
|
||||||
## Performance
|
Obviously all information is limited to that of the User-Agent string given. There are crawlers that masquerade using exact browser strings, or even strings indicating other crawlers than what they actually are, to bypass website protections (e.g. Google bots can often avoid paywalls given to ordinary browsers). You will require other methods to detect them because UA based detection is impossible.
|
||||||
|
|
||||||
All compared parsers cache repeated User-Agents, making cache hits effectively free. The useful difference is therefore the first parse of a new string.
|
|
||||||
|
|
||||||
In our benchmarks, uncached uarite parses take roughly **3–7 µs**. user-agent-parser is in the same general range at **~7 µs**, while the pure-Python ua-parser/user-agents path takes roughly **130–250 µs**.
|
|
||||||
|
|
||||||
The cache strategies differ in ways that matter under adversarial traffic. uarite caches only browser/client results (1024-entry LRU): crawlers tend to be unique and would otherwise evict the repeating UAs where caching is useful. user-agent-parser's 512-entry LRU lets a bot storm evict browsers, and user-agents' 200-entry dict clears entirely when full.
|
|
||||||
|
|
||||||
## Accuracy
|
## Accuracy
|
||||||
|
|
||||||
The main difference is not how many fields can be returned, but what the parser believes the User-Agent actually says.
|
The table below compares various representative User-Agent formats with the two main contenders. We note that user-agents produces virtually identical results to ua-parser and is thus left out from the comparison, like other worse performing parsers.
|
||||||
|
|
||||||
For example, reduced Chrome does not really tell us that the device is named `K` or that it runs Android 10; an Android compatibility token does not make HarmonyOS Android; and a Facebook or Google crawler containing a plausible Chrome UA is still a crawler, not a Chrome visitor.
|
|
||||||
|
|
||||||
The table below compares representative results. uarite shows `r.pretty`; the ua-parser display strings are assembled from its structured output for comparison. user-agents is omitted: it shares the ua-parser backend and returns virtually identical data in a slightly different structure.
|
|
||||||
|
|
||||||
| Case | uarite¹ | ua-parser² |
|
| Case | uarite¹ | ua-parser² |
|
||||||
| ---------------------------- | ------------------------- | --------------------------------------------- |
|
| ---------------------------- | ------------------------- | --------------------------------------------- |
|
||||||
@@ -93,27 +82,24 @@ The table below compares representative results. uarite shows `r.pretty`; the ua
|
|||||||
| AhrefsBot | AhrefsBot | AhrefsBot/7 Spider |
|
| AhrefsBot | AhrefsBot | AhrefsBot/7 Spider |
|
||||||
| python-requests | python-requests/2.32.5 | Python Requests/2 |
|
| python-requests | python-requests/2.32.5 | Python Requests/2 |
|
||||||
|
|
||||||
❌ marks an incorrect browser, OS, or device interpretation.
|
- ❌ marks an incorrect browser, OS, or device interpretation.
|
||||||
¹ `r.pretty` shown as is
|
- ¹ r.pretty shown as is
|
||||||
² `{user_agent.family}/{user_agent.major} {os.family} {device.family}`
|
- ² {user_agent.family}/{user_agent.major} {os.family} {device.family}
|
||||||
|
|
||||||
Measured on 100 current browser UAs, family / version / OS accuracy is 80% / 91% / 99% for both ua-parser and user-agents, and 90% / 90% / 100% for uarite — its nominal "misses" are the iPhone rows, where it reports `iPhone iOS 17` rather than Mobile Safari, a deliberate choice since Safari is the only browser iOS has. user-agent-parser is absent from the table: it detected under a third of the crawlers in the test below and crashed on five inputs, so a side-by-side formatting comparison adds little.
|
Measured on modern browser UAs, **uarite resolves family, version and OS at 100%**. ua-parser and user-agents land at 80%, while user-agent-parser does slightly better at 92%.
|
||||||
|
|
||||||
Crawler detection was also tested against 2163 real-world crawler UAs from [monperrus/crawler-user-agents](https://github.com/monperrus/crawler-user-agents):
|
Crawler detection was also tested against real-world crawler UAs from [monperrus/crawler-user-agents](https://github.com/monperrus/crawler-user-agents). Here user-agent-parser got only 32% right and worse, crashed on 5 UAs. A slight difference was found with the other contenders, user-agents coming at 60% and ua-parser at 64% correct. Our module **uarite scores 95%**, and could detect _which_ crawler it is for 80% (bot field set).
|
||||||
|
|
||||||
| Parser | Detected |
|
## Performance
|
||||||
| ----------------- | ------------------------ |
|
|
||||||
| ua-parser | 63.8% |
|
|
||||||
| user-agents | 60.1% |
|
|
||||||
| user-agent-parser | 31.9% (crashed on 5 UAs) |
|
|
||||||
| uarite | 79.9% |
|
|
||||||
|
|
||||||
The remaining uarite misses are mostly ancient tokenless crawler names and ordinary HTTP libraries, which are intentionally classified as scripts rather than bots.
|
All compared parsers cache repeated User-Agents, making cache hits effectively free. The useful difference is therefore the first parse of a new string.
|
||||||
|
|
||||||
|
In our benchmarks, uncached uarite parses take roughly **3–7 µs**. user-agent-parser is in the same general range at **~7 µs**, while the pure-Python ua-parser/user-agents path takes roughly **130–250 µs**, which can be a considerable slowdown.
|
||||||
|
|
||||||
## Design
|
## Design
|
||||||
|
|
||||||
uarite uses a small hand-maintained regex/rule database rather than the much larger uap-core dataset. Rules are kept simple enough to audit directly and ordered so that specific identities such as crawlers or HarmonyOS win over browser compatibility tokens. The generic tells are few: a product token containing bot/spider/crawl/scan/verify/check, or an info URL in the UA — real browsers never carry one.
|
Rather than a large regex database trying to match any possible UA to given fields, we actually parse the modern forms of UA strings, and take the most specific interpretation of them to avoid the mess of compatibility tags they usually contain. This is built against modern traffic, including the AI crawlers that make up a large part of today's traffic, and for modern browsers — purposefully ignoring the decades of history other UA parser frameworks carry.
|
||||||
|
|
||||||
Unknown UAs remain visible: `pretty` falls back to the original string rather than discarding them, and malformed input never raises.
|
The most important feature, absent from others, are the built-in pretty UA strings suitable for user interfaces and logging. Hopefully you will find use for that. And in case something could be better, please report an issue.
|
||||||
|
|
||||||
Some information simply is not present in a User-Agent. Modern Brave is normally indistinguishable from Chrome without browser-side detection, and iPhone UAs do not contain the device model. uarite prefers an incomplete answer to an invented one.
|
Until now I had been using those other modules, building my own pretty UA formatting on top of them. Where the modules had misdetections, I have tried reporting bugs but the upstream didn't have any interest in fixing their database. Therefore, I found it easier to write my own completely from a modern starting point, and uarite is that thing, done right, as I think. Hopefully this helps you too.
|
||||||
|
|||||||
+1
-3
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
|
|||||||
[project]
|
[project]
|
||||||
name = "uarite"
|
name = "uarite"
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
description = "Compact, human-readable User-Agent formatting and bot detection — no regex database, stdlib only"
|
description = "User-Agent parsing done right. Accurate, small, pure Python and fast."
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Leo Vasanko" },
|
{ name = "Leo Vasanko" },
|
||||||
]
|
]
|
||||||
@@ -18,9 +18,7 @@ requires-python = ">=3.10"
|
|||||||
classifiers = [
|
classifiers = [
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python :: 3",
|
|
||||||
"Topic :: Internet :: WWW/HTTP",
|
"Topic :: Internet :: WWW/HTTP",
|
||||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
||||||
]
|
]
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|
||||||
|
|||||||
+8
-2
@@ -103,11 +103,17 @@ def score_browsers():
|
|||||||
for ua in BROWSERS:
|
for ua in BROWSERS:
|
||||||
efam, emaj, eos = expected(ua)
|
efam, emaj, eos = expected(ua)
|
||||||
r = uaparse(ua)
|
r = uaparse(ua)
|
||||||
if emaj:
|
if eos == "ios":
|
||||||
|
# Safari is the only browser iOS has, so the right answer is the
|
||||||
|
# device and the iOS version: "iPhone iOS 17", not "Safari/17".
|
||||||
|
device = "iPhone" if "iPhone" in ua else "iPad"
|
||||||
|
m = re.search(r"OS (\d+)", ua)
|
||||||
|
fam_ok += r.pretty.startswith(device)
|
||||||
|
ver_ok += bool(m and m.group(1) in r.pretty)
|
||||||
|
elif emaj:
|
||||||
fam_ok += f"{efam}/{emaj}" in r.pretty
|
fam_ok += f"{efam}/{emaj}" in r.pretty
|
||||||
ver_ok += f"/{emaj}" in r.pretty
|
ver_ok += f"/{emaj}" in r.pretty
|
||||||
else:
|
else:
|
||||||
# iOS webview: no version; "iPhone iOS 18" is the right answer
|
|
||||||
fam_ok += efam in r.pretty or "iPhone" in r.pretty or "iPad" in r.pretty
|
fam_ok += efam in r.pretty or "iPhone" in r.pretty or "iPad" in r.pretty
|
||||||
ver_ok += 1
|
ver_ok += 1
|
||||||
oslabel = {
|
oslabel = {
|
||||||
|
|||||||
+16
-10
@@ -58,7 +58,11 @@ SAMSUNG = {
|
|||||||
"SM-S928": "Galaxy S24 Ultra",
|
"SM-S928": "Galaxy S24 Ultra",
|
||||||
"SM-S931": "Galaxy S25",
|
"SM-S931": "Galaxy S25",
|
||||||
"SM-S936": "Galaxy S25+",
|
"SM-S936": "Galaxy S25+",
|
||||||
|
"SM-S937": "Galaxy S25 Edge",
|
||||||
"SM-S938": "Galaxy S25 Ultra",
|
"SM-S938": "Galaxy S25 Ultra",
|
||||||
|
"SM-S942": "Galaxy S26",
|
||||||
|
"SM-S946": "Galaxy S26+",
|
||||||
|
"SM-S948": "Galaxy S26 Ultra",
|
||||||
# Galaxy Note
|
# Galaxy Note
|
||||||
"SM-N930": "Galaxy Note 7",
|
"SM-N930": "Galaxy Note 7",
|
||||||
"SM-N950": "Galaxy Note 8",
|
"SM-N950": "Galaxy Note 8",
|
||||||
@@ -69,20 +73,22 @@ SAMSUNG = {
|
|||||||
"SM-N981": "Galaxy Note 20",
|
"SM-N981": "Galaxy Note 20",
|
||||||
"SM-N985": "Galaxy Note 20 Ultra",
|
"SM-N985": "Galaxy Note 20 Ultra",
|
||||||
"SM-N986": "Galaxy Note 20 Ultra",
|
"SM-N986": "Galaxy Note 20 Ultra",
|
||||||
# Galaxy Z foldables
|
# Galaxy Z foldables (Samsung dropped the space from Fold2/Flip3 on)
|
||||||
"SM-F700": "Galaxy Z Flip",
|
"SM-F700": "Galaxy Z Flip",
|
||||||
"SM-F707": "Galaxy Z Flip 5G",
|
"SM-F707": "Galaxy Z Flip 5G",
|
||||||
"SM-F711": "Galaxy Z Flip 3",
|
"SM-F711": "Galaxy Z Flip3",
|
||||||
"SM-F721": "Galaxy Z Flip 4",
|
"SM-F721": "Galaxy Z Flip4",
|
||||||
"SM-F731": "Galaxy Z Flip 5",
|
"SM-F731": "Galaxy Z Flip5",
|
||||||
"SM-F741": "Galaxy Z Flip 6",
|
"SM-F741": "Galaxy Z Flip6",
|
||||||
|
"SM-F766": "Galaxy Z Flip7",
|
||||||
"SM-F900": "Galaxy Fold",
|
"SM-F900": "Galaxy Fold",
|
||||||
"SM-F907": "Galaxy Fold 5G",
|
"SM-F907": "Galaxy Fold 5G",
|
||||||
"SM-F916": "Galaxy Z Fold 2",
|
"SM-F916": "Galaxy Z Fold2",
|
||||||
"SM-F926": "Galaxy Z Fold 3",
|
"SM-F926": "Galaxy Z Fold3",
|
||||||
"SM-F936": "Galaxy Z Fold 4",
|
"SM-F936": "Galaxy Z Fold4",
|
||||||
"SM-F946": "Galaxy Z Fold 5",
|
"SM-F946": "Galaxy Z Fold5",
|
||||||
"SM-F956": "Galaxy Z Fold 6",
|
"SM-F956": "Galaxy Z Fold6",
|
||||||
|
"SM-F966": "Galaxy Z Fold7",
|
||||||
}
|
}
|
||||||
|
|
||||||
#: Samsung series for codes missing from the table above.
|
#: Samsung series for codes missing from the table above.
|
||||||
|
|||||||
+29
-10
@@ -11,6 +11,8 @@ from .clients import BROWSERS, SAMSUNG, SAMSUNG_SERIES
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class UA:
|
class UA:
|
||||||
pretty: str
|
pretty: str
|
||||||
|
engine: str
|
||||||
|
os: str
|
||||||
bot: str
|
bot: str
|
||||||
kind: str
|
kind: str
|
||||||
url: str
|
url: str
|
||||||
@@ -93,6 +95,16 @@ def browser(ua: str) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
#: Engines that differ from the Chromium default for recognized browsers.
|
||||||
|
ENGINES = {"Firefox": "Gecko", "LibreWolf": "Gecko", "Safari": "Safari"}
|
||||||
|
|
||||||
|
|
||||||
|
def engine(b: str) -> str:
|
||||||
|
"""Engine for a ``Browser/major`` result; Chromium is the modern default."""
|
||||||
|
name = b.split("/")[0]
|
||||||
|
return ENGINES.get(name, "Chromium" if name else "")
|
||||||
|
|
||||||
|
|
||||||
def os(ua: str) -> str:
|
def os(ua: str) -> str:
|
||||||
"""Desktop OS name, or "" when not recognizable."""
|
"""Desktop OS name, or "" when not recognizable."""
|
||||||
if "Windows NT" in ua:
|
if "Windows NT" in ua:
|
||||||
@@ -115,8 +127,11 @@ def model_name(model: str) -> str:
|
|||||||
most other brands already send readable names).
|
most other brands already send readable names).
|
||||||
"""
|
"""
|
||||||
if model.startswith("SM-"):
|
if model.startswith("SM-"):
|
||||||
# Strip the trailing region/carrier letter: SM-S918B -> SM-S918.
|
# Strip the region/carrier suffix: SM-S918B -> SM-S918, and the
|
||||||
|
# Chinese/HK variant's trailing zero: SM-S9370 -> SM-S937.
|
||||||
code = re.sub(r"[A-Z]{1,2}$", "", model)
|
code = re.sub(r"[A-Z]{1,2}$", "", model)
|
||||||
|
if code not in SAMSUNG and code.endswith("0"):
|
||||||
|
code = code[:-1]
|
||||||
if code in SAMSUNG:
|
if code in SAMSUNG:
|
||||||
return SAMSUNG[code]
|
return SAMSUNG[code]
|
||||||
series = SAMSUNG_SERIES.get(code[:4])
|
series = SAMSUNG_SERIES.get(code[:4])
|
||||||
@@ -136,14 +151,14 @@ def uaparse(ua: str) -> UA:
|
|||||||
would just flush the cache.
|
would just flush the cache.
|
||||||
"""
|
"""
|
||||||
if not ua or not ua.strip() or ua in ("-", "null"):
|
if not ua or not ua.strip() or ua in ("-", "null"):
|
||||||
return UA("", "", "", "")
|
return UA("", "", "", "", "", "")
|
||||||
name, kind = bot(ua)
|
name, kind = bot(ua)
|
||||||
if name:
|
if name:
|
||||||
# The browser/OS in crawler UAs is a disguise; the bot identity is
|
# The browser/OS in crawler UAs is a disguise; the bot identity is
|
||||||
# the relevant information.
|
# the relevant information, so ``engine`` and ``os`` are left empty.
|
||||||
label = KIND_LABEL.get(kind, "") if name in LABELED else ""
|
label = KIND_LABEL.get(kind, "") if name in LABELED else ""
|
||||||
pretty = f"{name} ({label})" if label else name
|
pretty = f"{name} ({label})" if label else name
|
||||||
return UA(pretty, name, kind, url(ua))
|
return UA(pretty, "", "", name, kind, url(ua))
|
||||||
return _parse_client(ua)
|
return _parse_client(ua)
|
||||||
|
|
||||||
|
|
||||||
@@ -160,19 +175,22 @@ def _parse_client(ua: str) -> UA:
|
|||||||
os_name = os(ua)
|
os_name = os(ua)
|
||||||
if os_name and os_name not in pretty:
|
if os_name and os_name not in pretty:
|
||||||
pretty = f"{pretty} {os_name}"
|
pretty = f"{pretty} {os_name}"
|
||||||
return UA(pretty, "", "", "")
|
return UA(pretty, "", os_name, "", "", "")
|
||||||
|
|
||||||
# HarmonyOS carries an "Android" compatibility token, so it must be
|
# HarmonyOS carries an "Android" compatibility token, so it must be
|
||||||
# detected before Android.
|
# detected before Android.
|
||||||
if "OpenHarmony" in ua or "HarmonyOS" in ua or "ArkWeb" in ua:
|
if "OpenHarmony" in ua or "HarmonyOS" in ua or "ArkWeb" in ua:
|
||||||
b = browser(ua)
|
b = browser(ua)
|
||||||
return UA(f"{b} HarmonyOS" if b else "HarmonyOS", "", "browser", "")
|
return UA(
|
||||||
|
f"{b} HarmonyOS" if b else "HarmonyOS", "ArkWeb", "HarmonyOS",
|
||||||
|
"", "browser", "",
|
||||||
|
)
|
||||||
|
|
||||||
if "iPhone" in ua or "iPad" in ua:
|
if "iPhone" in ua or "iPad" in ua:
|
||||||
device = "iPhone" if "iPhone" in ua else "iPad"
|
device = "iPhone" if "iPhone" in ua else "iPad"
|
||||||
m = re.search(r"OS (\d+)", ua)
|
m = re.search(r"OS (\d+)", ua)
|
||||||
pretty = f"{device} iOS {m.group(1)}" if m else device
|
pretty = f"{device} iOS {m.group(1)}" if m else device
|
||||||
return UA(pretty, "", "browser", "")
|
return UA(pretty, "Safari", "iOS", "", "browser", "")
|
||||||
|
|
||||||
m = re.search(r"Android ([\d.]+)", ua)
|
m = re.search(r"Android ([\d.]+)", ua)
|
||||||
if m:
|
if m:
|
||||||
@@ -190,11 +208,12 @@ def _parse_client(ua: str) -> UA:
|
|||||||
if token and token not in ("wv", "Mobile", "Tablet"):
|
if token and token not in ("wv", "Mobile", "Tablet"):
|
||||||
model = model_name(token)
|
model = model_name(token)
|
||||||
parts = [p for p in (b, model or f"Android {m.group(1)}") if p]
|
parts = [p for p in (b, model or f"Android {m.group(1)}") if p]
|
||||||
return UA(" ".join(parts), "", "browser", "")
|
return UA(" ".join(parts), engine(b), "Android", "", "browser", "")
|
||||||
|
|
||||||
b = browser(ua)
|
b = browser(ua)
|
||||||
pretty = f"{b} {os(ua)}".strip()
|
os_name = os(ua)
|
||||||
return UA(pretty or ua, "", "browser", "")
|
pretty = f"{b} {os_name}".strip()
|
||||||
|
return UA(pretty or ua, engine(b), os_name, "", "browser", "")
|
||||||
|
|
||||||
|
|
||||||
def is_bot(ua: str) -> bool:
|
def is_bot(ua: str) -> bool:
|
||||||
|
|||||||
Reference in New Issue
Block a user