3
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Steamの一日のゲーム時間を詳しく知りたい

Last updated at Posted at 2020-05-13

はじめに

自粛期間中なにしていますか?

僕は就活鬱と重なり死んだように一日中ゲームをしていました.普段,一日中ゲームという事はなかったのでSteamの過去2週間のゲーム時間が130時間を超えたときは大変驚きました.

さて,このSteamのゲーム時間記録ですが過去二週間のすべてのゲームの積算しか表示してくれません.Nintendo Switchは小さい子供さんも使うことを考慮して,各ゲームの細かなプレイ時間を記録1してくれるそうです.確かにSteamにそんな細かくゲーム時間を記録して監視するなんて機能不要ですよね.でも僕は自分でゲーム時間管理できないおこちゃまなのですごく欲しい.

そこで,いろいろと調べているとWebスクレイピングを使ってSteamの各ゲームのプレイ時間を記録してみた記事を発見しました.この手法は,Steamのプロフィール上にあるオンラインステータスのhtmlソースからオンライン/オフライン状況や,プレイ中のゲームを抽出するスクリプトをトリガーを使用して1分ごとに実行しGoogle Drive上のSpread Sheetに記録します.
img2.png

!?
こいつぁあったまいい!

という事で,この手法をベースに拝借し,一日の各ゲームの
プレイ時間をメールでお知らせしてくれるように手を加えてみました.
Steamのゲーム時間を管理したいあなた,時間を決めて楽しくゲームしましょう.
(*当方GASさらにはJavaScriptの超絶初心者ですので汚いコードをご容赦ください(涙))

WebスクレイピングによるSteamステータスの記録

Webスクレイピングとは,APIなどを介さずにWebページから直接情報を抽出し利用する技術です.
人がwebサイトを見る代わりに,コンピュータに見てもらうという感じですね.

さて,Steamのプロフィールには(公開設定にしてあれば)オンライン/オフライン状況,ゲーム中であればゲーム名が表示されます.そこで定期的にページにアクセスしアカウントのStatusを抽出することで事細かにゲーム状況を記録可能です.
status.PNG
上記のソースより,現在のStatus(オンライン・オフライン・ゲーム)を取得するには,HTMLソース上の<div class="profile_in_game persona >に挟まれた部分からonlineofflinein-gameを抽出し,さらにin-gameであった場合に<div class="profile_in_game_name"></div>で挟まれた部分からゲーム名を抽出することとなります.

この抽出にはParse Libraryを使用します.詳しい使い方やインポート方法についてはこちらを参照ください.

Spread sheetの用意と行の内容

Google Drive上で記録用のSpread Sheetを作成します.
ほぼデータベースのような位置づけです.僕の場合は下記のように記録しています.
spreadsheet.png
スクリプトの作成は,下記のToolsタブから行えます.
gas.png

各列の情報に頻繁にアクセスするため,スクリプト上であらかじめ変数として列番号を宣言しておきます.

const DATE = 1; //A列
const STATUS = 2; //B列
const STATES = 3; //以下同
const UNIX_TIME = 4;
const ELAPSED_TIME = 5;
const DEBUG = 7;

Statusの取得

Parse Libraryを使用します.詳しい使い方やインポート方法についてはこちらを参照ください.
Parse libraryを使用してcontext中からfromTexttoTextで挟まれた部分のtextを抽出します.
先述の通りStatusを記載するfromTexttoTextは下記のとおりです.

var url = "https://steamcommunity.com/id/your_steam_id";
var fromText = '<div class="profile_in_game persona ';
var toText = '">';
var content = UrlFetchApp.fetch(url).getContentText();

function startParse( content , fromText , toText )
{
  return Parser.data(content).from(fromText).to(toText).build();
}

Game名を取得

ゲーム名はStatusがin-gameの時にHTMLソース上で

var g_fromText = '<div class="profile_in_game_name">';
var g_toText = '</div>';

var game_name = startParse( context , g_fromText , g_toText);

とすることでstatusがin-gameの時に,ゲーム名を取得できます.
こちらは後の関数内で使用しています.

CellにStatusを記録

