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.
@@ -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
+ requests>=2.0.0
@@ -0,0 +1 @@
1
+ nstdata_ai_crawl
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=42", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )