ssc_codegen 0.6.1__tar.gz → 0.7.1__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.
- ssc_codegen-0.7.1/PKG-INFO +238 -0
- ssc_codegen-0.7.1/README.md +208 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/pyproject.toml +25 -7
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/__init__.py +9 -9
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/ast_build_utils.py +20 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/cli/cli_utils.py +13 -0
- ssc_codegen-0.7.1/ssc_codegen/cli/code_callbacks.py +38 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/cli/consts.py +7 -3
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/cli/main.py +227 -5
- ssc_codegen-0.7.1/ssc_codegen/cli/runtime_parse_runners.py +72 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/compiler.py +4 -1
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/converters/dart_universal_html.py +8 -7
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/converters/js_pure.py +4 -2
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/converters/json_to_schema.py +1 -1
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/converters/py_parsel.py +6 -4
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/converters/py_selectolax.py +1 -1
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/converters/templates/dart.py +2 -1
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/converters/templates/js.py +14 -26
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/document.py +25 -14
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/document_utlis.py +12 -5
- ssc_codegen-0.7.1/ssc_codegen/str_utils.py +182 -0
- ssc_codegen-0.6.1/.idea/.gitignore +0 -8
- ssc_codegen-0.6.1/.idea/dbnavigator.xml +0 -404
- ssc_codegen-0.6.1/.idea/discord.xml +0 -7
- ssc_codegen-0.6.1/.idea/inspectionProfiles/profiles_settings.xml +0 -6
- ssc_codegen-0.6.1/.idea/libraries/Dart_SDK.xml +0 -29
- ssc_codegen-0.6.1/.idea/misc.xml +0 -7
- ssc_codegen-0.6.1/.idea/modules.xml +0 -8
- ssc_codegen-0.6.1/.idea/poetry.xml +0 -10
- ssc_codegen-0.6.1/.idea/selector_schema_codegen.iml +0 -28
- ssc_codegen-0.6.1/.idea/vcs.xml +0 -6
- ssc_codegen-0.6.1/.idea/workspace.xml +0 -759
- ssc_codegen-0.6.1/PKG-INFO +0 -223
- ssc_codegen-0.6.1/README.md +0 -207
- ssc_codegen-0.6.1/docs/index.md +0 -13
- ssc_codegen-0.6.1/examples/booksToScrape/README.md +0 -39
- ssc_codegen-0.6.1/examples/booksToScrape/dart/booksToScrape.dart +0 -279
- ssc_codegen-0.6.1/examples/booksToScrape/dart/main.dart +0 -19
- ssc_codegen-0.6.1/examples/booksToScrape/generate_code.sh +0 -5
- ssc_codegen-0.6.1/examples/booksToScrape/go/booksToScrape.go +0 -326
- ssc_codegen-0.6.1/examples/booksToScrape/go/main.go +0 -41
- ssc_codegen-0.6.1/examples/booksToScrape/js/booksToScrape.js +0 -173
- ssc_codegen-0.6.1/examples/booksToScrape/js/main.js +0 -7
- ssc_codegen-0.6.1/examples/booksToScrape/manual_make.py +0 -33
- ssc_codegen-0.6.1/examples/booksToScrape/python/booksToScrape.py +0 -243
- ssc_codegen-0.6.1/examples/booksToScrape/python/main.py +0 -10
- ssc_codegen-0.6.1/examples/booksToScrape/python/main2.py +0 -7
- ssc_codegen-0.6.1/examples/booksToScrape/runtime_compile.py +0 -16
- ssc_codegen-0.6.1/examples/booksToScrape/schemas/booksToScrape.py +0 -65
- ssc_codegen-0.6.1/examples/quotesToScrapeJs/index.html +0 -193
- ssc_codegen-0.6.1/examples/quotesToScrapeJs/main.py +0 -72
- ssc_codegen-0.6.1/examples/quotesToScrapeJs/parser_schema.py +0 -75
- ssc_codegen-0.6.1/examples/quotesToScrapeJs/schema.py +0 -27
- ssc_codegen-0.6.1/examples/tutorial/1_basic/index.html +0 -30
- ssc_codegen-0.6.1/examples/tutorial/1_basic/main.py +0 -16
- ssc_codegen-0.6.1/examples/tutorial/1_basic/parser_schema.py +0 -91
- ssc_codegen-0.6.1/examples/tutorial/1_basic/schema.py +0 -33
- ssc_codegen-0.6.1/examples/tutorial/2_string_operations/index.html +0 -17
- ssc_codegen-0.6.1/examples/tutorial/2_string_operations/main.py +0 -26
- ssc_codegen-0.6.1/examples/tutorial/2_string_operations/parser_schema.py +0 -164
- ssc_codegen-0.6.1/examples/tutorial/2_string_operations/schema.py +0 -35
- ssc_codegen-0.6.1/examples/tutorial/3_default_values/index.html +0 -10
- ssc_codegen-0.6.1/examples/tutorial/3_default_values/main.py +0 -16
- ssc_codegen-0.6.1/examples/tutorial/3_default_values/parser_schema.py +0 -86
- ssc_codegen-0.6.1/examples/tutorial/3_default_values/schema.py +0 -24
- ssc_codegen-0.6.1/examples/tutorial/4_validation/bad_index.html +0 -10
- ssc_codegen-0.6.1/examples/tutorial/4_validation/index.html +0 -10
- ssc_codegen-0.6.1/examples/tutorial/4_validation/main.py +0 -18
- ssc_codegen-0.6.1/examples/tutorial/4_validation/parser_schema.py +0 -69
- ssc_codegen-0.6.1/examples/tutorial/4_validation/schema.py +0 -33
- ssc_codegen-0.6.1/examples/tutorial/5_schema_types/index.html +0 -39
- ssc_codegen-0.6.1/examples/tutorial/5_schema_types/main.py +0 -19
- ssc_codegen-0.6.1/examples/tutorial/5_schema_types/parser_schema.py +0 -170
- ssc_codegen-0.6.1/examples/tutorial/5_schema_types/schema.py +0 -41
- ssc_codegen-0.6.1/examples/tutorial/6_nested_schemas/index.html +0 -33
- ssc_codegen-0.6.1/examples/tutorial/6_nested_schemas/main.py +0 -70
- ssc_codegen-0.6.1/examples/tutorial/6_nested_schemas/parser_schema.py +0 -345
- ssc_codegen-0.6.1/examples/tutorial/6_nested_schemas/schema.py +0 -46
- ssc_codegen-0.6.1/examples/tutorial/7_json_extract/index.html +0 -15
- ssc_codegen-0.6.1/examples/tutorial/7_json_extract/main.py +0 -25
- ssc_codegen-0.6.1/examples/tutorial/7_json_extract/parser_schema.py +0 -47
- ssc_codegen-0.6.1/examples/tutorial/7_json_extract/raw_json.json +0 -1
- ssc_codegen-0.6.1/examples/tutorial/7_json_extract/schema.py +0 -23
- ssc_codegen-0.6.1/examples/tutorial/7_json_extract/template_json_schema.py +0 -10
- ssc_codegen-0.6.1/examples/tutorial/readme.md +0 -27
- ssc_codegen-0.6.1/main.py +0 -5
- ssc_codegen-0.6.1/scripts/check.sh +0 -15
- ssc_codegen-0.6.1/scripts/fix.sh +0 -12
- ssc_codegen-0.6.1/scripts/ver_patch.sh +0 -10
- ssc_codegen-0.6.1/scripts/version.py +0 -82
- ssc_codegen-0.6.1/ssc_codegen/str_utils.py +0 -81
- ssc_codegen-0.6.1/tests/converter_tools/test_str_utils.py +0 -83
- ssc_codegen-0.6.1/tests/converter_tools/test_variable.py +0 -32
- ssc_codegen-0.6.1/tests/converters/helpers_converters.py +0 -58
- ssc_codegen-0.6.1/tests/converters/test_buildin_converters.py +0 -127
- ssc_codegen-0.6.1/tests/converters/test_json_converter.py +0 -16
- ssc_codegen-0.6.1/tests/document/test_document_array.py +0 -33
- ssc_codegen-0.6.1/tests/document/test_document_assert.py +0 -25
- ssc_codegen-0.6.1/tests/document/test_document_invalid_expr.py +0 -47
- ssc_codegen-0.6.1/tests/document/test_document_regex.py +0 -32
- ssc_codegen-0.6.1/tests/document/test_document_select.py +0 -46
- ssc_codegen-0.6.1/tests/document/test_document_text.py +0 -30
- ssc_codegen-0.6.1/tests/document/test_nested.py +0 -33
- ssc_codegen-0.6.1/tests/document/test_selector_converters.py +0 -133
- ssc_codegen-0.6.1/tests/schema/helpers.py +0 -34
- ssc_codegen-0.6.1/tests/schema/test_default_expr.py +0 -25
- ssc_codegen-0.6.1/tests/schema/test_sc_magic_methods.py +0 -174
- ssc_codegen-0.6.1/tests/schema/test_sc_mro.py +0 -31
- ssc_codegen-0.6.1/tests/utils/test_convert_query.py +0 -24
- ssc_codegen-0.6.1/tests/utils/test_go_unimport.py +0 -146
- ssc_codegen-0.6.1/tests/utils/test_validate_query.py +0 -43
- ssc_codegen-0.6.1/uv.lock +0 -597
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/.gitignore +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/LICENSE +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/_compat.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/ast_builder.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/ast_ssc.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/cli/__init__.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/cli/cli_callbacks.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/consts.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/converters/__init__.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/converters/ast_utils.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/converters/base.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/converters/go_goquery.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/converters/py_base.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/converters/py_bs4.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/converters/py_scrapy.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/converters/templates/go.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/converters/templates/py.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/converters/templates/template_bindings.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/json_struct.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/schema.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/selector_utils.py +0 -0
- {ssc_codegen-0.6.1 → ssc_codegen-0.7.1}/ssc_codegen/tokens.py +0 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ssc_codegen
|
|
3
|
+
Version: 0.7.1
|
|
4
|
+
Summary: Python-dsl code converter to html parser for web scraping
|
|
5
|
+
Project-URL: Documentation, https://github.com/vypivshiy/selector_schema_codegen#readme
|
|
6
|
+
Project-URL: Issues, https://github.com/vypivshiy/selector_schema_codegen/issues
|
|
7
|
+
Project-URL: Source, https://github.com/vypivshiy/selector_schema_codegen
|
|
8
|
+
Project-URL: Examples, https://github.com/vypivshiy/selector_schema_codegen/examples
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
19
|
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: cssselect>=1.2.0
|
|
23
|
+
Requires-Dist: httpx>=0.28.1
|
|
24
|
+
Requires-Dist: ichrome>=4.0.4
|
|
25
|
+
Requires-Dist: lxml>=5.3.0
|
|
26
|
+
Requires-Dist: parsel>=1.10.0
|
|
27
|
+
Requires-Dist: typer>=0.15.1
|
|
28
|
+
Requires-Dist: typing-extensions; python_version < '3.11'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Selector Schema codegen
|
|
32
|
+
|
|
33
|
+
## Introduction
|
|
34
|
+
|
|
35
|
+
ssc-gen - based-python DSL language for writing html parsers in dataclass style for converting to targeting language.
|
|
36
|
+
|
|
37
|
+
Project solving next problems:
|
|
38
|
+
|
|
39
|
+
- boilerplate code
|
|
40
|
+
- create types (type annotations) and documentation
|
|
41
|
+
- simplify code support
|
|
42
|
+
- portability to other languages
|
|
43
|
+
|
|
44
|
+
## Support converters
|
|
45
|
+
|
|
46
|
+
Current support converters
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
| Language | Library (html parser backend) | XPath Support | CSS Support | Generated types | Code formatter |
|
|
50
|
+
|---------------|--------------------------------------------------------------|---------------|-------------|------------------------------------------|----------------|
|
|
51
|
+
| Python (3.8+) | bs4 | N | Y | TypedDict*, list, dict | ruff |
|
|
52
|
+
| ... | parsel | Y | Y | ... | - |
|
|
53
|
+
| ... | selectolax (modest) | N | Y | ... | - |
|
|
54
|
+
| ... | scrapy (possibly use parsel - pass Response.selector object) | Y | Y | ... | - |
|
|
55
|
+
| Dart (3) | universal_html | N | Y | record, List, Map | dart format |
|
|
56
|
+
| js (ES6) | pure (firefox/chrome) | Y | Y | Array, Map** | - |
|
|
57
|
+
| go (1.10+) | goquery | N | Y | struct(json anchors include), array, map | gofmt |
|
|
58
|
+
|
|
59
|
+
- *this annotation type was deliberately chosen as a compromise reasons.
|
|
60
|
+
Python has many ways of serialization: `dataclass, namedtuple, attrs, pydantic`
|
|
61
|
+
- TypedDict is like a build-in dict, but with IDE and linter hint support, and you can easily implement an adapter for the required structure.
|
|
62
|
+
- **js not exists build-in serialization methods
|
|
63
|
+
|
|
64
|
+
### Limitations
|
|
65
|
+
|
|
66
|
+
For maximum portability of the configuration to the target language:
|
|
67
|
+
|
|
68
|
+
- Use CSS selectors: they are guaranteed to be converted to XPATH
|
|
69
|
+
- Unlike javascript, most html parse libs implement [CSS3 selectors standard](https://www.w3.org/TR/selectors-3/)
|
|
70
|
+
- basic selectors: (`tag`, `.class`, `#id`)
|
|
71
|
+
- combined: (`div p`, `ul > li`, `h2 +p`\[1])
|
|
72
|
+
- attribute: (`a[href]`, `input[type='text']`)\[2]
|
|
73
|
+
- pseudo classes: (`:nth-child(n)`, `:first-child`, `:last-child`)\[3]
|
|
74
|
+
- **often, not support more complex, dynamic styles**: (`:has()`, `:nth-of-type()`, `:where()`, `:is()`)
|
|
75
|
+
|
|
76
|
+
1. Several libs not support `+` operations (eg: [selectolax(modest)](https://github.com/rushter/selectolax), [dart.universal_html](https://pub.dev/packages/universal_html))
|
|
77
|
+
2. Often, web scraping libs not supports attribute operations like `*=`, `~=`, `|=`, `^=` and `$=`
|
|
78
|
+
3. Several libs not support pseudo classes (eg: standard [dart.html](https://dart.dev/libraries/dart-html) lib miss this feature).
|
|
79
|
+
This project will not implement converters with such a cons
|
|
80
|
+
|
|
81
|
+
## Getting started
|
|
82
|
+
|
|
83
|
+
ssc_gen required python 3.10 version or higher
|
|
84
|
+
|
|
85
|
+
### Install
|
|
86
|
+
|
|
87
|
+
pip:
|
|
88
|
+
|
|
89
|
+
```shell
|
|
90
|
+
pip install ssc_codegen
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
uv:
|
|
94
|
+
|
|
95
|
+
```shell
|
|
96
|
+
uv pip install ssc_codegen
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
as cli converter tool:
|
|
100
|
+
|
|
101
|
+
| package manager | command |
|
|
102
|
+
|-----------------|-------------------------------|
|
|
103
|
+
| pipx | `pipx install ssc_codegen` |
|
|
104
|
+
| uv | `uv tool install ssc_codegen` |
|
|
105
|
+
|
|
106
|
+
## Example
|
|
107
|
+
|
|
108
|
+
### Create a file `schema.py` with:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from ssc_codegen import ItemSchema, D
|
|
112
|
+
|
|
113
|
+
class HelloWorld(ItemSchema):
|
|
114
|
+
title = D().css('title').text()
|
|
115
|
+
a_hrefs = D().css_all('a').attr('href')
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### try it in cli
|
|
119
|
+
|
|
120
|
+
>[!note]
|
|
121
|
+
> this tools developed for testing purposes, not for web-scraping
|
|
122
|
+
|
|
123
|
+
### from file
|
|
124
|
+
|
|
125
|
+
>[!warning]
|
|
126
|
+
> DO NOT PASS CONFIGS FROM UNKNOWN SOURCES:
|
|
127
|
+
>
|
|
128
|
+
> PYTHON CODE FROM CONFIGS COMPILE IN RUNTIME WOUT SECURITY CHECKS!!!
|
|
129
|
+
|
|
130
|
+
Download any html file and pass as argument:
|
|
131
|
+
|
|
132
|
+
```shell
|
|
133
|
+
ssc-gen parse-from-file index.html -t schema.py:HelloWorld
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Short options descriptions:
|
|
137
|
+
|
|
138
|
+
- `-t --target` - config schema file and class from where to start the parser
|
|
139
|
+
|
|
140
|
+

|
|
141
|
+
|
|
142
|
+
### from url
|
|
143
|
+
|
|
144
|
+
```shell
|
|
145
|
+
ssc-gen parse-from-url https://example.com -t schema.py:HelloWorld
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+

|
|
149
|
+
### from Chromium browser (CDP protocol)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
```shell
|
|
153
|
+
ssc-gen parse-from-chrome https://example.com -t schema.py:HelloWorld
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
>[!note]
|
|
157
|
+
> if script cannot found chrome executable - provide it manually:
|
|
158
|
+
|
|
159
|
+
```shell
|
|
160
|
+
ssc-gen parse-from-chrome https://example.com -t schema.py:HelloWorld -sc /usr/bin/chromium
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
### Convert to code
|
|
165
|
+
|
|
166
|
+
Convert to code for use in projects:
|
|
167
|
+
|
|
168
|
+
>![note]
|
|
169
|
+
> for example, used js: it can be fast test in developer console
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
```shell
|
|
173
|
+
ssc-gen js schema.py -o .
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Code output looks like this (code formatted by IDE):
|
|
177
|
+
|
|
178
|
+
```javascript
|
|
179
|
+
// autogenerated by ssc-gen DO NOT_EDIT
|
|
180
|
+
/**
|
|
181
|
+
*
|
|
182
|
+
*
|
|
183
|
+
* {
|
|
184
|
+
* "title": "String",
|
|
185
|
+
* "a_hrefs": "Array<String>"
|
|
186
|
+
* }
|
|
187
|
+
*/
|
|
188
|
+
class HelloWorld {
|
|
189
|
+
constructor(doc) {
|
|
190
|
+
if (typeof doc === 'string') {
|
|
191
|
+
this._doc = new DOMParser().parseFromString(doc, 'text/html');
|
|
192
|
+
} else if (doc instanceof Document || doc instanceof Element) {
|
|
193
|
+
this._doc = doc;
|
|
194
|
+
} else {
|
|
195
|
+
throw new Error("Invalid input: Expected a Document, Element, or string");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
_parseTitle(value) {
|
|
200
|
+
let value1 = value.querySelector('title');
|
|
201
|
+
return typeof value1.textContent === "undefined" ? value1.documentElement.textContent : value1.textContent;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
_parseAHrefs(value) {
|
|
205
|
+
let value1 = Array.from(value.querySelectorAll('a'));
|
|
206
|
+
return value1.map(e => e.getAttribute('href'));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
parse() {
|
|
210
|
+
return {title: this._parseTitle(this._doc), a_hrefs: this._parseAHrefs(this._doc)};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### copy code output and past to developer console:
|
|
216
|
+
|
|
217
|
+
Print output:
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
alert(JSON.stringify((new HelloWorld(document).parse())))
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+

|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
You can use any html source:
|
|
227
|
+
|
|
228
|
+
- read from html file
|
|
229
|
+
- get from http request
|
|
230
|
+
- get from browser (playwright, selenium, chrome-cdp)
|
|
231
|
+
- paste code to developer console (js)
|
|
232
|
+
- or call curl in shell and parse stdin
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
## See also
|
|
236
|
+
- [Brief](docs/brief.md) about css selectors and regular expressions.
|
|
237
|
+
- [Tutorial](docs/tutorial.md) how to use ssc-gen
|
|
238
|
+
- [Reference](docs/reference.md) about high-level API
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Selector Schema codegen
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
ssc-gen - based-python DSL language for writing html parsers in dataclass style for converting to targeting language.
|
|
6
|
+
|
|
7
|
+
Project solving next problems:
|
|
8
|
+
|
|
9
|
+
- boilerplate code
|
|
10
|
+
- create types (type annotations) and documentation
|
|
11
|
+
- simplify code support
|
|
12
|
+
- portability to other languages
|
|
13
|
+
|
|
14
|
+
## Support converters
|
|
15
|
+
|
|
16
|
+
Current support converters
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
| Language | Library (html parser backend) | XPath Support | CSS Support | Generated types | Code formatter |
|
|
20
|
+
|---------------|--------------------------------------------------------------|---------------|-------------|------------------------------------------|----------------|
|
|
21
|
+
| Python (3.8+) | bs4 | N | Y | TypedDict*, list, dict | ruff |
|
|
22
|
+
| ... | parsel | Y | Y | ... | - |
|
|
23
|
+
| ... | selectolax (modest) | N | Y | ... | - |
|
|
24
|
+
| ... | scrapy (possibly use parsel - pass Response.selector object) | Y | Y | ... | - |
|
|
25
|
+
| Dart (3) | universal_html | N | Y | record, List, Map | dart format |
|
|
26
|
+
| js (ES6) | pure (firefox/chrome) | Y | Y | Array, Map** | - |
|
|
27
|
+
| go (1.10+) | goquery | N | Y | struct(json anchors include), array, map | gofmt |
|
|
28
|
+
|
|
29
|
+
- *this annotation type was deliberately chosen as a compromise reasons.
|
|
30
|
+
Python has many ways of serialization: `dataclass, namedtuple, attrs, pydantic`
|
|
31
|
+
- TypedDict is like a build-in dict, but with IDE and linter hint support, and you can easily implement an adapter for the required structure.
|
|
32
|
+
- **js not exists build-in serialization methods
|
|
33
|
+
|
|
34
|
+
### Limitations
|
|
35
|
+
|
|
36
|
+
For maximum portability of the configuration to the target language:
|
|
37
|
+
|
|
38
|
+
- Use CSS selectors: they are guaranteed to be converted to XPATH
|
|
39
|
+
- Unlike javascript, most html parse libs implement [CSS3 selectors standard](https://www.w3.org/TR/selectors-3/)
|
|
40
|
+
- basic selectors: (`tag`, `.class`, `#id`)
|
|
41
|
+
- combined: (`div p`, `ul > li`, `h2 +p`\[1])
|
|
42
|
+
- attribute: (`a[href]`, `input[type='text']`)\[2]
|
|
43
|
+
- pseudo classes: (`:nth-child(n)`, `:first-child`, `:last-child`)\[3]
|
|
44
|
+
- **often, not support more complex, dynamic styles**: (`:has()`, `:nth-of-type()`, `:where()`, `:is()`)
|
|
45
|
+
|
|
46
|
+
1. Several libs not support `+` operations (eg: [selectolax(modest)](https://github.com/rushter/selectolax), [dart.universal_html](https://pub.dev/packages/universal_html))
|
|
47
|
+
2. Often, web scraping libs not supports attribute operations like `*=`, `~=`, `|=`, `^=` and `$=`
|
|
48
|
+
3. Several libs not support pseudo classes (eg: standard [dart.html](https://dart.dev/libraries/dart-html) lib miss this feature).
|
|
49
|
+
This project will not implement converters with such a cons
|
|
50
|
+
|
|
51
|
+
## Getting started
|
|
52
|
+
|
|
53
|
+
ssc_gen required python 3.10 version or higher
|
|
54
|
+
|
|
55
|
+
### Install
|
|
56
|
+
|
|
57
|
+
pip:
|
|
58
|
+
|
|
59
|
+
```shell
|
|
60
|
+
pip install ssc_codegen
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
uv:
|
|
64
|
+
|
|
65
|
+
```shell
|
|
66
|
+
uv pip install ssc_codegen
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
as cli converter tool:
|
|
70
|
+
|
|
71
|
+
| package manager | command |
|
|
72
|
+
|-----------------|-------------------------------|
|
|
73
|
+
| pipx | `pipx install ssc_codegen` |
|
|
74
|
+
| uv | `uv tool install ssc_codegen` |
|
|
75
|
+
|
|
76
|
+
## Example
|
|
77
|
+
|
|
78
|
+
### Create a file `schema.py` with:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from ssc_codegen import ItemSchema, D
|
|
82
|
+
|
|
83
|
+
class HelloWorld(ItemSchema):
|
|
84
|
+
title = D().css('title').text()
|
|
85
|
+
a_hrefs = D().css_all('a').attr('href')
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### try it in cli
|
|
89
|
+
|
|
90
|
+
>[!note]
|
|
91
|
+
> this tools developed for testing purposes, not for web-scraping
|
|
92
|
+
|
|
93
|
+
### from file
|
|
94
|
+
|
|
95
|
+
>[!warning]
|
|
96
|
+
> DO NOT PASS CONFIGS FROM UNKNOWN SOURCES:
|
|
97
|
+
>
|
|
98
|
+
> PYTHON CODE FROM CONFIGS COMPILE IN RUNTIME WOUT SECURITY CHECKS!!!
|
|
99
|
+
|
|
100
|
+
Download any html file and pass as argument:
|
|
101
|
+
|
|
102
|
+
```shell
|
|
103
|
+
ssc-gen parse-from-file index.html -t schema.py:HelloWorld
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Short options descriptions:
|
|
107
|
+
|
|
108
|
+
- `-t --target` - config schema file and class from where to start the parser
|
|
109
|
+
|
|
110
|
+

|
|
111
|
+
|
|
112
|
+
### from url
|
|
113
|
+
|
|
114
|
+
```shell
|
|
115
|
+
ssc-gen parse-from-url https://example.com -t schema.py:HelloWorld
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+

|
|
119
|
+
### from Chromium browser (CDP protocol)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
```shell
|
|
123
|
+
ssc-gen parse-from-chrome https://example.com -t schema.py:HelloWorld
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
>[!note]
|
|
127
|
+
> if script cannot found chrome executable - provide it manually:
|
|
128
|
+
|
|
129
|
+
```shell
|
|
130
|
+
ssc-gen parse-from-chrome https://example.com -t schema.py:HelloWorld -sc /usr/bin/chromium
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
### Convert to code
|
|
135
|
+
|
|
136
|
+
Convert to code for use in projects:
|
|
137
|
+
|
|
138
|
+
>![note]
|
|
139
|
+
> for example, used js: it can be fast test in developer console
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
```shell
|
|
143
|
+
ssc-gen js schema.py -o .
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Code output looks like this (code formatted by IDE):
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
// autogenerated by ssc-gen DO NOT_EDIT
|
|
150
|
+
/**
|
|
151
|
+
*
|
|
152
|
+
*
|
|
153
|
+
* {
|
|
154
|
+
* "title": "String",
|
|
155
|
+
* "a_hrefs": "Array<String>"
|
|
156
|
+
* }
|
|
157
|
+
*/
|
|
158
|
+
class HelloWorld {
|
|
159
|
+
constructor(doc) {
|
|
160
|
+
if (typeof doc === 'string') {
|
|
161
|
+
this._doc = new DOMParser().parseFromString(doc, 'text/html');
|
|
162
|
+
} else if (doc instanceof Document || doc instanceof Element) {
|
|
163
|
+
this._doc = doc;
|
|
164
|
+
} else {
|
|
165
|
+
throw new Error("Invalid input: Expected a Document, Element, or string");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_parseTitle(value) {
|
|
170
|
+
let value1 = value.querySelector('title');
|
|
171
|
+
return typeof value1.textContent === "undefined" ? value1.documentElement.textContent : value1.textContent;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_parseAHrefs(value) {
|
|
175
|
+
let value1 = Array.from(value.querySelectorAll('a'));
|
|
176
|
+
return value1.map(e => e.getAttribute('href'));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
parse() {
|
|
180
|
+
return {title: this._parseTitle(this._doc), a_hrefs: this._parseAHrefs(this._doc)};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### copy code output and past to developer console:
|
|
186
|
+
|
|
187
|
+
Print output:
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
alert(JSON.stringify((new HelloWorld(document).parse())))
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+

|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
You can use any html source:
|
|
197
|
+
|
|
198
|
+
- read from html file
|
|
199
|
+
- get from http request
|
|
200
|
+
- get from browser (playwright, selenium, chrome-cdp)
|
|
201
|
+
- paste code to developer console (js)
|
|
202
|
+
- or call curl in shell and parse stdin
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
## See also
|
|
206
|
+
- [Brief](docs/brief.md) about css selectors and regular expressions.
|
|
207
|
+
- [Tutorial](docs/tutorial.md) how to use ssc-gen
|
|
208
|
+
- [Reference](docs/reference.md) about high-level API
|
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ssc_codegen"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "
|
|
3
|
+
version = "0.7.1"
|
|
4
|
+
description = "Python-dsl code converter to html parser for web scraping "
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"cssselect>=1.2.0",
|
|
9
|
+
"httpx>=0.28.1",
|
|
10
|
+
"ichrome>=4.0.4",
|
|
9
11
|
"lxml>=5.3.0",
|
|
12
|
+
"parsel>=1.10.0",
|
|
10
13
|
"typer>=0.15.1",
|
|
11
|
-
"typing_extensions;python_version<'3.11'"
|
|
14
|
+
"typing_extensions;python_version<'3.11'",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Topic :: Software Development :: Code Generators",
|
|
22
|
+
"Topic :: Text Processing :: Markup :: HTML",
|
|
23
|
+
"Topic :: Utilities",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
|
|
12
29
|
]
|
|
13
30
|
|
|
14
31
|
[project.urls]
|
|
@@ -25,12 +42,14 @@ ssc-gen = 'ssc_codegen.cli.main:main'
|
|
|
25
42
|
requires = ["hatchling"]
|
|
26
43
|
build-backend = "hatchling.build"
|
|
27
44
|
|
|
28
|
-
[tool.
|
|
29
|
-
|
|
30
|
-
|
|
45
|
+
[tool.hatch.build.targets.sdist]
|
|
46
|
+
include = [
|
|
47
|
+
"ssc_codegen/*",
|
|
48
|
+
]
|
|
31
49
|
|
|
32
50
|
[dependency-groups]
|
|
33
51
|
dev = [
|
|
52
|
+
"coverage>=7.6.12",
|
|
34
53
|
"httpx>=0.28.1",
|
|
35
54
|
"mypy>=1.14.1",
|
|
36
55
|
"pytest>=8.3.4",
|
|
@@ -40,7 +59,6 @@ dev = [
|
|
|
40
59
|
tests = [
|
|
41
60
|
"bs4>=0.0.2",
|
|
42
61
|
"parsel>=1.10.0",
|
|
43
|
-
"pytest>=8.3.4",
|
|
44
62
|
"selectolax>=0.3.27",
|
|
45
63
|
]
|
|
46
64
|
|
|
@@ -3,7 +3,7 @@ from .document import HTMLDocument, StringDocument, ArrayDocument, AssertDocumen
|
|
|
3
3
|
from .json_struct import Json
|
|
4
4
|
from .schema import ItemSchema, DictSchema, ListSchema, FlatListSchema
|
|
5
5
|
|
|
6
|
-
VERSION = "0.
|
|
6
|
+
VERSION = "0.7.1"
|
|
7
7
|
|
|
8
8
|
class __MISSING(object):
|
|
9
9
|
pass
|
|
@@ -18,14 +18,14 @@ class Nested(HTMLDocument, NestedDocument, ArrayDocument, AssertDocument):
|
|
|
18
18
|
pass
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def D(
|
|
21
|
+
def D(default: None | str | int | float | __MISSING =_NO_DEFAULT) -> Document: # noqa
|
|
22
22
|
"""Shortcut as a Document() object
|
|
23
23
|
|
|
24
|
-
:param
|
|
24
|
+
:param default: .default() operator shortcut
|
|
25
25
|
"""
|
|
26
|
-
if
|
|
26
|
+
if default==_NO_DEFAULT:
|
|
27
27
|
return Document()
|
|
28
|
-
return Document().default(value=
|
|
28
|
+
return Document().default(value=default) # type: ignore
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def N() -> Nested: # noqa
|
|
@@ -33,12 +33,12 @@ def N() -> Nested: # noqa
|
|
|
33
33
|
return Nested()
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def R(
|
|
36
|
+
def R(default: None | str | int | float | __MISSING =_NO_DEFAULT) -> Document: # noqa
|
|
37
37
|
"""Shortcut as a Document().raw() object.
|
|
38
38
|
For regex and format string operations
|
|
39
39
|
|
|
40
|
-
:param
|
|
40
|
+
:param default: .default() operator shortcut
|
|
41
41
|
"""
|
|
42
|
-
if
|
|
42
|
+
if default==_NO_DEFAULT:
|
|
43
43
|
return Document().raw()
|
|
44
|
-
return Document().default(
|
|
44
|
+
return Document().default(default).raw() # type: ignore
|
|
@@ -52,11 +52,31 @@ def check_schema_required_fields(schema: Type[BaseSchema]) -> Type[BaseSchema]:
|
|
|
52
52
|
_check_required_attributes(
|
|
53
53
|
schema, "__SPLIT_DOC__", "__KEY__", "__VALUE__"
|
|
54
54
|
)
|
|
55
|
+
# delete non required fields
|
|
56
|
+
not_required_fields: list[str] = []
|
|
57
|
+
for k, v in schema.__get_mro_fields__().items():
|
|
58
|
+
if k not in {"__SPLIT_DOC__", "__KEY__", "__VALUE__"}:
|
|
59
|
+
msg = f"{schema.__name__}.{k} not required, remove"
|
|
60
|
+
warnings.warn(msg, category=SyntaxWarning)
|
|
61
|
+
not_required_fields.append(k)
|
|
62
|
+
schema = _remove_non_required_attributes(
|
|
63
|
+
schema, *not_required_fields
|
|
64
|
+
)
|
|
55
65
|
case StructType.FLAT_LIST:
|
|
56
66
|
schema = _remove_non_required_attributes(
|
|
57
67
|
schema, "__KEY__", "__VALUE__"
|
|
58
68
|
)
|
|
59
69
|
_check_required_attributes(schema, "__SPLIT_DOC__", "__ITEM__")
|
|
70
|
+
|
|
71
|
+
not_required_fields: list[str] = [] # type: ignore
|
|
72
|
+
for k, v in schema.__get_mro_fields__().items():
|
|
73
|
+
if k not in {"__SPLIT_DOC__", "__ITEM__"}:
|
|
74
|
+
msg = f"{schema.__name__}.{k} not required, remove"
|
|
75
|
+
warnings.warn(msg, category=SyntaxWarning)
|
|
76
|
+
not_required_fields.append(k)
|
|
77
|
+
schema = _remove_non_required_attributes(
|
|
78
|
+
schema, *not_required_fields
|
|
79
|
+
)
|
|
60
80
|
case _:
|
|
61
81
|
msg = f"{schema.__name__}: Unknown schema type"
|
|
62
82
|
raise SyntaxError(msg)
|
|
@@ -2,7 +2,9 @@ import importlib
|
|
|
2
2
|
import re
|
|
3
3
|
import warnings
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
from types import ModuleType
|
|
5
6
|
from typing import TYPE_CHECKING, Protocol
|
|
7
|
+
import difflib
|
|
6
8
|
|
|
7
9
|
from typer import BadParameter
|
|
8
10
|
|
|
@@ -77,3 +79,14 @@ def raw_json_check_keys(jsn: str) -> None:
|
|
|
77
79
|
raise BadParameter(
|
|
78
80
|
f"bad json keys count: {len(results)}, keys: {all_keys}"
|
|
79
81
|
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def suggest_class_name(module: ModuleType, target_class_name: str) -> str:
|
|
85
|
+
"""try to suggest correct class name"""
|
|
86
|
+
class_names = [
|
|
87
|
+
name for name in dir(module) if isinstance(getattr(module, name), type)
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
return difflib.get_close_matches(
|
|
91
|
+
target_class_name, class_names, n=3, cutoff=0.6
|
|
92
|
+
)[0]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
""""""
|
|
2
|
+
|
|
3
|
+
from typing import Callable
|
|
4
|
+
from ssc_codegen.str_utils import (
|
|
5
|
+
go_unimport_naive,
|
|
6
|
+
py_optimize_return_naive,
|
|
7
|
+
py_str_format_to_fstring,
|
|
8
|
+
js_pure_optimize_return,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseCodeCallback:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
*code_callbacks: Callable[[str], str],
|
|
16
|
+
join_sep: str = "\n",
|
|
17
|
+
remove_empty_lines: bool = False,
|
|
18
|
+
):
|
|
19
|
+
self.join_sep = join_sep
|
|
20
|
+
self.remove_empty_lines = remove_empty_lines
|
|
21
|
+
self.callbacks = code_callbacks
|
|
22
|
+
|
|
23
|
+
def __call__(self, lines: list[str]) -> str:
|
|
24
|
+
if self.remove_empty_lines:
|
|
25
|
+
code = self.join_sep.join([i for i in lines if i])
|
|
26
|
+
else:
|
|
27
|
+
code = self.join_sep.join(lines)
|
|
28
|
+
for cb in self.callbacks:
|
|
29
|
+
code = cb(code)
|
|
30
|
+
return code
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
CB_PY_CODE = BaseCodeCallback(
|
|
34
|
+
py_optimize_return_naive, py_str_format_to_fstring
|
|
35
|
+
)
|
|
36
|
+
CB_GO_CODE = BaseCodeCallback(go_unimport_naive, remove_empty_lines=True)
|
|
37
|
+
CB_DART_CODE = BaseCodeCallback(remove_empty_lines=True)
|
|
38
|
+
CB_JS_CODE = BaseCodeCallback(js_pure_optimize_return)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from
|
|
1
|
+
from ssc_codegen._compat import StrEnum
|
|
2
2
|
|
|
3
3
|
CONVERTERS_PATH = "ssc_codegen.converters"
|
|
4
4
|
|
|
@@ -40,8 +40,12 @@ HELP_JSON_TO_SCHEMA = "convert json response to ssc-gen schema"
|
|
|
40
40
|
CMD_VERSION = "Show version and exit"
|
|
41
41
|
CMD_PY = "generate python modules"
|
|
42
42
|
CMD_JS = "generate javascript modules"
|
|
43
|
-
CMD_DART = "generate dart modules
|
|
43
|
+
CMD_DART = "generate dart modules"
|
|
44
44
|
CMD_GO = "generate golang modules"
|
|
45
45
|
CMD_JSON_GEN = """generate schema code from json.
|
|
46
|
-
entrypoint should be:
|
|
46
|
+
entrypoint should be: map or array object and key starts as _[a-zA-Z] char
|
|
47
47
|
"""
|
|
48
|
+
|
|
49
|
+
DEFAULT_UA = (
|
|
50
|
+
"Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"
|
|
51
|
+
)
|