mooncat-browser-client 0.4.7__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.
Potentially problematic release.
This version of mooncat-browser-client might be problematic. Click here for more details.
- mooncat_browser_client-0.4.7/PKG-INFO +70 -0
- mooncat_browser_client-0.4.7/README.md +49 -0
- mooncat_browser_client-0.4.7/behavior.py +138 -0
- mooncat_browser_client-0.4.7/browser.py +469 -0
- mooncat_browser_client-0.4.7/captcha.py +115 -0
- mooncat_browser_client-0.4.7/crawler.py +125 -0
- mooncat_browser_client-0.4.7/find.py +150 -0
- mooncat_browser_client-0.4.7/honeypot.py +73 -0
- mooncat_browser_client-0.4.7/humanize.py +392 -0
- mooncat_browser_client-0.4.7/image.py +186 -0
- mooncat_browser_client-0.4.7/interact.py +193 -0
- mooncat_browser_client-0.4.7/markdown.py +38 -0
- mooncat_browser_client-0.4.7/mooncat_browser_client.egg-info/PKG-INFO +70 -0
- mooncat_browser_client-0.4.7/mooncat_browser_client.egg-info/SOURCES.txt +23 -0
- mooncat_browser_client-0.4.7/mooncat_browser_client.egg-info/dependency_links.txt +1 -0
- mooncat_browser_client-0.4.7/mooncat_browser_client.egg-info/requires.txt +2 -0
- mooncat_browser_client-0.4.7/mooncat_browser_client.egg-info/top_level.txt +15 -0
- mooncat_browser_client-0.4.7/pyproject.toml +50 -0
- mooncat_browser_client-0.4.7/ready.py +208 -0
- mooncat_browser_client-0.4.7/scroll.py +180 -0
- mooncat_browser_client-0.4.7/setup.cfg +4 -0
- mooncat_browser_client-0.4.7/tests/test_browser.py +184 -0
- mooncat_browser_client-0.4.7/upload.py +103 -0
- mooncat_browser_client-0.4.7/visual_target.py +47 -0
- mooncat_browser_client-0.4.7/visualize.py +91 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mooncat-browser-client
|
|
3
|
+
Version: 0.4.7
|
|
4
|
+
Summary: Python client for the local MoonCat browser daemon
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/NINTING/mooncat-browser
|
|
7
|
+
Project-URL: Repository, https://github.com/NINTING/mooncat-browser
|
|
8
|
+
Project-URL: Issues, https://github.com/NINTING/mooncat-browser/issues
|
|
9
|
+
Keywords: browser,automation,mooncat,browserd
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: httpx<1,>=0.27.0
|
|
20
|
+
Requires-Dist: markdownify<2,>=0.13.0
|
|
21
|
+
|
|
22
|
+
# MoonCat Browser Python Client
|
|
23
|
+
|
|
24
|
+
这是 MoonCat 本地浏览器服务 `browserd` 的 Python 客户端。复杂的浏览器逻辑运行在
|
|
25
|
+
统一后端中,Python 包只负责调用服务接口。
|
|
26
|
+
|
|
27
|
+
## 安装
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
pip install mooncat-browser-client
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
先启动 MoonCat 本地浏览器服务,再运行 Python:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from browser import Browser
|
|
37
|
+
|
|
38
|
+
browser = Browser()
|
|
39
|
+
browser.open()
|
|
40
|
+
tab = browser.new_tab("https://example.com")
|
|
41
|
+
print(browser.inner_text(tab, "body"))
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`new_tab()` 返回包含 `pageHandle` 的标签信息。为方便使用,`operate()`、`click()`、
|
|
45
|
+
`fill()` 等方法既接受完整标签信息,也接受其中的 `pageHandle`。
|
|
46
|
+
|
|
47
|
+
## 服务地址
|
|
48
|
+
|
|
49
|
+
默认连接 `http://127.0.0.1:17322`。可直接指定外部服务端口:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
browser = Browser(host="192.168.1.20", port=17322)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
也可使用完整地址:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
browser = Browser(base_url="http://192.168.1.20:17322")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
支持环境变量:
|
|
62
|
+
|
|
63
|
+
- `MOONCAT_BROWSERD_BASE_URL`
|
|
64
|
+
- `MOONCAT_BROWSERD_HOST`
|
|
65
|
+
- `MOONCAT_BROWSERD_PORT`
|
|
66
|
+
|
|
67
|
+
## 后端接口
|
|
68
|
+
|
|
69
|
+
客户端提供浏览器打开/关闭、标签页、通用页面操作、Cookie、下载任务和自动下载设置接口。
|
|
70
|
+
页面动作统一通过 `operate()` 发送给 browserd,Python 侧不重复实现浏览器逻辑。
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# MoonCat Browser Python Client
|
|
2
|
+
|
|
3
|
+
这是 MoonCat 本地浏览器服务 `browserd` 的 Python 客户端。复杂的浏览器逻辑运行在
|
|
4
|
+
统一后端中,Python 包只负责调用服务接口。
|
|
5
|
+
|
|
6
|
+
## 安装
|
|
7
|
+
|
|
8
|
+
```powershell
|
|
9
|
+
pip install mooncat-browser-client
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
先启动 MoonCat 本地浏览器服务,再运行 Python:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from browser import Browser
|
|
16
|
+
|
|
17
|
+
browser = Browser()
|
|
18
|
+
browser.open()
|
|
19
|
+
tab = browser.new_tab("https://example.com")
|
|
20
|
+
print(browser.inner_text(tab, "body"))
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`new_tab()` 返回包含 `pageHandle` 的标签信息。为方便使用,`operate()`、`click()`、
|
|
24
|
+
`fill()` 等方法既接受完整标签信息,也接受其中的 `pageHandle`。
|
|
25
|
+
|
|
26
|
+
## 服务地址
|
|
27
|
+
|
|
28
|
+
默认连接 `http://127.0.0.1:17322`。可直接指定外部服务端口:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
browser = Browser(host="192.168.1.20", port=17322)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
也可使用完整地址:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
browser = Browser(base_url="http://192.168.1.20:17322")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
支持环境变量:
|
|
41
|
+
|
|
42
|
+
- `MOONCAT_BROWSERD_BASE_URL`
|
|
43
|
+
- `MOONCAT_BROWSERD_HOST`
|
|
44
|
+
- `MOONCAT_BROWSERD_PORT`
|
|
45
|
+
|
|
46
|
+
## 后端接口
|
|
47
|
+
|
|
48
|
+
客户端提供浏览器打开/关闭、标签页、通用页面操作、Cookie、下载任务和自动下载设置接口。
|
|
49
|
+
页面动作统一通过 `operate()` 发送给 browserd,Python 侧不重复实现浏览器逻辑。
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""web/behavior — 人类行为模拟 (路由无关, 全走 operate).
|
|
2
|
+
|
|
3
|
+
忠实复刻 lib/web/behavior.js。
|
|
4
|
+
贝塞尔鼠标轨迹 / 随机点击偏移 / 打字节奏 / 平滑滚动。
|
|
5
|
+
所有页面动作统一走 operate(pageHandle, {action}),本模块只编排动作序列。
|
|
6
|
+
|
|
7
|
+
依赖: browser (operate)
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
import math
|
|
11
|
+
import random
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
sys.path.insert(0, __file__.rsplit("\\", 1)[0] if "\\" in __file__ else __file__.rsplit("/", 1)[0])
|
|
17
|
+
from browser import operate # noqa: E402
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _bezier(p0: float, p1: float, p2: float, p3: float, t: float) -> float:
|
|
21
|
+
u = 1 - t
|
|
22
|
+
return u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _trajectory(from_pt: dict, to_pt: dict, steps: int) -> list[dict]:
|
|
26
|
+
"""生成贝塞尔轨迹点 (含随机抖动)."""
|
|
27
|
+
pts = []
|
|
28
|
+
mx = (from_pt["x"] + to_pt["x"]) / 2 + (random.random() - 0.5) * abs(to_pt["x"] - from_pt["x"]) * 0.6
|
|
29
|
+
my = (from_pt["y"] + to_pt["y"]) / 2 + (random.random() - 0.5) * abs(to_pt["y"] - from_pt["y"]) * 0.6
|
|
30
|
+
c1 = {"x": from_pt["x"] + (mx - from_pt["x"]) * 0.3, "y": from_pt["y"] + (my - from_pt["y"]) * 0.3}
|
|
31
|
+
c2 = {"x": to_pt["x"] + (mx - to_pt["x"]) * 0.3, "y": to_pt["y"] + (my - to_pt["y"]) * 0.3}
|
|
32
|
+
for i in range(steps + 1):
|
|
33
|
+
t = i / steps
|
|
34
|
+
pts.append({
|
|
35
|
+
"x": _bezier(from_pt["x"], c1["x"], c2["x"], to_pt["x"], t) + (random.random() - 0.5) * 2,
|
|
36
|
+
"y": _bezier(from_pt["y"], c1["y"], c2["y"], to_pt["y"], t) + (random.random() - 0.5) * 2,
|
|
37
|
+
})
|
|
38
|
+
return pts
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _random_point_in_rect(rect: dict) -> dict:
|
|
42
|
+
return {
|
|
43
|
+
"x": rect["x"] + rect["width"] * (0.3 + random.random() * 0.4),
|
|
44
|
+
"y": rect["y"] + rect["height"] * (0.3 + random.random() * 0.4),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _move_trajectory(page_handle: dict, from_pt: dict, to_pt: dict) -> None:
|
|
49
|
+
"""沿贝塞尔轨迹多点 mouseMove 注入."""
|
|
50
|
+
pts = _trajectory(from_pt, to_pt, 10 + random.randint(0, 5))
|
|
51
|
+
for pt in pts:
|
|
52
|
+
operate(page_handle, {"action": "mouseMove", "x": pt["x"], "y": pt["y"]})
|
|
53
|
+
time.sleep(0.008 + random.random() * 0.016)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def create_behavior(page_handle: dict) -> dict:
|
|
57
|
+
"""创建绑定到 pageHandle 的行为模拟器.
|
|
58
|
+
|
|
59
|
+
返回: move_to, click, click_element, type_text, type_in_element, scroll_down, scroll_to_bottom, scroll_to_element
|
|
60
|
+
"""
|
|
61
|
+
if not page_handle:
|
|
62
|
+
raise ValueError("create_behavior: page_handle required")
|
|
63
|
+
last_pos = {"x": 100, "y": 100}
|
|
64
|
+
|
|
65
|
+
def move_to(x: float, y: float) -> None:
|
|
66
|
+
nonlocal last_pos
|
|
67
|
+
_move_trajectory(page_handle, last_pos, {"x": x, "y": y})
|
|
68
|
+
last_pos = {"x": x, "y": y}
|
|
69
|
+
|
|
70
|
+
def click(x: float, y: float, options: dict | None = None) -> None:
|
|
71
|
+
nonlocal last_pos
|
|
72
|
+
_move_trajectory(page_handle, last_pos, {"x": x, "y": y})
|
|
73
|
+
last_pos = {"x": x, "y": y}
|
|
74
|
+
time.sleep(0.05 + random.random() * 0.1)
|
|
75
|
+
operate(page_handle, {"action": "click", "x": x, "y": y})
|
|
76
|
+
delay = (options or {}).get("delay", 0.05 + random.random() * 0.1)
|
|
77
|
+
time.sleep(delay)
|
|
78
|
+
|
|
79
|
+
def click_element(selector: str, timeout: int = 10000) -> None:
|
|
80
|
+
nonlocal last_pos
|
|
81
|
+
operate(page_handle, {"action": "waitForSelector", "selector": selector, "timeout": timeout})
|
|
82
|
+
box_r = operate(page_handle, {"action": "boundingBox", "selector": selector})
|
|
83
|
+
box = None
|
|
84
|
+
if isinstance(box_r, dict) and box_r.get("width") is not None:
|
|
85
|
+
box = box_r
|
|
86
|
+
if not box:
|
|
87
|
+
operate(page_handle, {"action": "click", "selector": selector})
|
|
88
|
+
time.sleep(0.05 + random.random() * 0.1)
|
|
89
|
+
return
|
|
90
|
+
pt = _random_point_in_rect(box)
|
|
91
|
+
move_to(pt["x"], pt["y"])
|
|
92
|
+
time.sleep(0.05 + random.random() * 0.1)
|
|
93
|
+
operate(page_handle, {"action": "click", "x": pt["x"], "y": pt["y"]})
|
|
94
|
+
time.sleep(0.05 + random.random() * 0.1)
|
|
95
|
+
|
|
96
|
+
def type_text(text: str, delay: float = 0.1) -> None:
|
|
97
|
+
for ch in text:
|
|
98
|
+
operate(page_handle, {"action": "press", "selector": "body", "key": ch})
|
|
99
|
+
d = delay * (0.5 + random.random())
|
|
100
|
+
if random.random() < 0.1:
|
|
101
|
+
d += 0.2 + random.random() * 0.4
|
|
102
|
+
time.sleep(d)
|
|
103
|
+
|
|
104
|
+
def type_in_element(selector: str, text: str, options: dict | None = None) -> None:
|
|
105
|
+
opts = options or {}
|
|
106
|
+
delay = opts.get("delay", 0.1)
|
|
107
|
+
timeout = opts.get("timeout", 10000)
|
|
108
|
+
operate(page_handle, {"action": "waitForSelector", "selector": selector, "timeout": timeout})
|
|
109
|
+
operate(page_handle, {"action": "click", "selector": selector})
|
|
110
|
+
operate(page_handle, {"action": "type", "selector": selector, "value": text, "delay": delay})
|
|
111
|
+
|
|
112
|
+
def scroll_down(pixels: int = 400) -> None:
|
|
113
|
+
step = 30 + random.randint(0, 29)
|
|
114
|
+
operate(page_handle, {
|
|
115
|
+
"action": "evaluate",
|
|
116
|
+
"source": f"(p) => new Promise((r) => {{ let done = 0; const t = setInterval(() => {{ window.scrollBy(0, {step}); done += {step}; if (done >= p) {{ clearInterval(t); r() }} }}, 30 + Math.random() * 30) }})",
|
|
117
|
+
"args": pixels
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
def scroll_to_bottom() -> None:
|
|
121
|
+
operate(page_handle, {
|
|
122
|
+
"action": "evaluate",
|
|
123
|
+
"source": "() => new Promise((resolve) => { let last=-1; const t=setInterval(()=>{ window.scrollBy(0,100+Math.random()*100); const h=document.body.scrollHeight; if(h===last){clearInterval(t);resolve()} last=h }, 150) })"
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
def scroll_to_element(selector: str, timeout: int = 10000) -> None:
|
|
127
|
+
operate(page_handle, {
|
|
128
|
+
"action": "evaluate",
|
|
129
|
+
"source": f'(s) => {{ const el = document.querySelector(s); if (el) el.scrollIntoView({{ behavior: "smooth", block: "center" }}) }}',
|
|
130
|
+
"args": selector
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
"move_to": move_to, "click": click, "click_element": click_element,
|
|
135
|
+
"type_text": type_text, "type_in_element": type_in_element,
|
|
136
|
+
"scroll_down": scroll_down, "scroll_to_bottom": scroll_to_bottom,
|
|
137
|
+
"scroll_to_element": scroll_to_element,
|
|
138
|
+
}
|