useragentgen 1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- useragentgen-1.0/.gitignore +5 -0
- useragentgen-1.0/LICENSE +21 -0
- useragentgen-1.0/PKG-INFO +78 -0
- useragentgen-1.0/README.md +53 -0
- useragentgen-1.0/pyproject.toml +55 -0
- useragentgen-1.0/useragentgen/__init__.py +18 -0
- useragentgen-1.0/useragentgen/data/__init__.py +1 -0
- useragentgen-1.0/useragentgen/data/configs.json +79 -0
- useragentgen-1.0/useragentgen/data/engine.txt +14 -0
- useragentgen-1.0/useragentgen/data/exclusions.txt +25 -0
- useragentgen-1.0/useragentgen/data/mobile_token.txt +13 -0
- useragentgen-1.0/useragentgen/data/os.txt +18 -0
- useragentgen-1.0/useragentgen/data/prefix.txt +1 -0
- useragentgen-1.0/useragentgen/data/version.txt +17 -0
- useragentgen-1.0/useragentgen/generator.py +269 -0
- useragentgen-1.0/useragentgen/parser.py +31 -0
useragentgen-1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 [Masrkai]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: useragentgen
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: Random User-Agent string generator with embedded browser templates
|
|
5
|
+
Project-URL: Homepage, https://github.com/Masrkai/useragentgen
|
|
6
|
+
Project-URL: Repository, https://github.com/Masrkai/useragentgen
|
|
7
|
+
Project-URL: Issues, https://github.com/Masrkai/useragentgen/issues
|
|
8
|
+
Author: Masrkai
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: browser,fake-user-agent,http,scraping,testing,user-agent
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# useragentgen
|
|
27
|
+
|
|
28
|
+
Random User-Agent string generator for Chrome, Firefox, Safari, Edge, and Opera.
|
|
29
|
+
All browser templates are **embedded inside the package** — no external files needed after install.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install useragentgen
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import useragentgen
|
|
41
|
+
|
|
42
|
+
# Single UA, random browser
|
|
43
|
+
useragentgen.generate()
|
|
44
|
+
|
|
45
|
+
# Single UA, specific browser
|
|
46
|
+
useragentgen.generate("chrome")
|
|
47
|
+
useragentgen.generate("firefox")
|
|
48
|
+
useragentgen.generate("safari")
|
|
49
|
+
useragentgen.generate("edge")
|
|
50
|
+
useragentgen.generate("opera")
|
|
51
|
+
|
|
52
|
+
# Batch generation
|
|
53
|
+
useragentgen.generate_many(10) # 10 random-browser UAs
|
|
54
|
+
useragentgen.generate_many(5, "chrome") # 5 Chrome UAs
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Reproducible output with a seed:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import useragentgen
|
|
61
|
+
|
|
62
|
+
useragentgen.init(42)
|
|
63
|
+
print(useragentgen.generate("chrome")) # same result every run
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Supported browsers
|
|
67
|
+
|
|
68
|
+
| Key | Covers |
|
|
69
|
+
|------------|-------------------------------|
|
|
70
|
+
| `chrome` | Windows, macOS, Linux, Android |
|
|
71
|
+
| `firefox` | All platforms |
|
|
72
|
+
| `safari` | macOS, iOS, Windows |
|
|
73
|
+
| `edge` | Windows, macOS, Linux, Android |
|
|
74
|
+
| `opera` | Windows, macOS, Linux |
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
[MIT License Copyright (c) 2026 Masrkai, rights reserved](LICENSE)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# useragentgen
|
|
2
|
+
|
|
3
|
+
Random User-Agent string generator for Chrome, Firefox, Safari, Edge, and Opera.
|
|
4
|
+
All browser templates are **embedded inside the package** — no external files needed after install.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install useragentgen
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
import useragentgen
|
|
16
|
+
|
|
17
|
+
# Single UA, random browser
|
|
18
|
+
useragentgen.generate()
|
|
19
|
+
|
|
20
|
+
# Single UA, specific browser
|
|
21
|
+
useragentgen.generate("chrome")
|
|
22
|
+
useragentgen.generate("firefox")
|
|
23
|
+
useragentgen.generate("safari")
|
|
24
|
+
useragentgen.generate("edge")
|
|
25
|
+
useragentgen.generate("opera")
|
|
26
|
+
|
|
27
|
+
# Batch generation
|
|
28
|
+
useragentgen.generate_many(10) # 10 random-browser UAs
|
|
29
|
+
useragentgen.generate_many(5, "chrome") # 5 Chrome UAs
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Reproducible output with a seed:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import useragentgen
|
|
36
|
+
|
|
37
|
+
useragentgen.init(42)
|
|
38
|
+
print(useragentgen.generate("chrome")) # same result every run
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Supported browsers
|
|
42
|
+
|
|
43
|
+
| Key | Covers |
|
|
44
|
+
|------------|-------------------------------|
|
|
45
|
+
| `chrome` | Windows, macOS, Linux, Android |
|
|
46
|
+
| `firefox` | All platforms |
|
|
47
|
+
| `safari` | macOS, iOS, Windows |
|
|
48
|
+
| `edge` | Windows, macOS, Linux, Android |
|
|
49
|
+
| `opera` | Windows, macOS, Linux |
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
[MIT License Copyright (c) 2026 Masrkai, rights reserved](LICENSE)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "useragentgen"
|
|
7
|
+
version = "1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Masrkai" },
|
|
10
|
+
]
|
|
11
|
+
description = "Random User-Agent string generator with embedded browser templates"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
requires-python = ">=3.9"
|
|
15
|
+
keywords = [
|
|
16
|
+
"http",
|
|
17
|
+
"testing",
|
|
18
|
+
"browser",
|
|
19
|
+
"scraping",
|
|
20
|
+
"user-agent",
|
|
21
|
+
"fake-user-agent",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 3 - Alpha",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.9",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
34
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# No third-party runtime deps — stdlib only
|
|
38
|
+
dependencies = []
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/Masrkai/useragentgen"
|
|
42
|
+
Repository = "https://github.com/Masrkai/useragentgen"
|
|
43
|
+
Issues = "https://github.com/Masrkai/useragentgen/issues"
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Hatchling: include data files (.txt templates + configs.json)
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["useragentgen"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.sdist]
|
|
52
|
+
include = ["README.md", "useragentgen/", "pyproject.toml"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
useragentgen
|
|
3
|
+
============
|
|
4
|
+
Random User-Agent string generator with embedded templates.
|
|
5
|
+
|
|
6
|
+
Quick start
|
|
7
|
+
-----------
|
|
8
|
+
>>> from useragentgen import generate, generate_many, init
|
|
9
|
+
>>> init(42) # optional seed for reproducibility
|
|
10
|
+
>>> generate() # random browser
|
|
11
|
+
>>> generate("firefox") # specific browser
|
|
12
|
+
>>> generate_many(5) # list of 5 random UAs
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .generator import generate, generate_many, init
|
|
16
|
+
from .parser import init_parser, parse_line
|
|
17
|
+
|
|
18
|
+
__all__ = ["generate", "generate_many", "init", "parse_line", "init_parser"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# required for importlib.resources
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"chrome": {
|
|
3
|
+
"os_exclude": [
|
|
4
|
+
"ios"
|
|
5
|
+
],
|
|
6
|
+
"engine": "webkit_chrome",
|
|
7
|
+
"version_prefix": "Chrome/",
|
|
8
|
+
"mobile_token": {
|
|
9
|
+
"if": "android",
|
|
10
|
+
"value": "android_chrome"
|
|
11
|
+
},
|
|
12
|
+
"suffix": "Safari/537.36"
|
|
13
|
+
},
|
|
14
|
+
"firefox": {
|
|
15
|
+
"os_allow": "*",
|
|
16
|
+
"engine": "gecko",
|
|
17
|
+
"version_prefix": [
|
|
18
|
+
"Firefox/",
|
|
19
|
+
{
|
|
20
|
+
"prefix": "rv:",
|
|
21
|
+
"os_exclude": [
|
|
22
|
+
"android",
|
|
23
|
+
"ios"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"mobile_token": null,
|
|
28
|
+
"suffix": null
|
|
29
|
+
},
|
|
30
|
+
"safari": {
|
|
31
|
+
"os_allow": [
|
|
32
|
+
"macos",
|
|
33
|
+
"ios",
|
|
34
|
+
"windows"
|
|
35
|
+
],
|
|
36
|
+
"engine": "webkit_safari",
|
|
37
|
+
"version_prefix": "Version/",
|
|
38
|
+
"mobile_token": {
|
|
39
|
+
"if": "ios",
|
|
40
|
+
"value": "ios_safari"
|
|
41
|
+
},
|
|
42
|
+
"suffix": {
|
|
43
|
+
"method": "_safari_suffix"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"edge": {
|
|
47
|
+
"os_exclude": [
|
|
48
|
+
"ios"
|
|
49
|
+
],
|
|
50
|
+
"engine": "webkit_chrome",
|
|
51
|
+
"version_prefix": "Edg/",
|
|
52
|
+
"mobile_token": {
|
|
53
|
+
"if": "android",
|
|
54
|
+
"value": "android_chrome"
|
|
55
|
+
},
|
|
56
|
+
"suffix": {
|
|
57
|
+
"template": "Chrome/{rnd}.0.0.0 Safari/537.36",
|
|
58
|
+
"rnd": [
|
|
59
|
+
115,
|
|
60
|
+
134
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"opera": {
|
|
65
|
+
"os_exclude": [
|
|
66
|
+
"ios"
|
|
67
|
+
],
|
|
68
|
+
"engine": "webkit_chrome",
|
|
69
|
+
"version_prefix": "OPR/",
|
|
70
|
+
"mobile_token": null,
|
|
71
|
+
"suffix": {
|
|
72
|
+
"template": "Chrome/{rnd}.0.0.0 Safari/537.36",
|
|
73
|
+
"rnd": [
|
|
74
|
+
115,
|
|
75
|
+
134
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Engine strings by name
|
|
2
|
+
# Referenced in configs.json as "engine": "name"
|
|
3
|
+
|
|
4
|
+
# Chrome / Edge / Opera
|
|
5
|
+
webkit_chrome = AppleWebKit/537.36 (KHTML, like Gecko)
|
|
6
|
+
|
|
7
|
+
# Safari (desktop & iOS)
|
|
8
|
+
webkit_safari = AppleWebKit/605.1.15 (KHTML, like Gecko)
|
|
9
|
+
|
|
10
|
+
# Firefox (desktop)
|
|
11
|
+
gecko = Gecko/20100101
|
|
12
|
+
|
|
13
|
+
# Firefox (Android variant — version inline, no separate engine token)
|
|
14
|
+
gecko_android = Gecko/{115-122}.0
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Browser-OS exclusion rules
|
|
2
|
+
# Format: browser:os_substring_pattern
|
|
3
|
+
# Lines starting with # are ignored
|
|
4
|
+
|
|
5
|
+
# Safari was discontinued on Windows
|
|
6
|
+
safari:Windows NT
|
|
7
|
+
|
|
8
|
+
# Edge and Opera don't exist on iOS
|
|
9
|
+
edge:iPhone
|
|
10
|
+
edge:iPad
|
|
11
|
+
opera:iPhone
|
|
12
|
+
opera:iPad
|
|
13
|
+
|
|
14
|
+
# Firefox on iOS uses WebKit (different UA format)
|
|
15
|
+
# So exclude from our desktop Firefox generator
|
|
16
|
+
firefox:iPhone
|
|
17
|
+
firefox:iPad
|
|
18
|
+
|
|
19
|
+
# Safari on Linux doesn't exist
|
|
20
|
+
safari:Linux
|
|
21
|
+
|
|
22
|
+
# Chrome on iOS uses WebKit (still valid but different)
|
|
23
|
+
# Keep if you want, or uncomment to exclude:
|
|
24
|
+
# chrome:iPhone
|
|
25
|
+
# chrome:iPad
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Mobile token strings by name
|
|
2
|
+
# Referenced in configs.json as "mobile_token": {"value": "name"}
|
|
3
|
+
|
|
4
|
+
# iOS Safari (iPhone & iPad)
|
|
5
|
+
ios_safari = Mobile/15E148
|
|
6
|
+
|
|
7
|
+
# Chrome on Android
|
|
8
|
+
android_chrome = Mobile Safari/537.36
|
|
9
|
+
|
|
10
|
+
# Safari fallback tokens (desktop & iOS)
|
|
11
|
+
safari_604 = Safari/604.1
|
|
12
|
+
safari_605 = Safari/605.1.15
|
|
13
|
+
safari_537 = Safari/537.36
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Windows
|
|
2
|
+
Windows NT 10.0; Win64; x64
|
|
3
|
+
Windows NT 6.3; Win64; x64
|
|
4
|
+
Windows NT 6.1; Win64; x64
|
|
5
|
+
|
|
6
|
+
# macOS
|
|
7
|
+
Macintosh; Intel Mac OS X {10|11|12|13|14|15}_{0|1|2|3|4|5|6|7}_0
|
|
8
|
+
|
|
9
|
+
# Linux
|
|
10
|
+
X11; {|Ubuntu; |Fedora; |Linux Mint; }Linux {x86_64|i686}
|
|
11
|
+
|
|
12
|
+
# Android
|
|
13
|
+
Linux; Android {11-15}; {Pixel 6|Pixel 7|Pixel 8|Pixel 9|Pixel 10}
|
|
14
|
+
Linux; Android {11-15}; {SM-G991B|SM-G998B|SM-S918B|SM-A546B|SM-F946B}
|
|
15
|
+
|
|
16
|
+
# iOS
|
|
17
|
+
iPhone; CPU iPhone OS {15|16|17|18}_{0|1|2|3|4|5|6} like Mac OS X
|
|
18
|
+
iPad; CPU OS {15|16|17|18}_{0|1|2|3|4|5|6} like Mac OS X
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Mozilla/5.0
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Chrome
|
|
2
|
+
Chrome/{115-134}.0.{5000-6999}.{0-250}
|
|
3
|
+
|
|
4
|
+
# Firefox
|
|
5
|
+
Firefox/{115-135}.0
|
|
6
|
+
|
|
7
|
+
# Firefox (rv tag, desktop only)
|
|
8
|
+
rv:{115-135}.0
|
|
9
|
+
|
|
10
|
+
# Safari
|
|
11
|
+
Version/{15-18}.{0-6}
|
|
12
|
+
|
|
13
|
+
# Edge (Edg token)
|
|
14
|
+
Edg/{115-134}.0.{1000-2500}.{0-150}
|
|
15
|
+
|
|
16
|
+
# Opera (OPR token)
|
|
17
|
+
OPR/{100-117}.0.{4000-5500}.{0-150}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import random
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from . import parser
|
|
7
|
+
|
|
8
|
+
_DATA_PKG = "useragentgen.data"
|
|
9
|
+
|
|
10
|
+
# Module-level state
|
|
11
|
+
_rng = random.Random()
|
|
12
|
+
_data: Dict[str, Any] = {}
|
|
13
|
+
_configs: Dict[str, Any] = {}
|
|
14
|
+
_exclusions: Dict[str, List[str]] = {}
|
|
15
|
+
_engines: Dict[str, str] = {}
|
|
16
|
+
_mobile_tokens: Dict[str, str] = {}
|
|
17
|
+
_initialized = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _read_resource(filename: str) -> str:
|
|
21
|
+
return files(_DATA_PKG).joinpath(filename).read_text(encoding="utf-8")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse_single(filename: str) -> str:
|
|
25
|
+
for raw in _read_resource(filename).splitlines():
|
|
26
|
+
line = raw.strip()
|
|
27
|
+
if line and not line.startswith("#"):
|
|
28
|
+
return parser.parse_line(line)
|
|
29
|
+
return ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _parse_list(filename: str) -> List[str]:
|
|
33
|
+
results = []
|
|
34
|
+
for raw in _read_resource(filename).splitlines():
|
|
35
|
+
line = raw.strip()
|
|
36
|
+
if not line or line.startswith("#"):
|
|
37
|
+
continue
|
|
38
|
+
results.append(parser.parse_line(line))
|
|
39
|
+
return results
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _parse_lookup(filename: str) -> Dict[str, str]:
|
|
43
|
+
results = {}
|
|
44
|
+
for raw in _read_resource(filename).splitlines():
|
|
45
|
+
line = raw.strip()
|
|
46
|
+
if not line or line.startswith("#"):
|
|
47
|
+
continue
|
|
48
|
+
if "=" not in line:
|
|
49
|
+
continue
|
|
50
|
+
key, value = line.split("=", 1)
|
|
51
|
+
results[key.strip()] = parser.parse_line(value.strip())
|
|
52
|
+
return results
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# OS detection helpers (same as before)
|
|
56
|
+
def _is_windows(s: str) -> bool:
|
|
57
|
+
return s.startswith("Windows")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _is_macos(s: str) -> bool:
|
|
61
|
+
return s.startswith("Macintosh")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _is_android(s: str) -> bool:
|
|
65
|
+
return "Android" in s and "iPhone" not in s and "iPad" not in s
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _is_ios(s: str) -> bool:
|
|
69
|
+
return "iPhone" in s or "iPad" in s
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _is_excluded(browser: str, os_str: str) -> bool:
|
|
73
|
+
for pattern in _exclusions.get(browser, []):
|
|
74
|
+
if pattern in os_str:
|
|
75
|
+
return True
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _safari_suffix(os_str: str) -> str:
|
|
80
|
+
if _is_ios(os_str):
|
|
81
|
+
return _rng.choice(["Safari/604.1", "Safari/605.1.15"])
|
|
82
|
+
return "Safari/605.1.15"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Rule interpreters (same logic, using module _rng)
|
|
86
|
+
def _check_os(rule: Dict[str, Any], os_str: str) -> bool:
|
|
87
|
+
if "os_allow" in rule:
|
|
88
|
+
allowed = rule["os_allow"]
|
|
89
|
+
if allowed == "*":
|
|
90
|
+
return True
|
|
91
|
+
return any(globals()[f"_is_{opt}"](os_str) for opt in allowed)
|
|
92
|
+
if "os_exclude" in rule:
|
|
93
|
+
excluded = rule["os_exclude"]
|
|
94
|
+
return not any(globals()[f"_is_{opt}"](os_str) for opt in excluded)
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _check_version(rule: Dict[str, Any], version: str, os_str: str) -> bool:
|
|
99
|
+
"""Evaluate version_prefix rules, with optional OS conditions."""
|
|
100
|
+
prefixes = rule["version_prefix"]
|
|
101
|
+
if isinstance(prefixes, str):
|
|
102
|
+
prefixes = [prefixes]
|
|
103
|
+
|
|
104
|
+
for p in prefixes:
|
|
105
|
+
if isinstance(p, str):
|
|
106
|
+
# Simple string prefix
|
|
107
|
+
if version.startswith(p):
|
|
108
|
+
return True
|
|
109
|
+
else:
|
|
110
|
+
# Dict with OS conditions: {"prefix": "rv:", "os_exclude": ["android"]}
|
|
111
|
+
if not version.startswith(p["prefix"]):
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
# Check OS conditions if present
|
|
115
|
+
excluded = p.get("os_exclude", [])
|
|
116
|
+
if any(globals()[f"_is_{opt}"](os_str) for opt in excluded):
|
|
117
|
+
continue
|
|
118
|
+
allowed = p.get("os_allow", [])
|
|
119
|
+
if allowed: # Only check if explicitly defined
|
|
120
|
+
if not any(globals()[f"_is_{opt}"](os_str) for opt in allowed):
|
|
121
|
+
continue
|
|
122
|
+
return True
|
|
123
|
+
else:
|
|
124
|
+
return True # No restrictions means it's allowed
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _get_mobile_token(rule: Dict[str, Any], os_str: str) -> Optional[str]:
|
|
129
|
+
mobile_cfg = rule.get("mobile_token")
|
|
130
|
+
if not mobile_cfg:
|
|
131
|
+
return None
|
|
132
|
+
condition = mobile_cfg["if"]
|
|
133
|
+
if globals()[f"_is_{condition}"](os_str):
|
|
134
|
+
token_name = mobile_cfg["value"]
|
|
135
|
+
return _mobile_tokens.get(token_name, token_name)
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _get_suffix(rule: Dict[str, Any], version: str, os_str: str) -> Optional[str]:
|
|
140
|
+
suffix_rule = rule.get("suffix")
|
|
141
|
+
if suffix_rule is None:
|
|
142
|
+
return None
|
|
143
|
+
if isinstance(suffix_rule, str):
|
|
144
|
+
return suffix_rule
|
|
145
|
+
if "method" in suffix_rule:
|
|
146
|
+
return globals()[suffix_rule["method"]](os_str)
|
|
147
|
+
if "template" in suffix_rule:
|
|
148
|
+
template = suffix_rule["template"]
|
|
149
|
+
result = template
|
|
150
|
+
for key, val in suffix_rule.items():
|
|
151
|
+
if key == "template":
|
|
152
|
+
continue
|
|
153
|
+
if isinstance(val, list) and len(val) == 2:
|
|
154
|
+
result = result.replace(f"{{{key}}}", str(_rng.randint(val[0], val[1])))
|
|
155
|
+
return result
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def init(seed: Optional[int] = None) -> None:
|
|
160
|
+
"""Initialize or reinitialize the generator with an optional seed."""
|
|
161
|
+
global _initialized, _rng, _data, _configs, _exclusions, _engines, _mobile_tokens
|
|
162
|
+
|
|
163
|
+
_rng = random.Random(seed)
|
|
164
|
+
parser.init_parser(seed) # Sync parser RNG
|
|
165
|
+
|
|
166
|
+
# Core data
|
|
167
|
+
_data = {
|
|
168
|
+
"prefix": _parse_single("prefix.txt"),
|
|
169
|
+
"os_list": _parse_list("os.txt"),
|
|
170
|
+
"versions": _parse_list("version.txt"),
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# Browser configs from JSON
|
|
174
|
+
_configs = json.loads(_read_resource("configs.json"))
|
|
175
|
+
|
|
176
|
+
# Engine and mobile token lookups
|
|
177
|
+
_engines = _parse_lookup("engine.txt")
|
|
178
|
+
_mobile_tokens = _parse_lookup("mobile_token.txt")
|
|
179
|
+
|
|
180
|
+
# Exclusion rules
|
|
181
|
+
browsers = ("chrome", "firefox", "safari", "edge", "opera")
|
|
182
|
+
_exclusions = {b: [] for b in browsers}
|
|
183
|
+
try:
|
|
184
|
+
content = _read_resource("exclusions.txt")
|
|
185
|
+
for line in content.splitlines():
|
|
186
|
+
line = line.strip()
|
|
187
|
+
if not line or line.startswith("#") or ":" not in line:
|
|
188
|
+
continue
|
|
189
|
+
browser, pattern = line.split(":", 1)
|
|
190
|
+
browser = browser.strip().lower()
|
|
191
|
+
if browser in _exclusions:
|
|
192
|
+
_exclusions[browser].append(pattern.strip())
|
|
193
|
+
except FileNotFoundError:
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
_initialized = True
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _ensure_init() -> None:
|
|
200
|
+
if not _initialized:
|
|
201
|
+
init()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def generate(browser: Optional[str] = None) -> str:
|
|
205
|
+
"""Generate a single User-Agent string."""
|
|
206
|
+
_ensure_init()
|
|
207
|
+
|
|
208
|
+
browsers = ("chrome", "firefox", "safari", "edge", "opera")
|
|
209
|
+
if browser is None:
|
|
210
|
+
browser = _rng.choice(browsers)
|
|
211
|
+
else:
|
|
212
|
+
browser = browser.lower()
|
|
213
|
+
|
|
214
|
+
cfg = _configs.get(browser)
|
|
215
|
+
if not cfg:
|
|
216
|
+
raise ValueError(f"Unknown browser: {browser!r}. Choose from: {browsers}")
|
|
217
|
+
|
|
218
|
+
valid_os = [
|
|
219
|
+
os_str
|
|
220
|
+
for os_str in _data["os_list"]
|
|
221
|
+
if _check_os(cfg, os_str) and not _is_excluded(browser, os_str)
|
|
222
|
+
]
|
|
223
|
+
if not valid_os:
|
|
224
|
+
raise ValueError(f"No valid OS found for browser: {browser!r}")
|
|
225
|
+
os_str = _rng.choice(valid_os)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# In generate() function, after selecting OS:
|
|
230
|
+
if browser == "firefox":
|
|
231
|
+
# Firefox needs both rv: (in parens) and Firefox/ (after engine)
|
|
232
|
+
version_num = _rng.randint(115, 135)
|
|
233
|
+
rv_version = f"rv:{version_num}.0"
|
|
234
|
+
fx_version = f"Firefox/{version_num}.0"
|
|
235
|
+
|
|
236
|
+
# Insert rv: into OS parentheses
|
|
237
|
+
os_str = os_str.rstrip(')') + f"; {rv_version})"
|
|
238
|
+
engine = _engines.get(cfg["engine"], cfg["engine"])
|
|
239
|
+
parts = [_data["prefix"], f"({os_str}", engine, fx_version]
|
|
240
|
+
else:
|
|
241
|
+
# valid_versions = [v for v in _data["versions"] if _check_version(cfg, v)]
|
|
242
|
+
valid_versions = [v for v in _data["versions"] if _check_version(cfg, v, os_str)]
|
|
243
|
+
|
|
244
|
+
if not valid_versions:
|
|
245
|
+
raise ValueError(f"No valid version tokens found for browser: {browser!r}")
|
|
246
|
+
version = _rng.choice(valid_versions)
|
|
247
|
+
|
|
248
|
+
parts = [_data["prefix"], f"({os_str})"]
|
|
249
|
+
|
|
250
|
+
if cfg.get("engine"):
|
|
251
|
+
engine = _engines.get(cfg["engine"], cfg["engine"])
|
|
252
|
+
parts.append(engine)
|
|
253
|
+
|
|
254
|
+
mobile = _get_mobile_token(cfg, os_str)
|
|
255
|
+
if mobile:
|
|
256
|
+
parts.append(mobile)
|
|
257
|
+
|
|
258
|
+
parts.append(version)
|
|
259
|
+
|
|
260
|
+
suffix = _get_suffix(cfg, version, os_str)
|
|
261
|
+
if suffix:
|
|
262
|
+
parts.append(suffix)
|
|
263
|
+
|
|
264
|
+
return " ".join(parts)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def generate_many(count: int = 10, browser: Optional[str] = None) -> List[str]:
|
|
268
|
+
"""Generate multiple User-Agent strings."""
|
|
269
|
+
return [generate(browser) for _ in range(count)]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import random
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
# Module-level RNG (can be seeded via init_parser)
|
|
6
|
+
_rng: random.Random = random.Random()
|
|
7
|
+
|
|
8
|
+
CHOICE_PATTERN = re.compile(r"\{([^}]+)\}")
|
|
9
|
+
RANGE_PATTERN = re.compile(r"^(\d+)-(\d+)$")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def init_parser(seed: Optional[int] = None) -> None:
|
|
13
|
+
"""Initialize or reseed the parser's RNG."""
|
|
14
|
+
global _rng
|
|
15
|
+
_rng = random.Random(seed)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_line(line: str) -> str:
|
|
19
|
+
"""Expand all {a|b} and {1-10} patterns in a template string."""
|
|
20
|
+
|
|
21
|
+
def replace_match(match: re.Match) -> str:
|
|
22
|
+
content = match.group(1)
|
|
23
|
+
range_match = RANGE_PATTERN.match(content)
|
|
24
|
+
if range_match:
|
|
25
|
+
start, end = int(range_match.group(1)), int(range_match.group(2))
|
|
26
|
+
return str(_rng.randint(start, end))
|
|
27
|
+
if "|" in content:
|
|
28
|
+
return _rng.choice(content.split("|"))
|
|
29
|
+
return content
|
|
30
|
+
|
|
31
|
+
return CHOICE_PATTERN.sub(replace_match, line)
|