# 外部システムの操作にchromedpを採用してみた <!-- --> --- ## 皆さんこんにちは! :wave: --- <!-- 初めましての方も多いと思いますので自己紹介させて いただきます。 フリーランスのバックエンドエンジニアをやっております 音川といいます。 --> My Profile Otogawa Katsutoshi freelance backend enginner interesting golang, python, and more... [twitter account](https://twitter.com/k_otogawa) --- <!-- Setting 舞台 --> ## 突然ですがみなさん!スクレイピングはやったことありますか? :sunglasses: --- ## 私はたまにエロ画像収集に使っています! ![えっち](https://hackmd.io/_uploads/rkE3CHWKh.png =x400) --- <!-- 今回私が何しに来たか?というと --> ## 今回何しに来たか?というと ブラウザを使った、スクレイピングによくpythonやjsが使われるけど、golangのchromedpというライブラリを使う方が良いんじゃないか?という紹介に来ました。 ## 要するに --- ## chromedpを使ったスクレイピングの布教に来ました ![布教](https://i.imgur.com/FqNC6gr.png =x500) 入信したら幸せになれます! --- <!-- ここでchromedp以前に --> ## そもそもスクレイピングが必要な環境でない ![遠慮する](https://hackmd.io/_uploads/SJNeOvAF3.png =x400) --- ## 欲しい情報や操作はapiで公開されいる <!-- し、 --> 相当大規模にやらないと必要無いっしょ? ![呆れ](https://i.imgur.com/eoBvcN5.png =x500) <!-- と思われる方も多いと思います。 そんな事ありません! --> --- ## 現実として 必要なapiを公開しなかったり、~~クソな~~異様に使いづらかったり、api自体に~~バグくさい~~不思議な仕様があるときに必要になる ![インシデント](https://hackmd.io/_uploads/H19EHBqDn.png =x400) <!-- 自分の経験だと外部決済のサービスで決済日前日に事務員とバイトとエンジニア一人と、役員みんなで管理画面叩いているという地獄を見た事がある。顧客が数千人とかならまだ可能なので。 --> --- <!-- この環境なら --> ## ブラウザを操作できたら勇者になれる! ![勇者](https://hackmd.io/_uploads/BJWUBvCYh.png =x400) <!-- これビジネス的に結構重要で、わかりやすいIT化によるコスト削減なので、 エンジニア側も単価交渉しやすい。 役員になぜお金使ったか説明しやすい。 --> --- ## ということで、ブラウザのスクレイピング方法を覚えて単価を上げよう --- <!-- そもそも論ですが、 --> ## chromedp以前になぜgolangか? ![問題提起](https://i.imgur.com/N4Umxfe.png =x500) <!-- それ以前に --> --- ## そもそも言語による違い ブラウザを動かすのに、速度やメモリの言語の違いが出るのか? 重さがほとんどブラウザだろうから、どの言語でやってもあまり変わらないんじゃないか? ![問題提起](https://i.imgur.com/N4Umxfe.png =x500) <!-- という方もいると思います。 --> --- <!-- 昔、java, dotnet, nodejs, python, goで書き比べてみたんですが、 --> ## 結構差が出る スクレイピング的にメモリも速度もだいたい java > go = dotnet >= nodejs > python だった。 <!-- 言語による書き方の違い以外は pythonはよくわからないけどクラッシュする時がある。時間経過とともにメモリ使用量が増えていって怖い。バグ臭いので今は治っているかも。 javaは意外と悪く無いけど、ソースコードが多くなると、管理がきつかった。 dotnetはjavaよりもリソースの解放のタイミングが遅い。 クラス設計真面目にやるとクソだるいコードが多くなってくる。 nodejsはコールバック地獄と非同期処理がめちゃくちゃ表に出てくるのとエラー処理がきつかった。 async awaitだらけなの目がキツいのと、それがキツくならないようにclassと関数設計して分けるのが難しくて、時間考えたらそんなメリットないんじゃ無いかなと。 ただ、nodejsで書いたら、スクリプト言語としてはインチキ疑うぐらいパフォーマンス出たから、みんながnodejs得意なら採用しても良いかも。 --> --- ## goが一番バランス取れてそう 1. 軽さの割にメモリ使用量が少ない <!-- 速度でても、結局待ち時間が発生するし、向こうのサイトに迷惑かけたらいけないので、軽さに対するメモリ使用量が割と重要かなと。 --> 2. 型がある 3. コールバックがあってもそんなに複雑にならない ![](https://hackmd.io/_uploads/rJFosdAF2.png =x400) <!-- ということで、goを採用することにしました。 なんでchromedpを採用したかというと --> --- ## chromedpのいいところ 1. 各処理をタスクとして切り分けれる。 2. タスクとタスクを合わせてタスクを作れる <!-- 他のselenium一族や、puppeteerではオブジェクト指向と言いつつ、ちゃんとクラス設計や関数の設計しないと結局縦に処理続いているだけの長い手続き型な書き方になる。 --> --- ## puppeteerなどだと直列で処理をまとめて書くのが難しい loginSiteTasksとisSessionVertificationTasksは Promise\<Page\>を引数として受け取って、各処理を行う関数として考える。 ```js const puppeteer = require('puppeteer'); const browser = await puppeteer.launch({headless: false}); const page = await browser.newPage(); // こういう風にかきたい...関数にまとめてところで、PromiseAllは順番に実行が保証されない。 const [_, valid] = await Promise.all([ loginSiteTasks(page), isSessionVertificationTasks(page), ]) ``` --- 結局async awaitだらけの縦に長いソースコードになる。 ```js= ...中略 // 結局順番に処理させるために全部awaitつけることになる await loginSiteTasks(page); await isSessionVertificationTasks(page) ``` <!-- よって --> --- ## 処理を関数にまとめるときにしんどい --- ## なぜしんどいか 各処理を行うのにPromise\<Page\>が必要だが、簡単にまとめる方法がpuppeteerから提供されていないから。 まとめるのにテクニックが必要。 --- ## chromedpだと chromedp.Tasksとして提供されている。 ```go= ctx, cancel := chromedp.NewContext(context.Background()) // 中略 ... // taskを配列として渡せる tasks := chromedp.Tasks{ chromedp.Navigate(url), chromedp.Sleep(15*time.Minute) } // 渡した配列のタスクを処理。 chromedp.Run(ctx,tasks) ``` --- 可変長で配列のタスクを渡せる ```go= var tasks1, tasks2 chromedp.Tasks // 中略 初期化 ... // 渡したタスクを全て実行 chromedp.Run(ctx, tasks1, tasks2) ``` --- 自作のタスクが欲しい場合は chromedp.ActionFuncで簡単に自作できる。 ```go= // これだけでchromedp.Tasks型の変数になる task := chromedp.ActionFunc(func(ctx context.Context) error { err := chromedp.Click(sel).Do(ctx) if err != nil { log.Println("クリックできませんでした。", err) return err } return nil }) ``` --- なので、chromedp.Tasksを戻り値に持つ関数を作れば簡単に処理をまとめられる。 ```go= func MovePageTasks() chromedp.Tasks { return chromedp.Tasks{ chromedp.Navigate(url), chromedp.ActionFunc(func(ctx context.Context) error { err := chromedp.Sleep(waitTime).Do(ctx) if err != nil { log.Println("待てませんでした。", err) return err } log.Printf("%v待ちました。", waitTime) return nil }), } } ``` --- <!-- これ昔描いたスクレイピングのテストなんですが、 --> LoginSiteTasksとIsSessionVertificationTasksは chromedp.Tasksを返す各処理を行う関数として考える。 ```go= ctx, cancel := chromedp.NewContext(context.Background()) // 中略 ... var valid bool // 各処理をタスクという形で可変長引数として渡せる // この可変長引数は若い順番から実行されることが保証されている。 err := chromedp.Run(ctx, LoginSiteTasks(), // Loginした後にsessionが有効になったかチェックする。 IsSessionVerificationTasks(&valid), ) // エラーが出た。 if err != nil { log.Fatal(err) t.Errorf("TestLoginTasks() = %v", err) } if valid == tt.want { t.Errorf("TestLoginTasks() = %v, want %v", valid, tt.want) } ``` --- ## まとめ <!-- に入らせて頂きますと、 --> 1. chromedp良いよ! 2. タスクごとに処理分けれる。 3. 直列な処理が保証できる。 <!-- という事だけ覚えて頂けたら幸いです。 ご清聴ありがとうございました。 --> ![](https://i.imgur.com/c1s1RCE.png =x500)
{"breaks":true,"slideOptions":"{\"controls\":false,\"slideNumber\":false,\"progress\":true}","title":"外部システムの操作にchromedpを採用してみた","contributors":"[{\"id\":\"13bbf9e7-416d-4813-946c-591c31f51efc\",\"add\":14994,\"del\":9652}]"}
    429 views