【Java】Javaで外部プロセスを起動する

Javaで外部プロセス(外部のexeファイル)を起動します。
外部プロセスとはxcopyコマンドやffmpegコマンド等です。

例として、ffmpegでmp4をmp3に変換します。
ffmpegは最新版zipを解凍後のフォルダ(D:/DownloadFiles/freesoft/ffmpeg.exe)に存在し、
パスは通していないものとします。

  • 外部プロセス起動設定

    // 外部プロセスの起動のためのコマンドを設定する。
    // Listに設定するものはスペースを含めないように設定する。
    // 今回の例では「D:/DownloadFiles/freesoft/ffmpeg -i D:/test.mp4 -ab 128 D:/test.mp3」となる。
    String mp4File = "D:/test.mp4";
    String mp3File = "D:/test.mp3";
    List<String> cmd = new ArrayList<String>();
    // 「cmd /C」は必要に応じて設定する。
    // echoのようにコマンドプロントが解釈するコマンドを実行する場合に有効です。
    // cmd.add("cmd"); // コマンドプロンプトを起動する。
    // cmd.add("/C"); // コマンドを実行した後に終了を指定する。
    cmd.add("D:/DownloadFiles/freesoft/ffmpeg");
    cmd.add("-i");
    cmd.add(mp4File);
    cmd.add("-ab");
    cmd.add("128");
    cmd.add(mp3File);
    ProcessBuilder processBuilder = new ProcessBuilder(cmd);
    // 標準エラーを標準出力にマージする。
    processBuilder.redirectErrorStream(true);
    
  • 外部プロセス起動その1
    JDK1.7以上であれば今回の方法を選択すると
    外部プロセスの標準エラーを標準出力にマージする設定を活かしつつ、
    出力先を呼び出し側のJavaプロセスに設定できます。

    // 標準出力(標準エラー含む)の内容の出力先をJavaプロセスと同一にする。
    processBuilder.inheritIO();
    // 外部プロセスを起動する。
    Process process = processBuilder.start();
    // 外部プロセスの終了を待機する。
    // この設定を行わないと、「while ((line = br.readLine()) != null)」がないため、
    // Javaプロセスの方が先に処理が進み、「process.exitValue();」で
    // 「process has not exited」というエラーになります。
    process.waitFor();
    // 外部プロセスの結果を出力する。
    int ret = process.exitValue();
    System.out.println("結果:" + ret);
    
  • 外部プロセス起動その2
    外部プロセスの標準エラーを標準出力にマージする設定を活かしつつ、
    出力先を別ファイルに指定できます。

    // 標準出力(標準エラー含む)の内容の出力先を別ファイルに指定する。
    processBuilder.redirectOutput(new File("D:/process.log"));
    // 外部プロセスの終了を待機する。
    process.waitFor();
    // 外部プロセスの結果を出力する。
    int ret = process.exitValue();
    System.out.println("結果:" + ret);
    
  • 外部プロセス起動(非推奨な方法1)
    この方法の欠点として、InputStreamは1024バイトを超える量が保持できないため、
    ロック状態になる可能性があるようです。
    JavaAPIの仕様にも記載があります。

    // 外部プロセスを起動する。
    Process process = processBuilder.start();
    // 標準出力と標準エラーを受け取る。
    InputStream inputStream = process.getInputStream();
    // 受け取ったInputStreamを出力し、外部プロセスの結果を出力する。
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
    String line = null;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
    int ret = process.exitValue();
    System.out.println("結果:" + ret);
    
  • 外部プロセス起動(非推奨な方法2)
    JDK1.4まではRuntimeで起動させていたようですが、
    JDK1.5からはProcessBuilderが内部的にRuntimeを呼び出しているのと、
    標準エラーの内容を標準出力にマージできるようになったりとしているため、
    以下の呼び出し方法は非推奨なようです。

    String cmd = "D:/DownloadFiles/freesoft/ffmpeg -i " + mp4File + " -ab 128 " + mp3File;
    Process process = Runtime.getRuntime().exec(cmd);
    

【ニコニコ動画】コメント取得

