"""Lightweight browser/OS classification for human traffic (Chapter 08). Separate from Chapter 07's bot_identifier — that identifies automated crawlers via UA substring + DNS verification. This classifies ordinary browsers/OSes, same ordered-substring-match pattern, signatures as JSON data (not hardcoded), no third-party UA-parsing dependency — consistent with the low-footprint bias in Chapter 03. Order matters within each signature list: e.g. Edge/Opera must be checked before Chrome (their UAs also contain "Chrome/"); iOS before macOS (iPhone UAs also contain "like Mac OS X"); Android before Linux (Android UAs also contain "Linux"). """ from __future__ import annotations import json from dataclasses import dataclass from pathlib import Path _DATA_DIR = Path(__file__).parent / "data" @dataclass(frozen=True) class UASignature: name: str ua_substrings: tuple[str, ...] def _load(filename: str) -> list[UASignature]: raw = json.loads((_DATA_DIR / filename).read_text()) return [UASignature(name=e["name"], ua_substrings=tuple(e["ua_substrings"])) for e in raw] _BROWSER_SIGNATURES = _load("browser_signatures.json") _OS_SIGNATURES = _load("os_signatures.json") def _match(user_agent: str, signatures: list[UASignature], default: str) -> str: for sig in signatures: if any(sub in user_agent for sub in sig.ua_substrings): return sig.name return default def classify_browser(user_agent: str | None) -> str: if not user_agent: return "Unknown" return _match(user_agent, _BROWSER_SIGNATURES, "Other") def classify_os(user_agent: str | None) -> str: if not user_agent: return "Unknown" return _match(user_agent, _OS_SIGNATURES, "Other")