新しいことにはウェルカム

技術 | 電子工作 | ガジェット | ゲーム のメモ書き

Selenium・puppeteer・Playwrightでファイルドロップする

ここしばらく、Selenium・puppeteer・Playwrightの使い方をまとめていました。

まとめが長くなってきたので、ある程度の説明や手順が必要な操作は別記事にまとめようと思います。

ここでは、Selenium・puppeteer・Playwrightそれぞれで、ファイルドロップする方法をまとめました。

はじめに

ファイルドロップとは、ローカルファイルをWebページの指定場所にドロップすると、そのファイルがWebにアップロードされるようなページを想定しています。

Google画像検索のファイルアップロードのようなものです。

そういったファイルドロップする関数が、Selenium・puppeteer・Playwrightにあるものだと思っていたのですがありませんでした。

ですので、要素にローカルファイルをファイルドロップした時に起こっていることを、JavaScriptで再現することによって対応します。

ファイルドロップで起こっていること

要素にファイルがドロップされると、dropイベントが要素に送られます。

dropイベントのイベント情報には、DataTransferオブジェクトが付与されていて、DataTransferにドロップされた情報がfileで格納されています。

ローカルファイルの読み込み

セキュリティ上の理由により、Webページ内部のJavaScriptから、ローカルファイルにアクセスすることはできません。

そこで、Webページ内部とローカルファイルを橋渡しする物として、<input type="file" />ノードを利用します。

Webページ内部のJavaScriptから、<input type="file" />ノードは作成できますが、同じくセキュリティ上の理由により、ローカルファイルのパスの設定は、ブラウザ外部からしか行なえません。

ですので、<input type="file" />ノードを作成した後、ブラウザ外部からローカルファイルパスを設定します。

すると、<input type="file" />ノードはローカルファイルを読み込んでElement.filesに格納してくれます。

流れ

以上を踏まえ、ファイルドロップは下記のような流れになります。

  • <input type="file" />ノードを作成する
  • <input type="file" />ノードにローカルファイルパスを設定する
  • DataTransferオブジェクトを作成する
  • DataTransfer<input type="file" />ノードのfileをセットする
  • dropイベントを作成する
  • dropイベントのイベント情報にDataTransferをセットする
  • ドロップする要素にdropイベントを送る

以下が実装例になります。Google画像検索でファイルドロップしています。書き方は若干異なりますが、やっていることは皆同じです。

Selenium

driver.get('https://www.google.com/imghp')

# open drop area
driver.find_element(
    By.CSS_SELECTOR,
    'form[role="search"] div[role="button"]:last-child'
).click()

# create <input type="file" />
input = driver.execute_script("""
    const _input = document.createElement('INPUT');
    _input.setAttribute('type', 'file');
    document.documentElement.appendChild(_input);
    return _input;
    """)

# set uploadfile path
input.send_keys(os.path.abspath("./data/img/sample.jpg"))

# get drop area
drop = driver.find_element(
    By.CSS_SELECTOR, 'form img:nth-child(1) + div + div')

# dispatch drop event
driver.execute_script("""
    const _drop = arguments[0];
    const _input = arguments[1];
    const _dataTransfer = new DataTransfer();
    _dataTransfer.items.add(_input.files[0]);
    const _event = new DragEvent('drop', {
        dataTransfer: _dataTransfer,
        bubbles: true,
        cancelable: true
    });
    _drop.dispatchEvent(_event);
    """, drop, input)

puppeteer

await page.goto('https://www.google.com/imghp');

let selector;

// open drop area
selector = 'form[role="search"] div[role="button"]:last-child';
await page.waitForSelector(selector);
await page.click(selector);

// create <input type="file" />
const input = await page.evaluateHandle(() => {
    const _input = document.createElement('INPUT');
    _input.setAttribute('type', 'file');
    document.documentElement.appendChild(_input);
    return _input;
}) as ElementHandle<HTMLInputElement>;

