おつまみナイト

にほんブログ村 IT技術ブログへ

お酒飲む日はおつまみを2、3品作ってご飯を食べないパターンがわりとあります。
この日はロゼを飲みつつ以下のおつまみセットです。

豚バラの角煮、マッシュポテト、ブッファーラとサラダ。奥は春菊のガーリックソテーかな。
角煮は奥さんの得意技です。普段は炊飯器で通常の角煮を作りますが、この日はワイン用にバルサミコ酢とマーマレードで煮たようです。

ロゼとおつまみセット

ブロトピ:今日のブログ更新
ブロトピ:ブログ更新通知をどうぞ!
ブロトピ:ブログ更新通知
ブロトピ:ブログ更新しました! ブロトピ:今日の料理・グルメ情報

データ抽出してみよう(kinenote編その2)

データ抽出してみよう(kinenote編その2)
にほんブログ村 IT技術ブログへ

目次

  1. 前回までの話
  2. データ再検証
  3. Node.jsの並行処理
  4. 並行実行プロセス数を調節してみる
  5. スピードアップするため、その他の可能性を探る

前回までの話

映画レビューWebサービスのkinenoteのデータを当方で開発したスクレイピングツールを使って実際にデータ抽出してみました。
ツールで真っ正直に直列処理するとCSV保存の場合でもMongoDB保存の場合でも 、
1000件の映画情報をスクレイピングすると約1時間オーバー。
Promise.allで同時並行10件(※)の処理ではCSV保存、MongoDB保存共に 約30分でした。
詳細はこちらに。
(※前回、詳細なロジックを記載してませんでしがた、並行実行はPromise.allで実行しています。また、前回は並列という言葉を使いましたが、正確には並行が正しいようです。)

kinenoteのデータは全件抽出するとなると9万件オーバーなので、並行実行でも単純計算すると45時間掛かる?!ゆえに、【出来ればもう少し】スピードアップを図りたいのです。

データ再検証

ログをもう少し細かく見てみました。
前回は1リクエストの開始から終了までのトータル時間のみ集計しましたが、今度は①スクレイピング処理時間、②外部出力処理時間の二つに分けて集計してみました。
以下がその表です。

kinenoteスクレイピング処理時間

並行実行だと1リクエスト平均10秒位かかってるのですが、その処理の大部分はスクレイピング部分で時間を食っているのが分かります。
なんとなく外部出力(I/O)処理で時間掛かってるのかと思ってたのですが逆でした。
この傾向は直列処理時も同様なので、スクレイピング処理部分がネックなのは確かなようです。

まぁ、http通信してるのでレスポンス待ちで時間掛かってると言われれば、なんとなく納得してしまいそうですが。。。。
でも、並行実行した時と直列実行した時で平均処理時間がこんなに変わる原因って何ですかね?

Node.jsの並行処理

自分なりに調べてみました。
今回のツールではPromise#all()による並行処理を行っています。
並行処理って、処理が「同時進行」で別々に処理されるイメージを勝手に持っていたのですが、そうではなく、処理としては同時に走らせるのですが、それぞれの処理を細かく区切ってちょっとづつ切り替えながら実行していく形なんですね。ある瞬間で見ると、今まさに実行されている処理は一つしかなくて、あたかも同時に実行してるように見えるだけなんです。
なぜそんな動作かというと、Node.js、JavaScriptはシングルスレッドで動作しているからこうなるようです。
まったく別々に同時進行していくのは「並列処理」というもの。 当初私がイメージしていたのはこちらでした。ある瞬間の実行されている処理が複数ある形です。
これはマルチスレッドで実行可能な動きになるようです。

参考にさせてもらったサイトはこちら。

直列、並行と並列の処理の違いは以下のような認識です。
手書きで申し訳ありません。誤字、脱字が目立ちますが。。。
シーケンスが一筆書きなのが直列と並行。完全に二手に分かれるのが並列。

直列、並行、並列処理イメージ
直列、並行、並列処理イメージ

並行実行で10プロセス起動して処理してましたが、1~10のプロセスのうち1のプロセスを実行中は2~10の処理は動かないわけで。そして、その実行中のプロセスが重かった場合、その待ち時間が同時に他のプロセスの待ち時間に加算されていくので、ある意味並行実行するプロセスが多ければ多いほど、後続プロセスの待ち時間は加算されていくのだと思います。

という事で、私のなかでの結論としては、WebWorker等の並列処理を実装しないかぎりは処理のスピードアップは望めないのだろうという事になりました。

並行実行プロセス数を調節してみる

