Metadata-Version: 2.4
Name: quickplay
Version: 1.2.7
Summary: Add your description here
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: playwright>=1.40
Requires-Dist: selectolax>=0.3
Requires-Dist: pandas>=2.0
Requires-Dist: pyarrow>=14.0
Requires-Dist: camoufox>=0.4

# quickplay

## Overview - 概要

quickplay is a scraping utility library built on Playwright and selectolax.
quickplayはPlaywrightとselectolaxをベースにしたスクレイピングユーティリティライブラリです。

- **PlayPage** — Playwright `Page` のラッパー。スクレイピング用。
- **SelectParser** — selectolax `HTMLParser` のラッパー。ローカル抽出用。
- **browse()** — Playwright（Chromium）起動ランナー。
- **browse_camoufox()** — Camoufox（Firefox）起動ランナー。bot検知対策向け。
- その他ユーティリティ — FromHere, sleep_between, append_csv, write_parquet, hash_name, save_html

## Requirements - 必要条件

- Python 3.12 or higher
- Libraries: playwright, selectolax, pandas, camoufox（自動インストール）
- `write_parquet` を使う場合は pandas の Parquet エンジンとして `pyarrow`（または `fastparquet`）が必要です。
- Browser binaries（別途インストールが必要）

## Installation - インストール

### pip

```
pip install quickplay
```

### uv (推奨)

```
uv add quickplay
```

ブラウザバイナリを別途インストールしてください。

### Playwright（Chromium）

#### pip

```
python -m playwright install chromium
```

#### uv (推奨)

```
uv run playwright install chromium
```

### Camoufox（Firefox）

#### pip

```
camoufox fetch
```

#### uv (推奨)

```
uv run camoufox fetch
```

## Quick Reference - 主要メソッド一覧

### PlayPage のメソッド

- **`ss(selector: str) -> list[ElementHandle]`**  
  指定したCSSセレクタにマッチする**すべての要素**をリストで返します。  
  _例:_ `links = p.ss('a')`

- **`s(selector: str) -> ElementHandle | None`**  
  指定したCSSセレクタにマッチする**最初の要素**を返します。見つからなければ `None`。  
  _例:_ `title_elem = p.s('h1')`

- **`text(elem: ElementHandle | None) -> str | None`**  
  要素からテキスト内容を取得します（前後の空白は除去されます）。  
  _例:_ `title = p.text(p.s('h1'))`

- **`attr(attr_name: str, elem: ElementHandle | None) -> str | None`**  
  要素の指定された属性値を取得します。  
  _例:_ `href = p.attr('href', link_elem)`

- **`url(elem: ElementHandle | None) -> str | None`**  
  リンク要素 (`<a>`) の `href` を**絶対URL**に正規化して返します。無効なリンク（`javascript:` など）は除外されます。  
  _例:_ `next_url = p.url(p.s('a.next'))`

- **`goto(url: str | None) -> bool`**  
  指定したURLに移動します。成功すれば `True`、失敗すれば `False` を返します。  
  _例:_ `if p.goto('https://example.com'): ...`

### SelectParser のメソッド

- **`nxt(self, selector: str, node: LexborNode | None) -> LexborNode | None`**  
  ノードから、セレクタに一致する最初の弟ノードを取得します。  

- **`txt(self, node: LexborNode | None) -> str | None`**  
  ノードからテキスト内容を(子孫ノードまで全て含め)取得します（前後の空白は除去されます）。  

### ユーティリティ関数

- **`sleep_between(a: float, b: float) -> None`**  
  `a` 〜 `b` 秒の間でランダムに待機します。サーバーに負荷をかけないための基本的なマナーです。  
  _例:_ `sleep_between(1, 2)`

- **`append_csv(path: Path | str, row: dict) -> None`**  
  `dict` 形式のデータを1行としてCSVファイルに追記します。ファイルが存在しない場合はヘッダーも自動で書き込みます。  
  _例:_ `append_csv('data.csv', {'name': '太郎', 'age': 20})`

- **`write_parquet(path: Path | str, rows: list[dict]) -> None`**  
  `dict` のリストを1つの Parquet ファイルに書き出します。  
  _例:_ `write_parquet('data.parquet', [{'name': '太郎', 'age': 20}])`

- **`browse(fn: Callable[[Page], None], ...) -> None`**  
  Playwrightのブラウザを起動し、引数で渡した関数を実行します。`headless` や `user_agent` などのオプションを指定できます。  
  _例:_ `browse(scrape, headless=True, block_resources={'image'})`  
  _引数:_

  ```py
  def browse(
      # scrape(page) のような関数を渡す。
      fn: Callable[[Page], None],
      *,
      # ヘッドレスモードにするか。
      headless: bool = False,
      # ブラウザチャンネル（'chrome' など）。
      channel: str = 'chrome',
      # {'width': 1920, 'height': 1080} など。Noneなら未設定。
      viewport: dict | None = {'width': 1920, 'height': 1080},
      # User-Agent文字列。Noneなら未設定。chrome://version/で確認できる。
      user_agent: str | None = None,
      # Accept-Languageヘッダー。Noneなら未設定。
      accept_language: str | None = 'ja-JP,ja;q=0.9',
      # デフォルトタイムアウト（ミリ秒）。
      timeout: int = 15000,
      # ブロックするリソースタイプ。例: {'image'}。
      block_resources: set[str] | None = None,
  ) -> None:
  ```