ログインしたセッション、コメントサーバURL、ユーザID、スレッドIDを利用し、
コメントを取得します。

  • HTTP通信開始

    // HTTP通信を開始する。
    URL url = new URL(commentServer);
    HttpURLConnection http = (HttpURLConnection)url.openConnection();
    
  • リクエストヘッダ部設定

    // リクエストヘッダ部を設定する。
    http.setRequestMethod("POST");
    http.setDoOutput(true);
    // XML形式を指定する。
    http.setRequestProperty("Content-Type", "text/xml");
    // セッションと動画用クッキーを設定する。
    http.setRequestProperty("Cookie", "user_session=" + userSession);
    
  • リクエストボディ部設定

    // リクエストボディ部を設定する。
    PrintStream ps = new PrintStream(http.getOutputStream());
    StringBuilder xml = new StringBuilder();
    xml.append("<thread ");
    // スレッドIDを設定する。
    xml.append("thread=\"" + threadId + "\" ");
    // 固定値を設定する。
    xml.append("version=\"20061206\" ");
    // 取得コメント数を設定する。一般会員の場合、最大で1000件となる。
    xml.append("res_form=\"-100\" ");
    // ユーザIDを設定する。
    xml.append("user_id=\"" + userId + "\" ");
    xml.append("/>");
    ps.print(xml.toString());
    ps.close();
    
  • レスポンスボディ部取得

    // レスポンスボディ部を取得する。
    String comment = null;
    BufferedReader br = new BufferedReader(new InputStreamReader(http.getInputStream()));
    while((comment = br.readLine()) != null) {
        // XML形式のため、整形する必要がある。
        System.out.println(comment);
    }
    br.close();
    
  • レスポンスヘッダ部の取得は不要です。

【ニコニコ動画】動画取得

ログインしたセッション、動画サーバURL、動画拡張子、動画用クッキーを利用し、
動画を取得します。
一般会員、プレミアム会員によって動画サイズが異なる事象あり。
プレミアム会員の方が解像度が高そうです。

  • HTTP通信開始

    // HTTP通信を開始する。
    URL url = new URL(movieServer);
    HttpURLConnection http = (HttpURLConnection)url.openConnection();
    
  • リクエストヘッダ部設定

    // リクエストヘッダ部を設定する。
    http.setRequestMethod("GET");
    // セッションと動画用クッキーを設定する。
    http.setRequestProperty("Cookie", "user_session=" + userSession + "; nicohistory=" + nicoHistory);
    
  • GET通信のため、リクエストボディ部の設定は不要です。

  • レスポンスボディ部取得

    // レスポンスボディ部を取得する。
    InputStream in= http.getInputStream();
    File file = new File("D:/", "sm12345678" + movieExtension);
    Files.copy(in, file.toPath());
    in.close();
    
  • レスポンスヘッダ部の取得は不要です。

【ニコニコ動画】動画用クッキー取得

ログインしたセッションを維持して、
「http://www.nicovideo.jp/watch/sm12345678」にGET通信し、
動画用のクッキーを取得します。

  • HTTP通信開始

    // HTTP通信を開始する。
    URL url = new URL("http://www.nicovideo.jp/watch/sm12345678");
    HttpURLConnection http = (HttpURLConnection)url.openConnection();
    
  • リクエストヘッダ部設定

    // リクエストヘッダ部を設定する。
    http.setRequestMethod("GET");
    // セッションを設定する。
    http.setRequestProperty("Cookie", "user_session=" + userSession);
    
  • GET通信のため、リクエストボディ部の設定は不要です。

  • レスポンスヘッダ部取得

    // レスポンスヘッダ部を取得する。
    String nicoHistory = null;
    Map<String, List<String>> responseHeaders = http.getHeaderFields();
    Iterator<String> responseIt = responseHeaders.keySet().iterator();
    while (responseIt.hasNext()) {
        String responseKey = responseIt.next();
        List<String> responseList = responseHeaders.get(responseKey);
        for(String reponseValue: responseList) {
            System.out.println(responseKey + ":" + reponseValue);
            if ("Set-Cookie".equals(responseKey)) {
                String[] cookieAry = reponseValue.split(";");
                for (String cookie: cookieAry) {
                    String[] nicoHistoryAry = cookie.split("=");
                    if ("nicohistory".equals(nicoHistoryAry[0])) {
                        nicoHistory = nicoHistoryAry[1];
                    }
                }
            }
        }
    }
    
  • レスポンスボディ部の取得は不要です。

【ニコニコ動画】動画情報取得