Parseを使用して取得してきたstate(strings)に対応した内容をセルに記録する関数を用意します.
記録内容は[時刻][Status(online/offline or ゲーム名)][Statusに対応した数字(Offline=0, Online=1, Game=2][UNIX time]としています.

var sheet = SpreadsheetApp.getActiveSheet();

function setActivityToCell( status )
{
  var content = UrlFetchApp.fetch(url).getContentText();
  var date = new Date();
  
  if( status == "online")
  {
  // Onlineなので"online",'1'をセルへ
    sheet.getRange(sheet.getLastRow() + 1 , 1 , 1 , 4).setValues([[getDate() , "On line" , 1 , date.getTime() ]]);
  }
  else if( status == "offline" )
  {
  // Offlineなので"offline",'0'をセルへ
    sheet.getRange(sheet.getLastRow() + 1 , 1 , 1 , 4).setValues([[getDate() , "Off line" , 0 , date.getTime() ]]);
  }
  else if( status == "in-game" )
  { 
    // ゲーム中なので"game_name",'2'をセルへ
    // Parseにg_fromTextとg_toTextを与えればプレイ中のゲーム名の取得が可能
    sheet.getRange( sheet.getLastRow() + 1, 1 , 1 , 4).setValues([[getDate() , startParse( content , g_fromText , g_toText ) , 2 , date.getTime() ]]);
  }

  function getDate()
  {
    var date = new Date();
    return Utilities.formatDate(date, "JST", "yyyy/MM/dd HH:mm");
  }
}

記録タイミング

記事の手法では1分毎に取得した情報を記録するため,一日で1440行分の記録がたまります.
しかし,欲しいデータはステータス(Online/Offline・各ゲーム)の時間なのでステータスが切り替わった点で記録するようにしました.

まず,StatusをStringsではなく数字として記録したほうが分岐を用いる際に都合が良いので,status(online,offline,in-game)を受け取って,それに対応した数字(online=1 , offline=0 , in-game=2)を返す関数を定義します.

function returnStateNum( status )
{
  switch( status )
  {
    case "online":
      return 1;
      break;
    case "offline":
      return 0;
      break;
    case "in-game":
      return 2;
      break;
    default:
      break;
  }
}

続いて,この番号を前回の記録と比較して変更があったときのみ内容を記録する関数です.

//変更があったときのみ記録する関数
function setActivityIfChanged()
{
  // Steamのプロフィールページのソースを取得
  var content = UrlFetchApp.fetch(url).getContentText();

  //一行目はstatusの切り替えがないのでそのまま記録
  if( sheet.getLastRow() == 1 )
  {
    // 先述の記録する関数
    setActivityToCell( startParse( content , fromText , toText ) );
  //前日の行が何行目か保存(一行目なので初期値)
    sheet.getRange( 2 , DEBUG ).setValue( 2 );
  }
  //二行目以降(すなわち前回のStatusが記録されている場合)はこっち
  else
  {
    // Statusの数字が前回のStatus ID(offline=0 , online=1 , in-game=2)と異なった場合,Statusを記録
    if( sheet.getRange(sheet.getLastRow() , STATES ).getValue() !== returnStateNum( startParse( content , fromText , toText ) ) )
    { 
      setActivityToCell( startParse( content , fromText , toText ) );
   // 前回からの経過時間を記録
      calculateTime();
    }
    // Status番号が同じ場合(例:online->online , in-game->ingame , offline->offline)
    else
    {
      // ただしStatusがin-game中に違うゲームをプレイした場合には別で記録したいので
      // ゲーム名を比較して,ゲーム名が変わっていたら記録
      if( ( returnStateNum( startParse( content , fromText , toText ) ) == 2  ) && ( sheet.getRange( sheet.getLastRow() , STATUS ).getValue() !== startParse( content , g_fromText , g_toText ) ) )
      {
        // Status is "in-game" but played game is different.
        setActivityToCell( startParse( content , fromText , toText ) );
        calculateTime();
      }
      // ゲーム中ではない + 前回とStatusがおなじ もしくは 同じゲームをプレイしている
      else
      {
        //console.log("Not changed");
      }
    }
  }
}

// 前回からの経過時間を記録する関数
function calculateTime()
{
  var t1 = sheet.getRange( sheet.getLastRow() , UNIX_TIME ).getValue();
  var t0 = sheet.getRange( sheet.getLastRow() - 1 , UNIX_TIME ).getValue();
  var elapsed_time = Math.abs( t1 - t0 ) / ( 1000 );
  sheet.getRange( sheet.getLastRow() - 1 , ELAPSED_TIME ).setValue( elapsed_time );
}

脳筋コードなのでひたすらif-elseで分岐させただけなので汚いです.
ゲーム名比較の場所は,違うゲームをプレイする際にトリガーで設定した時間以内にゲームの切り替えた場合があった時にStatusの更新が出来なくなるための処理です.
これで,Statusもしくはプレイしているゲームが変わったときのみ記録することが可能です.

関数setActivityIfChanged()を毎分もしくは5分毎くらいにトリガーします.これにより分毎にSteamのプロフィールページにアクセスし,Statusが更新されているときのみSpread Sheetに記録します.トリガーは下記から行えます.
trigger.png

一日の記録の解析

最後に一日の記録時間を各項目(online/offline/in-gameの場合はゲーム名)ごとに積算します.

function analysis()
{
 // 最終記録からセル(2 , DEBUG)に保存しておいた行番号までの行をiteration
  var status = [];
  for( let i = sheet.getLastRow() - 1 ; i >= sheet.getRange(2, DEBUG).getValue() ; i -- )
  {
    status.push( sheet.getRange( i , STATUS ).getValue() );
  }
  // Statusの重複を除く
  var status_name = status.filter(function (x, i, self) {
            return self.indexOf(x) === i;
        });

  // Statusごとの時間を連想配列で保存
  // 初期化
  var times = {};
  for( let j = 0 ; j < status.length ; j ++ )
  {
    times[ status_name[ j ] ] = 0.0;
  }
  // セル(2 , DEBUG)に保存しておいた行番号までの行をiterationして時間を対応するkeyに積算
  for( let k = sheet.getLastRow() - 1 ; k >= sheet.getRange(2, DEBUG).getValue() ; k -- )
  {
    times[ sheet.getRange( k , STATUS).getValue() ] = times[ sheet.getRange( k , STATUS).getValue() ] + sheet.getRange( k , ELAPSED_TIME ).getValue() ;
  }

  // 積算した内容をメール
  mailing( status_name , times );

  // 積算した最終行を次回積算時の開始点とする(記録しておく)
  sheet.getRange( 2 , DEBUG ).setValue( sheet.getLastRow() );
}

前回積算を行った行をセル(2, DEBUG)に記録しておき,最新行から前回の行までiterationします.
最初にStatus名(online/offline/ゲーム名)のユニークを取った配列status_nameを作成します.
その後,連想配列timesによってゲーム名と時間を記録します.JSあんまり分からなくて雑な方法な気がするので,もっといいやり方あれば教えていただきたいです...
最後に,積算した最終行番号をセル(2 , DEBUG)に記録し,次回の積算時の開始点とします.

メール

積算した内容を整形してメールします.

function mailing( status_name , times )
{
  const recipient = 'your_mail_adress';
  var date = new Date();
  const subject = '【steam game alart】' + Utilities.formatDate(date, "JST", "yyyy/MM/dd");
  var body = 'Hi me!\n\nTodays steam game play time analisys is here.\n\n\n';
  body = body + "━━━━━━━━━━━━━━━━━━━━━━━━\n";
  for( let i = 0; i < status_name.length ; i ++ )
  {
    var str_time = sec2str( times[ status_name[ i ] ] );
    body = body + '   - ' +  status_name[ i ] + ' : ' + str_time + '\n';
  }
  body = body + "━━━━━━━━━━━━━━━━━━━━━━━━\n\n";
  body = body + '\n\n You need to reduce time of game....\n';
  console.log(body);
  GmailApp.sendEmail(recipient, subject, body);
}

時間は秒で記録しているので,見やすくします

function sec2str( sec )
{
  var t = Math.floor(sec);
  var str_time = '';
  var h = t / 3600 | 0;
  var m = t % 3600 / 60 | 0;
  var s = t % 60;
  
  if( h != 0 )
  {
    str_time = h + "時間" + padZero(m) + "" + padZero(s) + "";
  }
  else if( m != 0 )
  {
    str_time = "00時間" + m + "" + padZero(s) + "";
  }
  else
  {
    str_time = "00時間00分" + s + "";
  }
  return str_time;
  
  function padZero( v )
  {
    if( v < 10 )
    {
      return "0" + v;
    }
    else
    {
      return v;
    }
  }
}

全体のまとめ関数

最後に,日をまたぐタイミング等でトリガーを発生させる関数を作成して終了です.
日をまたいでもゲームしている可能性がある(というかほぼ毎回)ので強制的に行を更新してそこまでの情報で積算しメールを送信します.

function totalization()
{
  setActivity();
  analysis();
}

function setActivity()
{   
  // Set Activity even no state change
  var content = UrlFetchApp.fetch(url).getContentText();
  setActivityToCell( startParse( content , fromText , toText ) );
  calculateTime();
}

関数totalization()を日ごとにトリガーします.

実行するとこんな感じのメールが届きます.
IMG_6590.PNG

これで思う存分ゲームをしても叱ってくれるね!!!!

まとめ

GASを用いてSteamのプロフィールページからWebスクレイピングによってゲーム時間を記録し,メールするシステムを作ってみました.
Steamの細かいゲーム時間の記録って結構需要あると思うんですけど僕だけなんですかね?(震)

本当ならゲーム中にPCは起動しているわけだし,どうにかSteamのゲームが立ち上がっている情報をローカルで入手できそうなんですがどうなんでしょう.
将来的にはグラフ化等の解析ツールも組み込んでみたいですね.あと,SteamのサーバーやWebページが動いていることが前提です.たまーにSteamの鯖落ちを経験しているのでその時は記録は無理ですね.

GAS,初めて使いましたがとても便利そうです.もっと勉強します.

WHOもゲーム依存症については最近うるさい2ですし,自己管理しっかりして思う存分ゲームを楽しみましょう.
さて,トロコンするか.....

  1. https://support.nintendo.co.jp/app/answers/detail/a_id/34111

  2. https://www.nikkei.com/article/DGXMZO45280950V20C19A5MM8000/

3
8
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?