Metadata-Version: 2.4
Name: zschrome
Version: 0.2.1
Summary: ZSChrome — RPA browser automation toolkit for Chrome. Control your own browser with Python via Chrome Extension + WebSocket.
Author-email: ShenWang Holdings <dev@shenwang.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/shenwang/zschrome
Project-URL: Repository, https://github.com/shenwang/zschrome
Keywords: rpa,browser-automation,chrome,web-scraping,crawler
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: websockets>=12.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Provides-Extra: excel
Requires-Dist: xlsxwriter>=3.0; extra == "excel"
Requires-Dist: openpyxl>=3.0; extra == "excel"

# ZSChrome

Python RPA toolkit for Chrome — control your own browser via a Chrome Extension + WebSocket. No WebDriver, no `--remote-debugging-port`. Your cookies, logins, and sessions stay intact.

## Architecture

```
Python script  ←→  WebSocket (ws://127.0.0.1:9527)  ←→  Chrome Extension (MV3)
                                                         ├─ Content Script (DOM read/write)
                                                         └─ CDP (Input.dispatch* → trusted events)
```

- **Content Script** handles DOM queries, locators, data extraction (bypasses CSP `unsafe-eval` restrictions on most pages)
- **CDP (Chrome DevTools Protocol)** dispatches trusted OS-level mouse/keyboard events — sites like Taobao that ignore synthetic events still respond

## Quick Start

### 1. Install

```bash
pip install zschrome        # core
pip install zschrome[excel]  # with Excel export support
```

### 2. Load the Chrome Extension (one-time)

1. Open Chrome → `chrome://extensions/`
2. Enable **Developer mode**
3. Click **Load unpacked** → select the `extension/` directory from this repo
4. The extension badge shows "未连接" — it will connect when your script runs

### 3. Your first script

```python
import asyncio
from zschrome import Browser

async def main():
    browser = await Browser.connect_or_launch("https://www.baidu.com")
    page = browser.current_page

    await page.wait_for_selector("input#kw")
    await page.globalclick("input#kw")
    await page.globaltype("旺旺")
    await page.globalkey("Enter")

    await page.wait_for_selector("#content_left")
    results = await page.eval(
        "document.querySelectorAll('#content_left .result').length"
    )
    print(f"找到 {results} 条结果")
    await browser.close()

asyncio.run(main())
```

## Core API

### Browser lifecycle

```python
# Auto-launch Chrome + connect (recommended)
browser = await Browser.connect_or_launch("https://www.ctrip.com")

# Or connect to an already-running Chrome
browser = await Browser.connect()

page = browser.current_page
await browser.close()
```

### CDP trusted events

Use these on sites that block synthetic DOM events (Taobao, Ctrip, etc.):

```python
# Click by CSS or XPath (auto-detected)
await page.globalclick("input#q")
await page.globalclick('//*[@id="search-btn"]')

# Type text character-by-character
await page.globaltype("机械键盘")

# Press named keys
await page.globalkey("Enter")
await page.globalkey("Tab")
```

### Locator API (Playwright-style)

```python
# Fill an input
await page.locator("#hotels-destination").fill("上海神旺大酒店")

# Click by text
await page.get_by_text("搜索").click()

# Read attributes
val = await page.locator("#checkIn").get_attribute("value")

# Get all text contents in one round-trip
texts = await page.locator(".day-cell").all_text_contents()

# Nth element
await page.locator(".item").nth(2).click()

# Wait for visibility
await page.locator(".results").wait_for("visible", timeout=30)
```

### Waiting & navigation

```python
await page.wait_for_selector("input#kw", timeout=30)
await page.wait_for_selector('//*[@id="results"]', timeout=60)
await page.wait_for_load(timeout=30)
await page.navigate("https://example.com")
```

### Bulk extraction: extract_cards

Quick product/card extraction without manual selector setup:

```python
products = await page.extract_cards()
# → [{"title": "...", "price": "...", "shop": "...", "sales": "...", "link": "..."}, ...]

# Or with custom selectors
products = await page.extract_cards(
    card_selectors=["div[class*='item']", "li[class*='product']"],
    title_selectors=["[class*='title']", "h3"],
    price_selectors=["[class*='price']", "strong"],
)
```

### Bulk extraction: query_all + Field

Precision extraction with container + sub-selector pattern:

```python
from zschrome.selector import Selector
from zschrome.field import Field

fields = {
    "name":     Field.text(sub_selector=".hotel-name span"),
    "star":     Field.html(sub_selector=".star-rating"),
    "room":     Field.text(sub_selector=".room-type"),
    "location": Field.text(sub_selector=".location span"),
    "price_col": Field.text(sub_selector=".price-column"),
}

hotels = await page.query_all(Selector.css("#hotel-list div[id]"), fields)
for h in hotels:
    print(h["name"], h["price_col"])
```

### Tab management