恐らく多ければ多いほど平均の処理時間は多くなっていくものと思います。
トータルの処理時間としては、どうなるのか?変わらない気がしますが。。。
試してみましょう。

並行実行数20で処理

当初10でやってたので倍にした形です。平均処理時間も倍になるでしょうか?

並行実行(CSV保存)時の処理時間比較(同時実行10と20)
並行実行(CSV保存)時の処理時間比較(同時実行10と20)

1リクエスト終了までの平均実行時間がほぼ倍になりました。予想通りになりました。
しかし、全リクエスト終了までのトータル実行時間が2分程短縮されました。
何だこれ?っと思いましたが、よく考えたら1ループ(並行実行)が終わる度にインターバルタイムを2秒設けていたことが原因でした。
並行実行10から20に倍にした事でループ階数が減り、その分インターバルタイムが減ったため、終わるまでのトータル実行時間が2分程減ったというわけです。
というわけで、インターバルを2秒から1秒とかにして、さらに同時並行30位にすればまた少しスピードアップしそうです。
というか、以下である事が分かります。

直列処理、並行処理の時間計算式
直列処理、並行処理の時間計算式

つまり、Node.jsがシングルスレッドで並行処理している限り、1000回のリクエスト処理時間のトータルとしては変わらない。その際に1回のループにセットしたインターバルタイムの秒数分変化があるだけ。と分かりました。
並行実行してインターバル回数が減ればその分だけ、時間削減してるだけという。。。
じゃぁ、何の為の並行実行なんだという感じがしますが、今回のツールのロジックにはそぐわないのかもしれません。

スピードアップするため、その他の可能性を探る

とりあえず、WebWorkerの並列処理というのは有りますが、まずは現状のロジックで何かできないか探ります。
その後、単純なマシンパワーを上げていく力技を試してみようかと。現状、ローカルPCで実行してるのですが、AzureかAWSでCPUをちょっと多く積んだ実行環境を作って試そうかと思います。
その辺はまた次回に。

全データ抽出はもう少し先になりそうです。。。


にほんブログ村テーマ 便利なインターネットプログラミングへ
便利なインターネットプログラミング
ブロトピ:今日のブログ更新
ブロトピ:ブログ更新通知をどうぞ!
ブロトピ:ブログ更新通知
ブロトピ:ブログ更新しました!



金曜日の夜はお酒とともに

ハイボールとラムチョップ

今夜はお酒に合う料理を何品か。
我が家はラム肉好きなので 、いつも冷凍庫にストックしてあります。
お酒はたしなむ程度。週2、3回位?

晩御飯
ラムチョップ
ラムチョップグリル
3本は燻製にしたものを焼き、2本はグリルのみ
サラダ
ブッファーラとアボカドのグリーンサラダ
ドレッシングは自家製です
ウィスキー
ウイスキー水割り

にほんブログ村テーマ 便利なインターネットプログラミングへ
便利なインターネットプログラミング
ブロトピ:今日のブログ更新
ブロトピ:ブログ更新通知をどうぞ!
ブロトピ:ブログ更新通知
ブロトピ:ブログ更新しました!

いつかのご飯

遅めの朝ごはん

朝食3点盛りって感じです。
塩鮭、出し巻き玉子、なすの生姜焼き。
出し巻きと言いつつ、巻かずに2つ折りしただけのやーつ。いつもは3つ折りですが、今回のは更に簡略化バージョンです。(コスト削減)
なすの生姜焼きは奥さん作。うまし!
野菜、玉子、魚と、バランスの良い感じ。
ごちそう様でした!

3点盛り
にほんブログ村テーマ 便利なインターネットプログラミングへ
便利なインターネットプログラミング
ブロトピ:今日のブログ更新
ブロトピ:ブログ更新通知をどうぞ!
ブロトピ:ブログ更新通知
ブロトピ:ブログ更新しました!

データ抽出してみよう(kinenote編)

目次

  1. 概要
  2. データ取得方針
  3. 取得項目
  4. 保存形式
  5. 実行方法
  6. ログ集計コマンド
  7. 実行時間計測結果(1000件)
  8. 全件集計に向けて

概要

開発したスクレイピングツールで色々なサイトのデータを抽出を実際に行い、ツールの精度を高めていこうかと思います。実際に使ってみないと色々な状況に適したものは作れないので。
また、抽出するだけでは面白くないので、抽出したデータを集計、分析してみようかと思います。

開発当初よりテスト対象として使用させてもらっていたキネマ旬報社が提供する映画鑑賞記録サービスの kinenote を今回データ抽出してみます。

