nstdata-ai-crawl 0.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.
- nstdata_ai_crawl-0.1.0/LICENSE +21 -0
- nstdata_ai_crawl-0.1.0/PKG-INFO +74 -0
- nstdata_ai_crawl-0.1.0/README.md +49 -0
- nstdata_ai_crawl-0.1.0/nstdata_ai_crawl/__init__.py +37 -0
- nstdata_ai_crawl-0.1.0/nstdata_ai_crawl/client.py +149 -0
- nstdata_ai_crawl-0.1.0/nstdata_ai_crawl/constants.py +34 -0
- nstdata_ai_crawl-0.1.0/nstdata_ai_crawl/models.py +264 -0
- nstdata_ai_crawl-0.1.0/nstdata_ai_crawl.egg-info/PKG-INFO +74 -0
- nstdata_ai_crawl-0.1.0/nstdata_ai_crawl.egg-info/SOURCES.txt +13 -0
- nstdata_ai_crawl-0.1.0/nstdata_ai_crawl.egg-info/dependency_links.txt +1 -0
- nstdata_ai_crawl-0.1.0/nstdata_ai_crawl.egg-info/requires.txt +1 -0
- nstdata_ai_crawl-0.1.0/nstdata_ai_crawl.egg-info/top_level.txt +1 -0
- nstdata_ai_crawl-0.1.0/pyproject.toml +3 -0
- nstdata_ai_crawl-0.1.0/setup.cfg +4 -0
- nstdata_ai_crawl-0.1.0/setup.py +26 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nstdata
|
|
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,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nstdata-ai-crawl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for nstdata
|
|
5
|
+
Home-page: https://github.com/nstdata-ai/crawl-py
|
|
6
|
+
Author: nstdata-ai
|
|
7
|
+
Author-email: github@nstproxy.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.7
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: requests>=2.0.0
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: author-email
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
25
|
+
|
|
26
|
+
# nstdata crawl SDK (Python)
|
|
27
|
+
|
|
28
|
+
[中文版](README_zh.md)
|
|
29
|
+
|
|
30
|
+
Python SDK for nstdata crawl, an advanced web scraping and crawling service.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install nstdata-ai-crawl
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **Basic Scraping:** Get HTML and Markdown synchronously.
|
|
41
|
+
- **Screenshots & PDFs:** Take full-page screenshots and export pages as PDFs.
|
|
42
|
+
- **Browser Actions:** Orchestrate browser actions like wait for elements, fill forms, click buttons, scroll, and execute JS.
|
|
43
|
+
- **Custom Configuration:** Bring your own cookies, headers, and simulate mobile devices.
|
|
44
|
+
- **Content Filtering:** Automatically extract high-quality main content, filter out ads and navigational elements.
|
|
45
|
+
- **Proxy Support:** Specify proxy type (residential or datacenter) and exit country.
|
|
46
|
+
- **Asynchronous Execution & Polling:** Submit time-consuming tasks and poll for results.
|
|
47
|
+
- **Batch Crawling:** Traverse entire sites with depth and page limits.
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
See [main.py](./main.py) for a complete list of usage examples.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import os
|
|
55
|
+
from nstdata_ai_crawl import NstDataClient, ScrapeRequestDto, Format
|
|
56
|
+
|
|
57
|
+
# Initialize the client with your API token
|
|
58
|
+
TOKEN = os.getenv("NSTDATA_API_TOKEN", "YOUR_API_TOKEN")
|
|
59
|
+
|
|
60
|
+
def main():
|
|
61
|
+
with NstDataClient(TOKEN) as client:
|
|
62
|
+
# Submit a basic scraping task
|
|
63
|
+
res = client.submit_scrape_task_sync(ScrapeRequestDto(
|
|
64
|
+
url="https://example.com/",
|
|
65
|
+
formats=[Format.MARKDOWN],
|
|
66
|
+
timeout=60000,
|
|
67
|
+
))
|
|
68
|
+
|
|
69
|
+
# Output the resulting markdown
|
|
70
|
+
print(res.data.get_markdown())
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
main()
|
|
74
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# nstdata crawl SDK (Python)
|
|
2
|
+
|
|
3
|
+
[中文版](README_zh.md)
|
|
4
|
+
|
|
5
|
+
Python SDK for nstdata crawl, an advanced web scraping and crawling service.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install nstdata-ai-crawl
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Basic Scraping:** Get HTML and Markdown synchronously.
|
|
16
|
+
- **Screenshots & PDFs:** Take full-page screenshots and export pages as PDFs.
|
|
17
|
+
- **Browser Actions:** Orchestrate browser actions like wait for elements, fill forms, click buttons, scroll, and execute JS.
|
|
18
|
+
- **Custom Configuration:** Bring your own cookies, headers, and simulate mobile devices.
|
|
19
|
+
- **Content Filtering:** Automatically extract high-quality main content, filter out ads and navigational elements.
|
|
20
|
+
- **Proxy Support:** Specify proxy type (residential or datacenter) and exit country.
|
|
21
|
+
- **Asynchronous Execution & Polling:** Submit time-consuming tasks and poll for results.
|
|
22
|
+
- **Batch Crawling:** Traverse entire sites with depth and page limits.
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
See [main.py](./main.py) for a complete list of usage examples.
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import os
|
|
30
|
+
from nstdata_ai_crawl import NstDataClient, ScrapeRequestDto, Format
|
|
31
|
+
|
|
32
|
+
# Initialize the client with your API token
|
|
33
|
+
TOKEN = os.getenv("NSTDATA_API_TOKEN", "YOUR_API_TOKEN")
|
|
34
|
+
|
|
35
|
+
def main():
|
|
36
|
+
with NstDataClient(TOKEN) as client:
|
|
37
|
+
# Submit a basic scraping task
|
|
38
|
+
res = client.submit_scrape_task_sync(ScrapeRequestDto(
|
|
39
|
+
url="https://example.com/",
|
|
40
|
+
formats=[Format.MARKDOWN],
|
|
41
|
+
timeout=60000,
|
|
42
|
+
))
|
|
43
|
+
|
|
44
|
+
# Output the resulting markdown
|
|
45
|
+
print(res.data.get_markdown())
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
main()
|
|
49
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from .client import NstDataClient
|
|
2
|
+
from .constants import Format, TaskType, TaskStatus, ActionType, Proxy
|
|
3
|
+
from .models import (
|
|
4
|
+
CookieDto,
|
|
5
|
+
ActionDto,
|
|
6
|
+
JsRenderDto,
|
|
7
|
+
ScrapeRequestDto,
|
|
8
|
+
CrawlRequestDto,
|
|
9
|
+
SubmitResponseDto,
|
|
10
|
+
PageMetadataDto,
|
|
11
|
+
ScrapeDataDto,
|
|
12
|
+
TaskResultResponseDto,
|
|
13
|
+
CrawlStatusResponseDto,
|
|
14
|
+
CrawlPageItemResponseDto,
|
|
15
|
+
CrawlPageListResponseDto
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"NstDataClient",
|
|
20
|
+
"Format",
|
|
21
|
+
"TaskType",
|
|
22
|
+
"TaskStatus",
|
|
23
|
+
"ActionType",
|
|
24
|
+
"Proxy",
|
|
25
|
+
"CookieDto",
|
|
26
|
+
"ActionDto",
|
|
27
|
+
"JsRenderDto",
|
|
28
|
+
"ScrapeRequestDto",
|
|
29
|
+
"CrawlRequestDto",
|
|
30
|
+
"SubmitResponseDto",
|
|
31
|
+
"PageMetadataDto",
|
|
32
|
+
"ScrapeDataDto",
|
|
33
|
+
"TaskResultResponseDto",
|
|
34
|
+
"CrawlStatusResponseDto",
|
|
35
|
+
"CrawlPageItemResponseDto",
|
|
36
|
+
"CrawlPageListResponseDto"
|
|
37
|
+
]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import base64
|
|
3
|
+
from typing import Optional, Dict
|
|
4
|
+
from .models import (
|
|
5
|
+
ScrapeRequestDto,
|
|
6
|
+
CrawlRequestDto,
|
|
7
|
+
SubmitResponseDto,
|
|
8
|
+
TaskResultResponseDto,
|
|
9
|
+
PageMetadataDto,
|
|
10
|
+
ScrapeDataDto,
|
|
11
|
+
CrawlStatusResponseDto,
|
|
12
|
+
CrawlPageItemResponseDto,
|
|
13
|
+
CrawlPageListResponseDto
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Built-in API endpoint.
|
|
17
|
+
_BASE_URL = "https://api.nstproxy.com"
|
|
18
|
+
|
|
19
|
+
def _build(cls, data: dict):
|
|
20
|
+
"""Construct a DTO from a response dict, ignoring keys the DTO doesn't
|
|
21
|
+
declare so server-side field additions don't break the client."""
|
|
22
|
+
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
|
|
23
|
+
|
|
24
|
+
class NstDataClient:
|
|
25
|
+
def __init__(self, token: str):
|
|
26
|
+
self.base_url = _BASE_URL
|
|
27
|
+
self.token = token
|
|
28
|
+
self.session = requests.Session()
|
|
29
|
+
|
|
30
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
31
|
+
return {"x-api-key": self.token}
|
|
32
|
+
|
|
33
|
+
def _unwrap(self, resp: requests.Response) -> dict:
|
|
34
|
+
resp.raise_for_status()
|
|
35
|
+
data = resp.json()
|
|
36
|
+
if isinstance(data, dict) and "err" in data and "code" in data:
|
|
37
|
+
if data.get("err") or data.get("code") != 200:
|
|
38
|
+
raise Exception(f"API error: code={data.get('code')} msg={data.get('msg')}")
|
|
39
|
+
return data.get("data") or {}
|
|
40
|
+
return data
|
|
41
|
+
|
|
42
|
+
def submit_scrape_task(self, req: ScrapeRequestDto) -> SubmitResponseDto:
|
|
43
|
+
if not req.url:
|
|
44
|
+
raise ValueError("url is required")
|
|
45
|
+
if not req.formats:
|
|
46
|
+
raise ValueError("formats must contain at least one format")
|
|
47
|
+
url = f"{self.base_url}/api/v1/crawl/scrape/submit"
|
|
48
|
+
resp = self.session.post(url, json=req.to_dict(), headers=self._get_headers())
|
|
49
|
+
data = self._unwrap(resp)
|
|
50
|
+
return _build(SubmitResponseDto, data)
|
|
51
|
+
|
|
52
|
+
def submit_scrape_task_sync(self, req: ScrapeRequestDto) -> TaskResultResponseDto:
|
|
53
|
+
if not req.url:
|
|
54
|
+
raise ValueError("url is required")
|
|
55
|
+
if not req.formats:
|
|
56
|
+
raise ValueError("formats must contain at least one format")
|
|
57
|
+
url = f"{self.base_url}/api/v1/crawl/scrape/submit-sync"
|
|
58
|
+
resp = self.session.post(url, json=req.to_dict(), headers=self._get_headers())
|
|
59
|
+
data = self._unwrap(resp)
|
|
60
|
+
|
|
61
|
+
if 'data' in data and data['data'] is not None:
|
|
62
|
+
if 'metadata' in data['data'] and data['data']['metadata'] is not None:
|
|
63
|
+
data['data']['metadata'] = _build(PageMetadataDto, data['data']['metadata'])
|
|
64
|
+
raw = data['data']
|
|
65
|
+
data['data'] = _build(ScrapeDataDto, raw)
|
|
66
|
+
data['data']._html = raw.get('html')
|
|
67
|
+
data['data']._markdown = raw.get('markdown')
|
|
68
|
+
data['data']._client = self
|
|
69
|
+
|
|
70
|
+
return _build(TaskResultResponseDto, data)
|
|
71
|
+
|
|
72
|
+
def submit_crawl_task(self, req: CrawlRequestDto) -> SubmitResponseDto:
|
|
73
|
+
if not req.url:
|
|
74
|
+
raise ValueError("url is required")
|
|
75
|
+
if not req.formats:
|
|
76
|
+
raise ValueError("formats must contain at least one format")
|
|
77
|
+
url = f"{self.base_url}/api/v1/crawl/submit"
|
|
78
|
+
resp = self.session.post(url, json=req.to_dict(), headers=self._get_headers())
|
|
79
|
+
data = self._unwrap(resp)
|
|
80
|
+
return _build(SubmitResponseDto, data)
|
|
81
|
+
|
|
82
|
+
def get_crawl_status(self, crawl_id: str) -> CrawlStatusResponseDto:
|
|
83
|
+
url = f"{self.base_url}/api/v1/crawl/status/{crawl_id}"
|
|
84
|
+
resp = self.session.get(url, headers=self._get_headers())
|
|
85
|
+
data = self._unwrap(resp)
|
|
86
|
+
return _build(CrawlStatusResponseDto, data)
|
|
87
|
+
|
|
88
|
+
def list_crawl_pages(self, crawl_id: str, after_cursor: Optional[str] = None, before_cursor: Optional[str] = None, limit: Optional[int] = None) -> CrawlPageListResponseDto:
|
|
89
|
+
url = f"{self.base_url}/api/v1/crawl/pages/{crawl_id}"
|
|
90
|
+
params = {}
|
|
91
|
+
if after_cursor is not None:
|
|
92
|
+
params["after_cursor"] = after_cursor
|
|
93
|
+
if before_cursor is not None:
|
|
94
|
+
params["before_cursor"] = before_cursor
|
|
95
|
+
if limit is not None:
|
|
96
|
+
params["limit"] = limit
|
|
97
|
+
|
|
98
|
+
resp = self.session.get(url, params=params, headers=self._get_headers())
|
|
99
|
+
data = self._unwrap(resp)
|
|
100
|
+
|
|
101
|
+
if 'data' in data and data['data'] is not None:
|
|
102
|
+
items = []
|
|
103
|
+
for item in data['data']:
|
|
104
|
+
if item.get('data') is not None:
|
|
105
|
+
if item['data'].get('metadata') is not None:
|
|
106
|
+
item['data']['metadata'] = _build(PageMetadataDto, item['data']['metadata'])
|
|
107
|
+
raw = item['data']
|
|
108
|
+
item['data'] = _build(ScrapeDataDto, raw)
|
|
109
|
+
item['data']._html = raw.get('html')
|
|
110
|
+
item['data']._markdown = raw.get('markdown')
|
|
111
|
+
|
|
112
|
+
item['data']._client = self
|
|
113
|
+
items.append(_build(CrawlPageItemResponseDto, item))
|
|
114
|
+
data['data'] = items
|
|
115
|
+
|
|
116
|
+
return _build(CrawlPageListResponseDto, data)
|
|
117
|
+
|
|
118
|
+
def get_scrape_task_result(self, task_id: str) -> TaskResultResponseDto:
|
|
119
|
+
url = f"{self.base_url}/api/v1/crawl/scrape/result/{task_id}"
|
|
120
|
+
resp = self.session.get(url, headers=self._get_headers())
|
|
121
|
+
data = self._unwrap(resp)
|
|
122
|
+
|
|
123
|
+
if 'data' in data and data['data'] is not None:
|
|
124
|
+
if 'metadata' in data['data'] and data['data']['metadata'] is not None:
|
|
125
|
+
data['data']['metadata'] = _build(PageMetadataDto, data['data']['metadata'])
|
|
126
|
+
raw = data['data']
|
|
127
|
+
data['data'] = _build(ScrapeDataDto, raw)
|
|
128
|
+
data['data']._html = raw.get('html')
|
|
129
|
+
data['data']._markdown = raw.get('markdown')
|
|
130
|
+
data['data']._client = self
|
|
131
|
+
|
|
132
|
+
return _build(TaskResultResponseDto, data)
|
|
133
|
+
|
|
134
|
+
def read_storage(self, st: str) -> bytes:
|
|
135
|
+
url = f"{self.base_url}/api/v1/crawl/storage/read"
|
|
136
|
+
resp = self.session.get(url, params={"st": st}, headers=self._get_headers())
|
|
137
|
+
resp.raise_for_status()
|
|
138
|
+
return resp.content
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def close(self):
|
|
143
|
+
self.session.close()
|
|
144
|
+
|
|
145
|
+
def __enter__(self):
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
149
|
+
self.close()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class Format:
|
|
2
|
+
HTML = "html"
|
|
3
|
+
MARKDOWN = "markdown"
|
|
4
|
+
LINKS = "links"
|
|
5
|
+
PDF = "pdf"
|
|
6
|
+
SCREENSHOT = "screenshot"
|
|
7
|
+
RAW_DATA = "rawData"
|
|
8
|
+
|
|
9
|
+
class TaskType:
|
|
10
|
+
SCRAPE = "scrape"
|
|
11
|
+
CRAWL = "crawl"
|
|
12
|
+
|
|
13
|
+
class TaskStatus:
|
|
14
|
+
PROCESSING = "processing"
|
|
15
|
+
COMPLETED = "completed"
|
|
16
|
+
FAILED = "failed"
|
|
17
|
+
SCRAPING = "scraping" # crawl 会话仍在抓取中
|
|
18
|
+
|
|
19
|
+
class ActionType:
|
|
20
|
+
CLICK = "click" # click an element; requires selector
|
|
21
|
+
WAIT = "wait" # wait a fixed time; requires milliseconds
|
|
22
|
+
WAIT_SELECTOR = "waitSelector" # wait until element is visible; requires selector
|
|
23
|
+
SCROLL_X = "scrollX" # scroll horizontally by x pixels
|
|
24
|
+
SCROLL_Y = "scrollY" # scroll vertically by y pixels
|
|
25
|
+
SCROLL_TO = "scrollTo" # scroll to an element; requires selector
|
|
26
|
+
FILL = "fill" # fill an input; requires selector and text
|
|
27
|
+
EXECUTE = "execute" # run JavaScript on the page; requires execute
|
|
28
|
+
SCREENSHOT = "screenshot" # capture screenshot; fullScreenshot / particularScreenshot / viewport
|
|
29
|
+
WAIT_FOR_REQUEST_COMPLETION = "waitForRequestCompletion" # wait for a matching network response; requires urlPattern
|
|
30
|
+
|
|
31
|
+
class Proxy:
|
|
32
|
+
"""Proxy types for ScrapeRequestDto.proxy (defaults to residential when unset)."""
|
|
33
|
+
RESIDENTIAL = "residential"
|
|
34
|
+
DATACENTER = "datacenter"
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
from typing import Optional, List, Dict, Any
|
|
2
|
+
from dataclasses import dataclass, asdict, field
|
|
3
|
+
|
|
4
|
+
from .constants import ActionType
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _to_clean_dict(obj) -> Dict[str, Any]:
|
|
8
|
+
def _clean(v):
|
|
9
|
+
if isinstance(v, dict):
|
|
10
|
+
return {k: _clean(x) for k, x in v.items() if x is not None}
|
|
11
|
+
elif isinstance(v, list):
|
|
12
|
+
return [_clean(x) for x in v if x is not None]
|
|
13
|
+
return v
|
|
14
|
+
return _clean(asdict(obj))
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class CookieDto:
|
|
18
|
+
domain: Optional[str] = None # Domain the cookie belongs to
|
|
19
|
+
httpOnly: Optional[bool] = None # True if the cookie is HTTP-only
|
|
20
|
+
name: Optional[str] = None # Name of the cookie
|
|
21
|
+
path: Optional[str] = None # Path for the cookie
|
|
22
|
+
secure: Optional[bool] = None # True if the cookie requires HTTPS
|
|
23
|
+
value: Optional[str] = None # Value of the cookie
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ActionDto:
|
|
27
|
+
execute: Optional[str] = None # JavaScript code snippet to execute
|
|
28
|
+
fullScreenshot: Optional[bool] = None # Set to true to capture a full-page screenshot
|
|
29
|
+
key: Optional[str] = None # Keyboard key to press
|
|
30
|
+
milliseconds: Optional[float] = None # Milliseconds to wait
|
|
31
|
+
particularScreenshot: Optional[str] = None # CSS selector of an element to screenshot
|
|
32
|
+
selector: Optional[str] = None # CSS selector for the action target
|
|
33
|
+
text: Optional[str] = None # Text to type into an input field
|
|
34
|
+
type: Optional[str] = None # Type of action (e.g., 'click', 'wait', 'fill')
|
|
35
|
+
urlPattern: Optional[str] = None # URL pattern to wait for (regex support)
|
|
36
|
+
waitSelector: Optional[str] = None # CSS selector to wait for
|
|
37
|
+
x: Optional[float] = None # X coordinate for scroll/click
|
|
38
|
+
y: Optional[float] = None # Y coordinate for scroll/click
|
|
39
|
+
|
|
40
|
+
# Factory helpers: each one sets `type` plus the fields that action
|
|
41
|
+
# requires, so callers don't need to remember which fields pair with
|
|
42
|
+
# which type.
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def click(cls, selector: str) -> "ActionDto":
|
|
46
|
+
"""Click the element matched by selector."""
|
|
47
|
+
return cls(type=ActionType.CLICK, selector=selector)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def wait(cls, milliseconds: float) -> "ActionDto":
|
|
51
|
+
"""Pause for a fixed time in milliseconds."""
|
|
52
|
+
return cls(type=ActionType.WAIT, milliseconds=milliseconds)
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def wait_selector(cls, selector: str, timeout_ms: Optional[float] = None) -> "ActionDto":
|
|
56
|
+
"""Wait until the element matched by selector is visible."""
|
|
57
|
+
return cls(type=ActionType.WAIT_SELECTOR, selector=selector, milliseconds=timeout_ms)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def scroll_x(cls, pixels: int) -> "ActionDto":
|
|
61
|
+
"""Scroll the window horizontally by pixels."""
|
|
62
|
+
return cls(type=ActionType.SCROLL_X, text=str(pixels), x=pixels)
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def scroll_y(cls, pixels: int) -> "ActionDto":
|
|
66
|
+
"""Scroll the window vertically by pixels."""
|
|
67
|
+
return cls(type=ActionType.SCROLL_Y, text=str(pixels), y=pixels)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def scroll_to(cls, selector: str) -> "ActionDto":
|
|
71
|
+
"""Scroll to the element matched by selector."""
|
|
72
|
+
return cls(type=ActionType.SCROLL_TO, selector=selector)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def fill(cls, selector: str, text: str) -> "ActionDto":
|
|
76
|
+
"""Type text into the input matched by selector."""
|
|
77
|
+
return cls(type=ActionType.FILL, selector=selector, text=text)
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def execute_js(cls, script: str) -> "ActionDto":
|
|
81
|
+
"""Run a JavaScript snippet on the page."""
|
|
82
|
+
return cls(type=ActionType.EXECUTE, execute=script)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def screenshot(cls, full: bool = False, selector: Optional[str] = None) -> "ActionDto":
|
|
86
|
+
"""Capture a screenshot: full page if full=True, the element matched
|
|
87
|
+
by selector if given, otherwise the current viewport."""
|
|
88
|
+
return cls(type=ActionType.SCREENSHOT, fullScreenshot=full or None, particularScreenshot=selector)
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def wait_for_request(cls, url_pattern: str, timeout_ms: Optional[float] = None) -> "ActionDto":
|
|
92
|
+
"""Wait for a network response whose URL matches url_pattern
|
|
93
|
+
(supports * wildcard)."""
|
|
94
|
+
return cls(type=ActionType.WAIT_FOR_REQUEST_COMPLETION, urlPattern=url_pattern, milliseconds=timeout_ms)
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class JsRenderDto:
|
|
98
|
+
actions: Optional[List[ActionDto]] = None # List of browser actions to perform
|
|
99
|
+
waitFor: Optional[float] = None # Global wait time in milliseconds
|
|
100
|
+
waitForSelector: Optional[str] = None # CSS selector to wait for before proceeding
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class ScrapeRequestDto:
|
|
104
|
+
url: str # The URL to scrape
|
|
105
|
+
cookies: Optional[List[CookieDto]] = None # Optional cookies to include in the request
|
|
106
|
+
country: Optional[str] = None # Country code for proxy location (e.g., 'us', 'gb')
|
|
107
|
+
excludeTags: Optional[List[str]] = None # HTML tags to remove entirely (e.g., ['nav', 'footer'])
|
|
108
|
+
formats: Optional[List[str]] = None # Desired output formats (e.g., ['markdown', 'html', 'links'])
|
|
109
|
+
headers: Optional[Dict[str, str]] = None # Custom HTTP headers
|
|
110
|
+
includeTags: Optional[List[str]] = None # Tags to keep. WARNING: strips all other tags (keeping text), which may break markdown
|
|
111
|
+
jsRender: Optional[JsRenderDto] = None # JavaScript rendering instructions
|
|
112
|
+
mobile: Optional[bool] = None # Whether to emulate a mobile device
|
|
113
|
+
onlyMainContent: Optional[bool] = None # Automatically extract main article content and remove noise
|
|
114
|
+
proxy: Optional[str] = None # Proxy type (e.g., 'datacenter', 'residential')
|
|
115
|
+
removeSelectors: Optional[List[str]] = None # CSS selectors for elements to remove (e.g., ['.ads', '#popup'])
|
|
116
|
+
targetSelector: Optional[str] = None # CSS selector to restrict extraction to a specific element
|
|
117
|
+
timeout: Optional[float] = None # Request timeout in milliseconds
|
|
118
|
+
|
|
119
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
120
|
+
return _to_clean_dict(self)
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class CrawlRequestDto:
|
|
124
|
+
"""Scrape options plus crawl traversal controls
|
|
125
|
+
(maxDepth, maxPages, includeUrls, excludeUrls, ignoreQuery)."""
|
|
126
|
+
url: str # The starting URL to crawl
|
|
127
|
+
cookies: Optional[List[CookieDto]] = None # Optional cookies to include in the request
|
|
128
|
+
country: Optional[str] = None # Country code for proxy location
|
|
129
|
+
excludeTags: Optional[List[str]] = None # HTML tags to remove entirely
|
|
130
|
+
excludeUrls: Optional[List[str]] = None # Regex patterns to exclude specific URLs from crawling
|
|
131
|
+
formats: Optional[List[str]] = None # Desired output formats (e.g., ['markdown', 'html'])
|
|
132
|
+
headers: Optional[Dict[str, str]] = None # Custom HTTP headers
|
|
133
|
+
ignoreQuery: Optional[bool] = None # Whether to ignore URL query parameters for deduplication
|
|
134
|
+
includeTags: Optional[List[str]] = None # Tags to keep. WARNING: strips all other tags
|
|
135
|
+
includeUrls: Optional[List[str]] = None # Regex patterns to only crawl matching URLs
|
|
136
|
+
jsRender: Optional[JsRenderDto] = None # JavaScript rendering instructions
|
|
137
|
+
maxDepth: Optional[int] = None # Maximum depth of links to follow
|
|
138
|
+
maxPages: Optional[int] = None # Maximum number of pages to crawl
|
|
139
|
+
mobile: Optional[bool] = None # Whether to emulate a mobile device
|
|
140
|
+
onlyMainContent: Optional[bool] = None # Automatically extract main article content
|
|
141
|
+
proxy: Optional[str] = None # Proxy type
|
|
142
|
+
removeSelectors: Optional[List[str]] = None # CSS selectors for elements to remove
|
|
143
|
+
targetSelector: Optional[str] = None # CSS selector to restrict extraction
|
|
144
|
+
timeout: Optional[float] = None # Request timeout in milliseconds
|
|
145
|
+
|
|
146
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
147
|
+
return _to_clean_dict(self)
|
|
148
|
+
|
|
149
|
+
@dataclass
|
|
150
|
+
class SubmitResponseDto:
|
|
151
|
+
cached: Optional[bool] = None # True if the response was served from cache
|
|
152
|
+
code: Optional[int] = None # HTTP status code
|
|
153
|
+
id: Optional[str] = None # Task ID
|
|
154
|
+
status: Optional[str] = None # Task status message
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class PageMetadataDto:
|
|
158
|
+
canonical: Optional[str] = None # Canonical URL of the page
|
|
159
|
+
description: Optional[str] = None # Meta description text
|
|
160
|
+
language: Optional[str] = None # Document language code
|
|
161
|
+
ogImage: Optional[str] = None # OpenGraph preview image URL
|
|
162
|
+
statusCode: Optional[int] = None # HTTP response status code
|
|
163
|
+
title: Optional[str] = None # Page title
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class ScrapeDataDto:
|
|
167
|
+
htmlRef: Optional[str] = None # Reference link to fetch the HTML content
|
|
168
|
+
links: Optional[List[str]] = None # List of extracted hyperlinks
|
|
169
|
+
markdownRef: Optional[str] = None # Reference link to fetch the Markdown content
|
|
170
|
+
metadata: Optional[PageMetadataDto] = None # Extracted page metadata and SEO tags
|
|
171
|
+
pdfRef: Optional[str] = None # Reference link to fetch the PDF
|
|
172
|
+
rawDataRef: Optional[str] = None # Reference link to fetch the raw data
|
|
173
|
+
screenshotRef: Optional[str] = None # Reference link to fetch the screenshot
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
_html: Optional[str] = field(default=None, repr=False, init=False)
|
|
177
|
+
_markdown: Optional[str] = field(default=None, repr=False, init=False)
|
|
178
|
+
_pdf: Optional[bytes] = field(default=None, repr=False, init=False)
|
|
179
|
+
_rawData: Optional[bytes] = field(default=None, repr=False, init=False)
|
|
180
|
+
_screenshot: Optional[bytes] = field(default=None, repr=False, init=False)
|
|
181
|
+
_client: Any = field(default=None, repr=False, init=False)
|
|
182
|
+
|
|
183
|
+
def get_html(self) -> Optional[str]:
|
|
184
|
+
if self._html is not None:
|
|
185
|
+
return self._html
|
|
186
|
+
if self.htmlRef and self._client:
|
|
187
|
+
self._html = self._client.read_storage(self.htmlRef).decode('utf-8')
|
|
188
|
+
return self._html
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
def get_markdown(self) -> Optional[str]:
|
|
192
|
+
if self._markdown is not None:
|
|
193
|
+
return self._markdown
|
|
194
|
+
if self.markdownRef and self._client:
|
|
195
|
+
self._markdown = self._client.read_storage(self.markdownRef).decode('utf-8')
|
|
196
|
+
return self._markdown
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
def get_pdf(self) -> Optional[bytes]:
|
|
200
|
+
if self._pdf is not None:
|
|
201
|
+
return self._pdf
|
|
202
|
+
if self.pdfRef and self._client:
|
|
203
|
+
self._pdf = self._client.read_storage(self.pdfRef)
|
|
204
|
+
return self._pdf
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
def get_raw_data(self) -> Optional[bytes]:
|
|
208
|
+
if self._rawData is not None:
|
|
209
|
+
return self._rawData
|
|
210
|
+
if self.rawDataRef and self._client:
|
|
211
|
+
self._rawData = self._client.read_storage(self.rawDataRef)
|
|
212
|
+
return self._rawData
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
def get_screenshot(self) -> Optional[bytes]:
|
|
216
|
+
if self._screenshot is not None:
|
|
217
|
+
return self._screenshot
|
|
218
|
+
if self.screenshotRef and self._client:
|
|
219
|
+
self._screenshot = self._client.read_storage(self.screenshotRef)
|
|
220
|
+
return self._screenshot
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
@dataclass
|
|
224
|
+
class TaskResultResponseDto:
|
|
225
|
+
code: Optional[int] = None # Custom status code
|
|
226
|
+
data: Optional[ScrapeDataDto] = None # The extracted scrape/crawl data
|
|
227
|
+
errorCode: Optional[str] = None # Error code identifier if any
|
|
228
|
+
errorMessage: Optional[str] = None # Detailed error message
|
|
229
|
+
status: Optional[str] = None # Current status (e.g., 'completed', 'failed')
|
|
230
|
+
success: Optional[bool] = None # True if the task executed successfully
|
|
231
|
+
|
|
232
|
+
@dataclass
|
|
233
|
+
class CrawlStatusResponseDto:
|
|
234
|
+
"""Aggregate progress of a crawl session."""
|
|
235
|
+
code: Optional[int] = None # Custom status code
|
|
236
|
+
completed: Optional[int] = None # Number of completed pages
|
|
237
|
+
failed: Optional[int] = None # Number of failed pages
|
|
238
|
+
id: Optional[str] = None # Crawl task ID
|
|
239
|
+
pending: Optional[int] = None # Number of pending pages
|
|
240
|
+
status: Optional[str] = None # Current status message
|
|
241
|
+
total: Optional[int] = None # Total number of discovered pages
|
|
242
|
+
|
|
243
|
+
@dataclass
|
|
244
|
+
class CrawlPageItemResponseDto:
|
|
245
|
+
"""One page's normalized result within a crawl session."""
|
|
246
|
+
data: Optional[ScrapeDataDto] = None # Extracted data for this page
|
|
247
|
+
errorCode: Optional[str] = None # Error code if any
|
|
248
|
+
errorMessage: Optional[str] = None # Detailed error message
|
|
249
|
+
status: Optional[str] = None # Status of this page extraction
|
|
250
|
+
success: Optional[bool] = None # True if extraction was successful
|
|
251
|
+
taskId: Optional[str] = None # ID of the specific page task
|
|
252
|
+
url: Optional[str] = None # URL of the scraped page
|
|
253
|
+
|
|
254
|
+
@dataclass
|
|
255
|
+
class CrawlPageListResponseDto:
|
|
256
|
+
"""Cursor-paginated list of crawl page results."""
|
|
257
|
+
code: Optional[int] = None # Custom status code
|
|
258
|
+
completed: Optional[int] = None # Number of completed pages
|
|
259
|
+
data: Optional[List[CrawlPageItemResponseDto]] = None # List of page results
|
|
260
|
+
nextCursor: Optional[str] = None # Cursor for fetching the next page of results
|
|
261
|
+
prevCursor: Optional[str] = None # Cursor for fetching the previous page of results
|
|
262
|
+
total: Optional[int] = None # Total number of pages
|
|
263
|
+
|
|
264
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nstdata-ai-crawl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for nstdata
|
|
5
|
+
Home-page: https://github.com/nstdata-ai/crawl-py
|
|
6
|
+
Author: nstdata-ai
|
|
7
|
+
Author-email: github@nstproxy.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.7
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: requests>=2.0.0
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: author-email
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
25
|
+
|
|
26
|
+
# nstdata crawl SDK (Python)
|
|
27
|
+
|
|
28
|
+
[中文版](README_zh.md)
|
|
29
|
+
|
|
30
|
+
Python SDK for nstdata crawl, an advanced web scraping and crawling service.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install nstdata-ai-crawl
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **Basic Scraping:** Get HTML and Markdown synchronously.
|
|
41
|
+
- **Screenshots & PDFs:** Take full-page screenshots and export pages as PDFs.
|
|
42
|
+
- **Browser Actions:** Orchestrate browser actions like wait for elements, fill forms, click buttons, scroll, and execute JS.
|
|
43
|
+
- **Custom Configuration:** Bring your own cookies, headers, and simulate mobile devices.
|
|
44
|
+
- **Content Filtering:** Automatically extract high-quality main content, filter out ads and navigational elements.
|
|
45
|
+
- **Proxy Support:** Specify proxy type (residential or datacenter) and exit country.
|
|
46
|
+
- **Asynchronous Execution & Polling:** Submit time-consuming tasks and poll for results.
|
|
47
|
+
- **Batch Crawling:** Traverse entire sites with depth and page limits.
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
See [main.py](./main.py) for a complete list of usage examples.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import os
|
|
55
|
+
from nstdata_ai_crawl import NstDataClient, ScrapeRequestDto, Format
|
|
56
|
+
|
|
57
|
+
# Initialize the client with your API token
|
|
58
|
+
TOKEN = os.getenv("NSTDATA_API_TOKEN", "YOUR_API_TOKEN")
|
|
59
|
+
|
|
60
|
+
def main():
|
|
61
|
+
with NstDataClient(TOKEN) as client:
|
|
62
|
+
# Submit a basic scraping task
|
|
63
|
+
res = client.submit_scrape_task_sync(ScrapeRequestDto(
|
|
64
|
+
url="https://example.com/",
|
|
65
|
+
formats=[Format.MARKDOWN],
|
|
66
|
+
timeout=60000,
|
|
67
|
+
))
|
|
68
|
+
|
|
69
|
+
# Output the resulting markdown
|
|
70
|
+
print(res.data.get_markdown())
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
main()
|
|
74
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
nstdata_ai_crawl/__init__.py
|
|
6
|
+
nstdata_ai_crawl/client.py
|
|
7
|
+
nstdata_ai_crawl/constants.py
|
|
8
|
+
nstdata_ai_crawl/models.py
|
|
9
|
+
nstdata_ai_crawl.egg-info/PKG-INFO
|
|
10
|
+
nstdata_ai_crawl.egg-info/SOURCES.txt
|
|
11
|
+
nstdata_ai_crawl.egg-info/dependency_links.txt
|
|
12
|
+
nstdata_ai_crawl.egg-info/requires.txt
|
|
13
|
+
nstdata_ai_crawl.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nstdata_ai_crawl
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
this_directory = Path(__file__).parent
|
|
5
|
+
long_description = (this_directory / "README.md").read_text(encoding="utf-8")
|
|
6
|
+
|
|
7
|
+
setup(
|
|
8
|
+
name="nstdata-ai-crawl",
|
|
9
|
+
version="0.1.0",
|
|
10
|
+
packages=find_packages(),
|
|
11
|
+
install_requires=[
|
|
12
|
+
"requests>=2.0.0",
|
|
13
|
+
],
|
|
14
|
+
author="nstdata-ai",
|
|
15
|
+
author_email="github@nstproxy.com",
|
|
16
|
+
description="Python SDK for nstdata",
|
|
17
|
+
long_description=long_description,
|
|
18
|
+
long_description_content_type="text/markdown",
|
|
19
|
+
url="https://github.com/nstdata-ai/crawl-py",
|
|
20
|
+
python_requires=">=3.7",
|
|
21
|
+
classifiers=[
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
],
|
|
26
|
+
)
|