- **`browse_camoufox(fn: Callable[[Page], None], ...) -> None`**  
  Camoufox（Firefox）でブラウザを起動し、引数で渡した関数を実行します。bot検知が厳しいサイト向け。  
  _例:_ `browse_camoufox(scrape, humanize=True, block_images=True)`  
   _引数:_
  ```py
  def browse_camoufox(
      # scrape(page) のような関数を渡す。
      fn: Callable[[Page], None],
      *,
      # ヘッドレスモードにするか。
      headless: bool | Literal['virtual'] = False,
      # ブラウザのロケール（言語・地域設定）を指定
      # 英語サイト中心なら `'en-US,en'` への変更を検討
      locale: str | list[str] | None = 'ja-JP,ja',
      # カーソルの動きを人間らしく模倣するかどうか。
      # `True`（デフォルト）: デフォルト設定（最大約1.5秒）で有効化。
      # `False`: 無効。カーソルが瞬時に移動する。高速化したい場合やカーソル操作をしないスクレイピングに
      # `float`: カーソル移動の最大秒数を指定して有効化。例: `2.0` なら最大2秒かけて移動。
      humanize: bool | float = True,
      # 画像リソースのリクエストをすべてブロックするかどうか。
      # `False`（デフォルト）: 画像を読み込む。
      # `True`: 画像をすべてブロック。
      block_images: bool = False,
      # `False`（デフォルト）: COOPを有効のまま。通常はこれでよい。
      # `True`: COOPを無効化。主な用途はCloudflareのTurnstile（チェックボックス認証）の突破。
      # 注意: セキュリティポリシーを緩める設定なので、必要な場合だけ使う。
      disable_coop: bool = False,
      # デフォルトタイムアウト（ミリ秒）。
      timeout: int = 15000,
      **kwargs,
  ) -> None:
  ```

## Basic Usage - 基本的な使い方

```python
from quickplay import *

fh = FromHere(__file__)

def scrape(page):
    p = PlayPage(page)
    p.goto('https://www.foobarbaz1.jp')

    pref_urls = [p.url(e) for e in p.ss('li.item > ul > li > a')]

    classroom_urls = []
    for i, url in enumerate(pref_urls, 1):
        print(f'{i}/{len(pref_urls)} pref_urls')
        if not p.goto(url):
            continue
        sleep_between(1, 2)
        links = [p.url(e) for e in p.ss('.school-area h4 a')]
        classroom_urls.extend(links)

    for i, url in enumerate(classroom_urls, 1):
        print(f'{i}/{len(classroom_urls)} classroom_urls')
        if not p.goto(url):
            continue
        sleep_between(1, 2)
        append_csv(fh('csv/out.csv'), {
            'URL': page.url,
            '教室名': p.text(p.s('h1 .text01')),
            '住所': p.text(p.s('.item .mapText')),
            '電話番号': p.text(p.s('.item .phoneNumber')),
            'HP': p.url(p.s_in('a', p.next(p.s_re('th', 'ホームページ')))),
        })


if __name__ == '__main__':
    browse(
        scrape,
        user_agent='Mozilla/5.0 ...',
        block_resources={'image'},
    )
```

## Save HTML while scraping - スクレイピングしながらHTMLを保存する

```python
from quickplay import *

fh = FromHere(__file__)

def scrape(page):
    p = PlayPage(page)
    p.goto('https://www.foobarbaz1.jp')

    item_urls = [p.url(e) for e in p.ss('ul.items > li > a')]

    for i, url in enumerate(item_urls, 1):
        print(f'{i}/{len(item_urls)} item_urls')
        if not p.goto(url):
            continue
        sleep_between(1, 2)
        if not p.wait('#logo', timeout=10000):
            continue
        file_name = f'{hash_name(url)}.html'
        if not save_html(fh('html') / file_name, page.content()):
            continue
        append_csv(fh('outurlhtml.csv'), {
            'URL': url,
            'HTML': file_name,
        })

if __name__ == '__main__':
    browse(scrape, block_resources={'image'})
```

## Scrape from local HTML files - 保存済みHTMLからスクレイピングしてParquetに出力する

```python
import pandas as pd

from quickplay import *

fh = FromHere(__file__)
p = SelectParser()

df = pd.read_csv(fh('outurlhtml.csv'))
results = []
for i, (url, path) in enumerate(zip(df['URL'], df['HTML']), 1):
    print(i)
    if not p.load(fh('html') / path):
        continue
    results.append({
        'URL': url,
        '教室名': p.txt(p.s('h1 .text02')),
        '住所': p.txt(p.s('.item .mapText')),
        '所在地': p.txt(p.nxt('dd', p.s_re('dt', r'所在地'))),
    })
write_parquet(fh('outhtml.parquet'), results)
```

## License - ライセンス

[MIT](./LICENSE)

