ここしばらく、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は通常使う分には、locator
でJSHandle
を隠蔽してくれるので便利なのですが、いざ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 });
参考記事
- https://gist.github.com/florentbr/349b1ab024ca9f3de56e6bf8af2ac69e
- https://github.com/microsoft/playwright/issues/10667