ログインしたセッションを維持して、
「http://flapi.nicovideo.jp/api/getflv/sm12345678」にGET通信し、
以下の動画情報を取得します。

  • 動画サーバURL
    APIのレスポンスボディ部のうち、
    「url=http%3A%2F%2Fsmile-pow64.nicovideo.jp%2Fsmile%3Fm%3D11111111.11111」を取得します。
    URLデコードしておく必要があります。
    「http://smile-pow64.nicovideo.jp/smile?m=11111111.11111」となります。
  • 動画拡張子
    動画サーバURLのパラメータから判断します。
    パラメータが「m」の場合、動画拡張子が「.mp4」、
    パラメータが「s」の場合、動画拡張子が「.swf」、
    パラメータが上記以外の場合、動画拡張子が「.flv」になります。
  • コメントサーバURL
    APIのレスポンスボディ部のうち、
    「ms=http%3A%2F%2Fnmsg.nicovideo.jp%2Fapi%2F」を取得します。
    URLデコードしておく必要があります。
  • ユーザID
    APIのレスポンスボディ部のうち、
    「user_id=11111」を取得します。
  • スレッドID
    APIのレスポンスボディ部のうち、
    「thread_id=1111111111」を取得します。
  • HTTP通信開始

    // HTTP通信を開始する。
    URL url = new URL("http://flapi.nicovideo.jp/api/getflv/sm12345678");
    HttpURLConnection http = (HttpURLConnection)url.openConnection();
    
  • リクエストヘッダ部設定

    // リクエストヘッダ部を設定する。
    http.setRequestMethod("GET");
    // セッションを設定する。
    http.setRequestProperty("Cookie", "user_session=" + userSession);
    
  • GET通信のため、リクエストボディ部の設定は不要です。

  • レスポンスボディ部取得

    // レスポンスボディ部を取得する。
    String str = null;
    String movieServer = null;
    String movieExtension = null;
    String commentServer = null;
    String userId = null;
    String threadId = null;
    BufferedReader br = new BufferedReader(new InputStreamReader(http.getInputStream()));
    while((str = br.readLine()) != null) {
        System.out.println(str);
        System.out.println();
    
        String[] responseBodyAry = str.split("&");
        for (String responseBody: responseBodyAry) {
            String[] elementAry = responseBody.split("=");
            if ("url".equals(elementAry[0])) {
                movieServer = URLDecoder.decode(elementAry[1], "UTF-8");
                String extensionChk = movieServer.substring(movieServer.indexOf("?")+1, movieServer.indexOf("?")+2);
                if ("m".equals(extensionChk)) {
                    movieExtension = ".mp4";
                } else if ("s".equals(extensionChk)) {
                    movieExtension = ".swf";
                } else {
                    movieExtension = ".flv";
                }
            }
            if ("ms".equals(elementAry[0])) {
                commentServer = URLDecoder.decode(elementAry[1], "UTF-8");
            }
            if ("user_id".equals(elementAry[0])) {
                userId = URLDecoder.decode(elementAry[1], "UTF-8");
            }
            if ("thread_id".equals(elementAry[0])) {
                threadId = URLDecoder.decode(elementAry[1], "UTF-8");
            }
        }
    }
    br.close();
    
  • レスポンスヘッダ部の取得は不要です。

【ニコニコ動画】ログイン処理

ニコニコ動画にHTTP通信でログインし、セッションを取得します。

  • HTTP通信開始

    // HTTP通信を開始する。
    URL url = new URL("https://secure.nicovideo.jp/secure/login?site=niconico");
    String params = String.format("mail=%s&password=%s", "niconico@gmail.com", "password");
    HttpsURLConnection https = (HttpsURLConnection)url.openConnection();
    
  • リクエストヘッダ部設定

    // リクエストヘッダ部を設定する。
    https.setRequestMethod("POST");
    https.setDoOutput(true);
    https.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
    https.setRequestProperty("Content-Length", Integer.toString(params.length()));
    
  • リクエストボディ部設定

    // リクエストボディ部を設定する。
    PrintStream ps = new PrintStream(https.getOutputStream());
    ps.print(params);
    ps.close();
    
  • レスポンスヘッダ部取得

    // レスポンスヘッダ部を取得する。
    String userSession = null;
    Map<String, List<String>> responseHeaders = https.getHeaderFields();
    Iterator<String> responseIt = responseHeaders.keySet().iterator();
    while (responseIt.hasNext()) {
        String responseKey = responseIt.next();
        List<String> responseList = responseHeaders.get(responseKey);
        for(String reponseValue: responseList) {
            System.out.println(responseKey + ":" + reponseValue);
            if ("Set-Cookie".equals(responseKey)) {
                String[] cookieAry = reponseValue.split(";");
                for (String cookie: cookieAry) {
                    String[] userSessionAry = cookie.split("=");
                    if ("user_session".equals(userSessionAry[0]) 
                            && userSessionAry[1].indexOf("deleted") == -1) {
                        userSession = userSessionAry[1];
                    }
                }
            }
        }
    }
    
  • レスポンスヘッダは以下のように出力されます。
    HTTPステータスコードが302なので、
    Locationに指定されたニコニコ動画のトップ画面にリダイレクトしているのが分かります。

    null:HTTP/1.1 302 Found
    x-niconico-authflag:0
    x-niconico-authflag:0
    Content-Language:ja
    Date:Fri, 28 Apr 2017 13:39:03 GMT
    Content-Length:0
    Location:http://www.nicovideo.jp/
    Set-Cookie:nicosid=1111111111.1111111111; Max-Age=315360000; Expires=Mon, 26 Apr 2027 13:39:03 GMT; Path=/; Domain=.nicovideo.jp
    Set-Cookie:user_session_secure=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx; Max-Age=2591999; Expires=Sun, 28 May 2017 13:39:02 GMT; Path=/; Domain=.nicovideo.jp; Secure; HTTPOnly
    Set-Cookie:user_session=user_session_22222_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz; Max-Age=2591999; Expires=Sun, 28 May 2017 13:39:02 GMT; Path=/; Domain=.nicovideo.jp
    Set-Cookie:user_session=deleted; Max-Age=0; Expires=Fri, 28 Apr 2017 13:39:03 GMT; Path=/
    Set-Cookie:nicosid=3333333333.3333333333; expires=Mon, 26-Apr-2027 13:39:03 GMT; Max-Age=315360000; path=/; domain=.nicovideo.jp
    X-niconico-sid:3333333333.3333333333
    X-niconico-sid:4444444444.4444444444
    Connection:close
    Content-Type:text/html; charset=UTF-8
    Server:Apache
    

    取得したいのは5つある「Set-Cookie」のうち、3つ目の「user_session」の値です。
    不要:「nicosid」
    不要:「user_session_secure」
    必要:「user_session」
    不要:「user_session」(deletedのものは不要)
    不要:「nicosid」

    クッキーからセッションを取得できれば良いので
    レスポンスボディ部の取得は行いません。