```python
# List all open tabs
tabs = await page.list_tabs()
# → [{"id": 123, "url": "...", "title": "...", "active": true}, ...]

# Switch to a tab
await page.switch_to_tab(tab_id)

# Find a tab by URL
tab_id = await page.find_tab("search")
```

### Scrolling

```python
await page.scroll_to(y=3000)
await page.scroll_to(x=0, y=0)  # back to top
```

### Keyboard

```python
await page.keyboard.press("Enter")
await page.keyboard.press("Control+A")
await page.keyboard.type("hello")
await page.keyboard.insert_text("paste this value")
```

### JavaScript eval (when CSP allows)

```python
# Simple expression
count = await page.eval("document.querySelectorAll('.item').length")

# Arbitrary code
data = await page.eval_raw('''
    const cards = document.querySelectorAll('.card');
    return Array.from(cards).map(c => ({
        title: c.querySelector('.title')?.innerText,
        price: c.querySelector('.price')?.innerText,
    }));
''')
```

Note: `eval`/`eval_raw` are blocked on sites with strict CSP (e.g., Ctrip). Use `query_all`, `extract_cards`, or the Locator API instead.

### Manual mode

```python
await page.wait_manual("Please solve the captcha")
# Shows a prompt in terminal, user presses Enter when ready
```

Or just use Python's `input()`:

```python
print("请在浏览器中完成验证码后按 Enter")
input()
```

## Complete examples

### Baidu search

```python
import asyncio
from zschrome import Browser

async def main():
    browser = await Browser.connect_or_launch("https://www.baidu.com")
    page = browser.current_page

    await page.wait_for_selector("input#kw", timeout=15)
    await page.globalclick("input#kw")
    await page.globaltype("旺旺")
    await page.globalkey("Enter")

    await page.wait_for_selector("#content_left", timeout=15)
    count = await page.eval(
        "document.querySelectorAll('#content_left .result').length"
    )
    print(f"找到 {count} 条结果")

    await browser.close()

asyncio.run(main())
```

### Taobao product crawling with Excel export

```python
import asyncio
from zschrome import Browser

async def main():
    browser = await Browser.connect_or_launch("https://www.taobao.com")
    page = browser.current_page

    keyword = "水神 次氯酸水"
    await page.globalclick("input#q")
    await page.globaltype(keyword)
    await page.globalkey("Enter")
    await asyncio.sleep(3)

    products = await page.extract_cards()
    print(f"提取到 {len(products)} 个商品")

    # Export with openpyxl or xlsxwriter...
    await browser.close()

asyncio.run(main())
```

### Ctrip hotel search with date picker

```python
import asyncio
from zschrome import Browser, Selector, Field

CHECKIN = "2026-05-25"
CHECKOUT = "2026-05-27"
KEYWORD = "上海神旺大酒店"

async def main():
    browser = await Browser.connect_or_launch("https://www.ctrip.com/")
    page = browser.current_page

    # Select dates: click checkIn → calendar popup → click day cells
    await page.globalclick("//*[@id='checkIn']")
    await asyncio.sleep(1.5)
    # ... click calendar day for CHECKIN, then CHECKOUT

    # Search
    await page.wait_for_selector('//*[@id="hotels-destination"]', timeout=60)
    await page.globalclick('//*[@id="hotels-destination"]')
    await page.globaltype(KEYWORD)
    await page.globalclick('//*[@id="kakxi"]/li[6]/div')

    # Extract
    await page.wait_for_selector('//*[@id="hp_container"]/div[2]/div[2]/div[2]/div[3]/div[1]', timeout=60)
    hotels = await page.query_all(
        Selector.css("#hp_container div[id]"),
        {
            "name": Field.text(sub_selector="div:nth-of-type(2) > div:nth-of-type(1) > div:nth-of-type(1) > div > span"),
            "location": Field.text(sub_selector="div:nth-of-type(2) > div:nth-of-type(1) > div:nth-of-type(3) > span:nth-of-type(1)"),
        }
    )
    for h in hotels:
        name = (h.get("name") or "").strip()
        if name:
            print(name)

    await browser.close()

asyncio.run(main())
```

## Project structure

```
zschrome/
├── __init__.py      # Public API exports
├── browser.py       # Browser launch + WebSocket connect
├── page.py          # Page object (all actions)
├── transport.py     # WebSocket server
├── selector.py      # CSS/XPath Selector
├── field.py         # Field extraction definitions
├── locator.py       # Playwright-style Locator
├── keyboard.py      # Keyboard actions
├── errors.py        # Exception hierarchy
└── protocol.py      # Request/response serialization

extension/           # Chrome Extension (MV3), load unpacked

sample/              # Working example scripts
├── search_baidu.py
├── crawl_taobao.py
└── query_hotel.py
```

## Requirements

- Python ≥ 3.10
- Chrome or Chromium
- Chrome Extension loaded in Developer Mode (extension/ folder)
- `websockets` ≥ 12.0 (installed automatically)
- Optional: `xlsxwriter` or `openpyxl` for Excel export

## License

MIT