kinenote01

データ取得方針

kinenoteは基本的に映画のレビューを複数ユーザーで登録、共有していくサービスです。
そのため映画の基本情報が元データとなり、その下にユーザーレビューデータが紐づいているようです。それぞれの映画タイトルは「cinema_id」という数字のみの形式のIDが振られています。
「cinema_id」をインクリメントしながら順次抽出していけば良さそうです。

cinema_id

取得項目

今回は、映画の基本情報を抽出し、集計していこうかと思います。取得項目は以下のようになります。

項目名 備考
タイトル  
ジャンル  
製作国  
製作年  
監督 複数ある場合でも1名のみ取得
脚本 複数ある場合でも1名のみ取得
出演者 3名のみ取得
あらすじ  

以下は取得項目の入れ物とHTMLの取得部分。CSSセレクターはChromeの開発者ツールで解析してくれたやつをコピペしただけです。

const KinenoteScraping = function(logger){
    this.buffer =  {
        title : "",             // タイトル
        genre : "",             // ジャンル
        productionCountry : "", // 製作国
        productionYear : "",    // 製作年
        director : "",          // 監督
        screenplay : "",        // 脚本
        performer1 : "",         // 出演者1
        performer2 : "",         // 出演者2
        performer3 : "",         // 出演者3
        overview : "",          // あらすじ
    };
    this.log = logger;
};

KinenoteScraping.prototype.do = function(htmldoc, url, cinemaid){
    this.log.debug("スクレイピング処理開始");
    if(htmldoc.$('h1').text() === ""){
        return Promise.reject("HTTP Get Error!!");
    }
    this.buffer['title'] = htmldoc.$('h1').text().trim();
    this.buffer['genre'] = htmldoc.$('#movie > div.movie_info > div.block_info > div.block_right > div:nth-child(3) > div > div > table > tbody > tr:nth-child(1) > td').text().trim();
    this.buffer['productionCountry'] = htmldoc.$('#movie > div.movie_info > div.block_info > div.block_right > div:nth-child(3) > div > div > table > tbody > tr:nth-child(2) > td').text().trim();   
    this.buffer['productionYear'] = htmldoc.$('#movie > div.movie_info > div.block_info > div.block_right > div:nth-child(3) > div > div > table > tbody > tr:nth-child(3) > td').text().trim();
    this.buffer['director'] = htmldoc.$('#movie > div.movie_info > div.block_info > div.block_right > div:nth-child(5) > div > div > table > tbody > tr:nth-child(1) > td').text().trim();
    this.buffer['screenplay'] = htmldoc.$('#movie > div.movie_info > div.block_info > div.block_right > div:nth-child(5) > div > div > table > tbody > tr:nth-child(2) > td').text().trim();
    this.buffer['performer1'] = htmldoc.$('#movie > div.movie_info > div.block_info > div.block_right > div:nth-child(7) > div > div > table > tbody > tr:nth-child(1) > td.setWidth').text().trim();
    this.buffer['performer2'] = htmldoc.$('#movie > div.movie_info > div.block_info > div.block_right > div:nth-child(7) > div > div > table > tbody > tr:nth-child(2) > td.setWidth').text().trim();
    this.buffer['performer3'] = htmldoc.$('#movie > div.movie_info > div.block_info > div.block_right > div:nth-child(7) > div > div > table > tbody > tr:nth-child(3) > td.setWidth').text().trim();
    var tmpText = htmldoc.$('#movie > div.movie_info > h2:nth-child(3)').text().trim();
    if(tmpText === "場面"){
        this.buffer['overview'] = htmldoc.$('#movie > div.movie_info > div:nth-child(10)').text().trim();
    }else{
        this.buffer['overview'] = htmldoc.$('#movie > div.movie_info > div:nth-child(5)').text().trim();
    }
    return Promise.resolve(this.buffer);
};

module.exports = KinenoteScraping;

保存形式

スクレイピングツールは、CSV、TSV、JSONファイルに保存かMongoDBに保存できます。
今回は集計作業をやりやすくするためMongoDBを選択しますが、実行スピードの計測等もしたいので、別途CSVファイルに保存するケースも実行してみたいと思います。

実行方法

ツールでは、大量のHTTPリクエストを発行するので、シリアル(直列)実行かパラレル(並行)実行かを選択できます。
実行スピード計測のため、こちらもシリアルとパラレル両方実行してみたいと思います。
パラレルは同実行数10で試します。
また、1回のリクエストが終了した後、インターバルに2秒の休憩時間を入れます。息継ぎ時間を入れておかないと、向こうのシステムに迷惑が掛かるので。

