Skip to main content

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

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

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

# 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.):

# 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)

# 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

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:

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:

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

# 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

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

Keyboard

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)

# 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

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

Or just use Python's input():

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

Complete examples

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

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

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

Metadata

Release files for zschrome 0.3.9

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for zschrome 0.3.9
File Size Uploaded Provenance Actions
zschrome-0.3.9.tar.gz 32.9 kB Without provenance Details for zschrome-0.3.9.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for zschrome 0.3.9
File Interpreter ABI Platform Actions
zschrome-0.3.9-py3-none-any.whl Python 3 none any Details for zschrome-0.3.9-py3-none-any.whl

Total release size: 60.9 kB

Release files / zschrome-0.3.9.tar.gz

Download URL zschrome-0.3.9.tar.gz
Size 32.9 kB
Tags Source
SHA-256 checksum
How to use checksums
75a0b345b8cc06d352a660d18c39e872bcd44fbaf92a3a1dde1e909b51619800
BLAKE2b-256 checksum
How to use checksums
60dfe3505b261fdb31030ba162a062c6fa6e7591ecea4432d084cc0b225d19b5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.3

Release files / zschrome-0.3.9-py3-none-any.whl

Download URL zschrome-0.3.9-py3-none-any.whl
Size 28.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a3dd9e9bef017ee48d5741821195f637e2c9eee9918c293a0c343360dc19822c
BLAKE2b-256 checksum
How to use checksums
d1d56eed422156235f469d2d0aeb511cbf9023a806747d3c2740dd08e8f8b3ac
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.3

Security for zschrome 0.3.9

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page