【ニコニコ動画】JavaでHTTP通信

JavaでHTTP通信をお試しする上で電車検索のAPIやGoogleのAPI等、色々ありますが、
ニコニコ動画のAPIを利用してみることにしました。
まずはHTTP通信についての基礎事項を記載します。

  • HTTP通信開始
    HTTP通信を利用するには抽象クラスであるURLConnectionクラスを利用します。

    // HTTP通信接続
    URL url = new URL("http://xxx");
    HttpURLConnection httpUrlConnection = (HttpURLConnection)(url.openConnection());
    
    // HTTPS通信接続
    URL url = new URL("https://xxx");
    HttpsURLConnection httpsUrlConnection = (HttpsURLConnection)(url.openConnection());
    

    上記だけではまだサーバへの接続は開始されておらず、
    接続管理オブジェクトが生成された状態で、
    後述の「httpUrlConnection.getOutputStream()」か
    「httpUrlConnection.getInputStream()」が呼び出されると、
    内部的に「httpUrlConnection.connect()」が呼び出されます。
    切断メソッドはなく、「httpUrlConnection.getInputStream()」で取得した
    「InputStream」がcloseされるとサーバから切断されます。

  • POST通信とGET通信
    POST通信、またはGET通信を行う設定は以下の通りです。

    // POST通信
    httpUrlConnection.setRequestMethod("POST");
    httpUrlConnection.setDoOutput(true);
    
    // GET通信
    httpUrlConnection.setRequestMethod("GET");
    
  • リクエストヘッダ部の設定
    リクエストヘッダ部に値を設定する場合は以下の通りです。

    // リクエストヘッダ部設定
    httpUrlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
    
  • リクエストボディ部の設定
    リクエストボディ部に値を設定する場合は以下の通りです。
    しかし、GET通信の場合、リクエストボディ部は設定しないのが正しいです。
    パラメタは「http://xxx?mail=xxxx.yyyy.zzzz@gmail.com&password=pass」で渡します。

    // リクエストボディ部に設定するパラメータ
    String params = String.format("mail=%s&password=%s", "xxxx.yyyy.zzzz@gmail.com", "pass");
    // リクエストボディ部設定
    PrintStream out = new PrintStream(httpUrlConnection.getOutputStream());
    out.print(params);
    out.close();
    
  • レスポンスボディ部の取得
    レスポンスボディ部を取得する場合は以下の通りです。

    BufferedReader responseBody = new BufferedReader(new InputStreamReader(httpUrlConnection.getInputStream()));
    String str = null;
    while((str = responseBody.readLine()) != null) {
        System.out.println(str);
    }
    responseBody.close();
    
  • レスポンスヘッダ部の取得
    レスポンスヘッダ部を取得する場合は以下の通りです。

    Map<String, List<String>> responseHeaders = https.getHeaderFields();
    Iterator<String> responseIt = responseHeaders.keySet().iterator();
    while (responseIt.hasNext()) {
        String responseKey = responseIt.next();
        List<String> responseList = responseHeaders.get(responseKey);
        for(String reponseValue: responseList) {
            System.out.println(responseKey + ":" + reponseValue);
        }
    }