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.
@@ -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