// set uploadfile path
await input.uploadFile('./data/img/sample.jpg');

// get drop area
selector = 'form img:nth-child(1) + div + div';
const drop = await page.waitForSelector(selector);

// dispatch drop event
await page.evaluate((_drop, _input) => {
    const _dataTransfer = new DataTransfer();
    _dataTransfer.items.add(_input.files[0]);
    const _event = new DragEvent('drop', {
        dataTransfer: _dataTransfer,
        bubbles: true,
        cancelable: true
    });
    _drop.dispatchEvent(_event);
}, drop, input);

Playwright

await page.goto('https://www.google.com/imghp');

// open drop area
await page.locator('form[role="search"] div[role="button"]:last-child').click();

// create <input type="file" />
const input = await page.evaluateHandle(() => {
    const _input = document.createElement('INPUT');
    _input.setAttribute('type', 'file');
    _input.id = 'id_drop_file';
    document.documentElement.appendChild(_input);
    return _input;
});

// set uploadfile path
await page.locator('#id_drop_file').setInputFiles('./data/img/sample.jpg');

// get drop area
const drop = await page
    .locator('form img:nth-child(1) + div + div')
    .evaluateHandle((dom: Element) => dom);

// dispatch drop event
await page.evaluate(arg => {
    const _drop = arg[0];
    const _input = arg[1] as HTMLInputElement;
    const _dataTransfer = new DataTransfer();
    _dataTransfer.items.add(_input.files[0]);
    const _event = new DragEvent('drop', {
        dataTransfer: _dataTransfer,
        bubbles: true,
        cancelable: true
    });
    _drop.dispatchEvent(_event);
}, [drop, input]);

感想など

Playwrightは通常使う分には、locatorJSHandleを隠蔽してくれるので便利なのですが、いざJSHandleが必要な場面になると、その分書き方がトリッキーになってしまいますね。

Selenium・puppeteer・Playwrightの使い方というより、JavaScriptの話になってしまいました。もっといい方法ないかとChrome DevTools Protocolも見たのですがありませんでした。

他の方法

はじめは、<input type="file" />を介した方法を知らず、プログラム側でファイルを読み込んだ後、そのデータをWebページのJavaScriptに送っていました。

ファイルの情報はバイナリーですが、JavaScriptへはプリミティブなデータしか送れないので、バイナリーを一旦Base64で文字列にして送り、WebページのJavaScript側でバイナリーに復元します。

後はその復元したバイナリーからfileを作成し、DataTransferへとつないでいきます。

何とも力技なのですが、これでも動きます。この試行錯誤の過程で色々勉強になったので、参考までにここに、Playwrightでのそのやり方を残しておきます。

import * as mime from 'mime';

await page.goto('https://www.google.com/imghp');

// open drop area
await page.locator('form[role="search"] div[role="button"]:last-child').click();

// read file & binary to string
const filePath = './data/img/sample.jpg';
const mimeType = mime.getType(path.parse(filePath).ext);
const binaryBase64 = fs.readFileSync(filePath).toString('base64');

// create DataTransfer
const dataTransfer = await page.evaluateHandle((param) => {

    // string to binary
    const _binaryString = atob(param.data);
    const _len = _binaryString.length;
    const _bytes = new Uint8Array(_len);
    for (let i = 0; i < _len; ++i) {
        _bytes[i] = _binaryString.charCodeAt(i);
    }

    // create file
    const _file = new File([_bytes.buffer], param.path, { type: param.type });

    // create DataTransfer
    const _dataTransfer = new DataTransfer();
    _dataTransfer.items.add(_file);

    return _dataTransfer;

}, { data: binaryBase64, path: filePath, type: mimeType });

// dispatch drop event to drop area
await page.locator('form img:nth-child(1) + div + div')
    .dispatchEvent('drop', { dataTransfer });

参考記事

関連カテゴリー記事

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com