また、実行スピード計測は全件対象すると大変なので、(どうやら kinenote の登録データは9万件以上あるようです。 )1000件指定とします。
スピード計測方法は以下のようなログの実行時間をあとから集計する形をとります。

[2019-04-02T02:51:48.393] [DEBUG] 2d975e906eb2554c92d48d939e9e7011 – kinenoteデータスクレイピング処理開始
[2019-04-02T02:51:48.405] [DEBUG] 2d975e906eb2554c92d48d939e9e7011 – Paramaters: CinemaId 1

集計にはPowerShellでも良かったのですが、Node.jsで一応以下のように書きました。
最終的にオブジェクトをコンソール出力していますが、PowerShellでCSV形式にして集計する予定です。(ちょっとそこまでNode.jsでやるのが面倒でした。)
PowerShellはコマンド一発でCSV出力できるのが魅力的です。

const fs = require('fs');
var readline = require("readline");
var filePath = process.argv[2];
const LogBuffer = function(logMsg){
    this.log = [logMsg];
}
LogBuffer.prototype.getLog = function(){
    return this.log.sort((aa, bb) => {
        return aa.time.getTime() - bb.time.getTime();
    });
};
LogBuffer.prototype.getTimeDiff = function(){
    var tmpArray = this.getLog();
    return tmpArray[tmpArray.length - 1].time.getTime() - tmpArray[0].time.getTime();
};
var logRecord = new Map();
var stream = fs.createReadStream(filePath, "utf8");
var reader = readline.createInterface({ input: stream });
reader.on("line", (data) => {
    if(data.match(/interval/) !== null){
        return;
    }
    var dataArray = data.split(" ");
    var hashKey1 = dataArray[2];
    var logObj = {
        "time" : new Date(dataArray[0].replace(/[\[\]]/g,"")),
        "type" : dataArray[1].replace(/[\[\]]/g,""),
        "message" : dataArray.splice(4).join(" ")
    };
    if(logRecord.has(hashKey1)){
        logRecord.get(hashKey1).log.push(logObj);
    }else{
        logRecord.set(hashKey1, new LogBuffer(logObj));
    }
});
reader.on('close', function () {
    for(var key of logRecord.keys()){
        var logs = logRecord.get(key).getLog();
        var record = {
            hashid: key,
            cinemaId: logs[1].message.split(" ")[2],
            startTime: logs[0].time.toLocaleTimeString(),
            endTime: logs[logs.length - 1].time.toLocaleTimeString(),
            timeDiff: logRecord.get(key).getTimeDiff() / 1000
        }
        console.log(JSON.stringify(record));
    }
});

ログ集計コマンド

上記スクリプトを以下のように実行してCSVファイルを作成、Excel集計します。

node .\LogParse.js .\parallel_mongodb_WebScraping.log | ConvertFrom-Json | Export-Csv -Path .\parallel_mongodb_WebScraping.csv -NoTypeInformation

実行時間計測結果(1000件)

実行方法保存形式トータル実行時間1リクエストの平均実行時間(秒)
直列CSV1:02:301.7
直列MongoDB1:23:173.0
並列(同時実行10)CSV0:31:059.2
並列(同時実行10) MongoDB0:33:1310.3

トータルの実行時間を見ると、1000件の処理を直列して実行するより10件づつ並列実行する方がやはり早く完了しています。
保存形式で言うと、CSVファイルよりMongoDBに保存した方が若干遅くなるというのは、DBとのやり取りが必要な分遅れが出ているものと思います。
また、1リクエストの平均実行時間で言うと直列より並列の方が遅くなるのはNode.jsがシングルスレッドで処理するからでしょうか?並列で実行しても結局処理の待ち合わせ時間が発生して遅くなっているという事でしょうか?

全件集計に向けて

今のところCSVファイル保存で並列実行するのが早いのは分かっています。
しかし、それでも9万件オーバーの処理だと単純計算で45時間位掛かってしまう事に。。。
同時実行数をさらに上げて試すというのもありですが、時間の短縮にはスクリプトのロジック再検討が必要かもしれません。
その辺はまた次回に。

にほんブログ村テーマ 便利なインターネットプログラミングへ
便利なインターネットプログラミング
ブロトピ:今日のブログ更新
ブロトピ:ブログ更新通知をどうぞ!
ブロトピ:ブログ更新通知
ブロトピ:ブログ更新しました!