js-vars-extractor 0.1.6__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.
- js_vars_extractor-0.1.6/PKG-INFO +57 -0
- js_vars_extractor-0.1.6/README.md +46 -0
- js_vars_extractor-0.1.6/pyproject.toml +110 -0
- js_vars_extractor-0.1.6/src/js_vars_extractor/__init__.py +1 -0
- js_vars_extractor-0.1.6/src/js_vars_extractor/_js_vars.py +124 -0
- js_vars_extractor-0.1.6/src/js_vars_extractor/py.typed +0 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: js-vars-extractor
|
|
3
|
+
Version: 0.1.6
|
|
4
|
+
Summary: Extract JavaScript variables from HTML
|
|
5
|
+
Author: Avazart
|
|
6
|
+
Requires-Dist: chompjs>=1.4.1
|
|
7
|
+
Requires-Dist: esprima>=4.0.1
|
|
8
|
+
Requires-Dist: selectolax>=0.4.11
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# av-js-vars-extractor
|
|
13
|
+
|
|
14
|
+
A lightweight Python library designed to extract JavaScript variables, objects, and configurations from `<script>` tags within HTML documents
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Using [`uv`](https://github.com/astral-sh/uv):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv add av-js-vars-extractor
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Quick Start
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from selectolax.lexbor import LexborHTMLParser
|
|
28
|
+
from av_js_vars_extractor import find_js_var
|
|
29
|
+
|
|
30
|
+
html = """
|
|
31
|
+
<html>
|
|
32
|
+
<head>
|
|
33
|
+
<script>
|
|
34
|
+
window.APP_CONFIG = {
|
|
35
|
+
env: "production",
|
|
36
|
+
apiUrl: "[https://api.example.com](https://api.example.com)",
|
|
37
|
+
features: ["auth", "payments"]
|
|
38
|
+
};
|
|
39
|
+
</script>
|
|
40
|
+
</head>
|
|
41
|
+
</html>
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
parser = LexborHTMLParser(html)
|
|
45
|
+
config = find_js_var("window.APP_CONFIG", parser)
|
|
46
|
+
print(config)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
# Output:
|
|
51
|
+
{
|
|
52
|
+
'env': 'production',
|
|
53
|
+
'apiUrl': '[https://api.example.com](https://api.example.com)',
|
|
54
|
+
'features': ['auth', 'payments']
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# av-js-vars-extractor
|
|
2
|
+
|
|
3
|
+
A lightweight Python library designed to extract JavaScript variables, objects, and configurations from `<script>` tags within HTML documents
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Using [`uv`](https://github.com/astral-sh/uv):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv add av-js-vars-extractor
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from selectolax.lexbor import LexborHTMLParser
|
|
17
|
+
from av_js_vars_extractor import find_js_var
|
|
18
|
+
|
|
19
|
+
html = """
|
|
20
|
+
<html>
|
|
21
|
+
<head>
|
|
22
|
+
<script>
|
|
23
|
+
window.APP_CONFIG = {
|
|
24
|
+
env: "production",
|
|
25
|
+
apiUrl: "[https://api.example.com](https://api.example.com)",
|
|
26
|
+
features: ["auth", "payments"]
|
|
27
|
+
};
|
|
28
|
+
</script>
|
|
29
|
+
</head>
|
|
30
|
+
</html>
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
parser = LexborHTMLParser(html)
|
|
34
|
+
config = find_js_var("window.APP_CONFIG", parser)
|
|
35
|
+
print(config)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
# Output:
|
|
40
|
+
{
|
|
41
|
+
'env': 'production',
|
|
42
|
+
'apiUrl': '[https://api.example.com](https://api.example.com)',
|
|
43
|
+
'features': ['auth', 'payments']
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "js_vars_extractor"
|
|
3
|
+
version = "0.1.6"
|
|
4
|
+
description = "Extract JavaScript variables from HTML"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Avazart"}
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"chompjs>=1.4.1",
|
|
12
|
+
"esprima>=4.0.1",
|
|
13
|
+
"selectolax>=0.4.11",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build>=0.8.21,<0.9.0"]
|
|
18
|
+
build-backend = "uv_build"
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"mypy>=2.3.1",
|
|
23
|
+
"pre-commit>=4.6.2",
|
|
24
|
+
"pytest>=9.1.1",
|
|
25
|
+
"ruff>=0.16.6",
|
|
26
|
+
"vulture>=2.16",
|
|
27
|
+
]
|
|
28
|
+
[tool.ruff]
|
|
29
|
+
exclude = [
|
|
30
|
+
".git",
|
|
31
|
+
"__pycache__",
|
|
32
|
+
".pytest_cache",
|
|
33
|
+
".mypy_cache",
|
|
34
|
+
".ruff_cache",
|
|
35
|
+
"venv",
|
|
36
|
+
".venv",
|
|
37
|
+
".env",
|
|
38
|
+
".idea",
|
|
39
|
+
".vscode",
|
|
40
|
+
]
|
|
41
|
+
line-length = 79
|
|
42
|
+
target-version = "py311"
|
|
43
|
+
|
|
44
|
+
[tool.ruff.format]
|
|
45
|
+
quote-style = "double"
|
|
46
|
+
skip-magic-trailing-comma = false
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint]
|
|
49
|
+
select = [
|
|
50
|
+
# pycodestyle
|
|
51
|
+
#"E",
|
|
52
|
+
# Pyflakes
|
|
53
|
+
"F",
|
|
54
|
+
# pyupgrade
|
|
55
|
+
"UP",
|
|
56
|
+
# flake8-bugbear
|
|
57
|
+
"B",
|
|
58
|
+
# flake8-simplify
|
|
59
|
+
#"SIM",
|
|
60
|
+
# isort
|
|
61
|
+
"I",
|
|
62
|
+
]
|
|
63
|
+
ignore = []
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint.extend-per-file-ignores]
|
|
66
|
+
"__init__.py" = ["F401"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
[tool.mypy]
|
|
70
|
+
strict = false
|
|
71
|
+
exclude = [
|
|
72
|
+
"^venv/.*$",
|
|
73
|
+
"^migrations/.*$",
|
|
74
|
+
"^tests/.*$",
|
|
75
|
+
"^scripts/.*$",
|
|
76
|
+
"^research/.*$",
|
|
77
|
+
]
|
|
78
|
+
plugins = []
|
|
79
|
+
|
|
80
|
+
#[[tool.mypy.overrides]]
|
|
81
|
+
# module = "aiogram.*,apscheduler.*,gspread.*,oauth2client.*"
|
|
82
|
+
# ignore_missing_imports = true
|
|
83
|
+
|
|
84
|
+
[tool.pytest.ini_options]
|
|
85
|
+
testpaths = [
|
|
86
|
+
"tests",
|
|
87
|
+
]
|
|
88
|
+
python_files = "test_*.py"
|
|
89
|
+
pythonpath = ["."]
|
|
90
|
+
log_cli = true
|
|
91
|
+
log_cli_level = "DEBUG"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
[tool.pylint.messages_control]
|
|
95
|
+
disable = [
|
|
96
|
+
"C0114",
|
|
97
|
+
"C0115",
|
|
98
|
+
"C0116",
|
|
99
|
+
"R0903",
|
|
100
|
+
"R0913",
|
|
101
|
+
"R0914",
|
|
102
|
+
"R0917",
|
|
103
|
+
]
|
|
104
|
+
[tool.pylint.master]
|
|
105
|
+
ignore = [
|
|
106
|
+
".venv",
|
|
107
|
+
"migrations",
|
|
108
|
+
"tests",
|
|
109
|
+
"scripts",
|
|
110
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from ._js_vars import extract_js_vars, find_js_var, js_vars_iter
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import chompjs
|
|
9
|
+
import esprima
|
|
10
|
+
from esprima import Error
|
|
11
|
+
from esprima.nodes import ExpressionStatement, Node, VariableDeclaration
|
|
12
|
+
from selectolax.lexbor import LexborHTMLParser
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(Path(__name__).parent.name)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StatementType(enum.StrEnum):
|
|
18
|
+
EXPRESSION_STATEMENT = "ExpressionStatement"
|
|
19
|
+
VARIABLE_DECLARATION = "VariableDeclaration"
|
|
20
|
+
FUNCTION_DECLARATION = "FunctionDeclaration"
|
|
21
|
+
CLASS_DECLARATION = "ClassDeclaration"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ExpressionType(enum.StrEnum):
|
|
25
|
+
LITERAL = "Literal"
|
|
26
|
+
OBJECT_EXPRESSION = "ObjectExpression"
|
|
27
|
+
ARRAY_EXPRESSION = "ArrayExpression"
|
|
28
|
+
|
|
29
|
+
MEMBER_EXPRESSION = "MemberExpression" # window.CONFIG, obj['prop']
|
|
30
|
+
UNARY_EXPRESSION = "UnaryExpression" # -100, !true, void 0
|
|
31
|
+
BINARY_EXPRESSION = "BinaryExpression" # "a" + "b", 10 + 20
|
|
32
|
+
IDENTIFIER = "Identifier" # undefined, NaN, configVar
|
|
33
|
+
CALL_EXPRESSION = "CallExpression" # JSON.parse("...")
|
|
34
|
+
TEMPLATE_LITERAL = "TemplateLiteral" # `template_string`
|
|
35
|
+
LOGICAL_EXPRESSION = "LogicalExpression" # a || b
|
|
36
|
+
CONDITIONAL_EXPRESSION = "ConditionalExpression" # a ? b : c
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _extract_var_name(left: Node) -> str | None:
|
|
40
|
+
if left.type == ExpressionType.MEMBER_EXPRESSION:
|
|
41
|
+
obj_name = (
|
|
42
|
+
left.object.name
|
|
43
|
+
if left.object.type == ExpressionType.IDENTIFIER
|
|
44
|
+
else ""
|
|
45
|
+
)
|
|
46
|
+
prop_name = (
|
|
47
|
+
left.property.name
|
|
48
|
+
if left.property.type == ExpressionType.IDENTIFIER
|
|
49
|
+
else ""
|
|
50
|
+
)
|
|
51
|
+
return f"{obj_name}.{prop_name}" if obj_name else prop_name
|
|
52
|
+
elif left.type == ExpressionType.IDENTIFIER:
|
|
53
|
+
return left.name
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _extract_var_value(
|
|
58
|
+
expr,
|
|
59
|
+
script_text: str,
|
|
60
|
+
) -> int | float | str | dict | list | None:
|
|
61
|
+
if expr.type == ExpressionType.LITERAL:
|
|
62
|
+
if isinstance(expr.value, str):
|
|
63
|
+
try:
|
|
64
|
+
return json.loads(expr.value)
|
|
65
|
+
except json.JSONDecodeError:
|
|
66
|
+
pass
|
|
67
|
+
return expr.value
|
|
68
|
+
elif expr.type in (
|
|
69
|
+
ExpressionType.OBJECT_EXPRESSION,
|
|
70
|
+
ExpressionType.ARRAY_EXPRESSION,
|
|
71
|
+
):
|
|
72
|
+
text = script_text[expr.range[0] : expr.range[1]] # noqa: E203
|
|
73
|
+
value = chompjs.parse_js_object(text)
|
|
74
|
+
return value
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _parse_assignment(
|
|
79
|
+
statement: ExpressionStatement,
|
|
80
|
+
scrtipt_text: str,
|
|
81
|
+
data: dict[str, object],
|
|
82
|
+
) -> None:
|
|
83
|
+
expr = statement.expression
|
|
84
|
+
if expr.left and (var_name := _extract_var_name(expr.left)):
|
|
85
|
+
if expr.right.type == ExpressionType.LITERAL:
|
|
86
|
+
data[var_name] = _extract_var_value(expr.right, scrtipt_text)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _parse_variable_declaration(
|
|
90
|
+
statement: VariableDeclaration,
|
|
91
|
+
script_text: str,
|
|
92
|
+
data: dict[str, object],
|
|
93
|
+
) -> None:
|
|
94
|
+
for decl in statement.declarations:
|
|
95
|
+
if decl.init:
|
|
96
|
+
data[decl.id.name] = _extract_var_value(decl.init, script_text)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def extract_js_vars(script_text: str) -> dict[str, object]:
|
|
100
|
+
data: dict[str, object] = {}
|
|
101
|
+
ast = esprima.parseScript(script_text, {"tolerant": True, "range": True})
|
|
102
|
+
for statement in ast.body:
|
|
103
|
+
if statement.type == StatementType.EXPRESSION_STATEMENT:
|
|
104
|
+
_parse_assignment(statement, script_text, data)
|
|
105
|
+
elif statement.type == StatementType.VARIABLE_DECLARATION:
|
|
106
|
+
_parse_variable_declaration(statement, script_text, data)
|
|
107
|
+
return data
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def js_vars_iter(parser: LexborHTMLParser) -> Iterator[dict[str, object]]:
|
|
111
|
+
for script_el in parser.css("script:not([type='application/ld+json'])"):
|
|
112
|
+
if text := script_el.text():
|
|
113
|
+
try:
|
|
114
|
+
if script_data := extract_js_vars(text):
|
|
115
|
+
yield script_data
|
|
116
|
+
except Error as e:
|
|
117
|
+
logger.debug("%s %s", type(e), e)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def find_js_var(var_name: str, parser: LexborHTMLParser) -> Any | None:
|
|
121
|
+
for variables in js_vars_iter(parser):
|
|
122
|
+
if var_name in variables:
|
|
123
|
+
return variables[var_name]
|
|
124
|
+
return None
|
|
File without changes
|