CS_INDUSTRIAL

AR/VRエンジニアの趣味ブログ

人工知能についてのメモ②「Python環境の設定」

人工知能についてのメモ①では人工知能についてを軽く知り、それを実現するためには機械学習または深層学習が方法としてあり、いろいろな言語があるけどPythonが一番人気というところまでやりました。
今回はPythonの環境を整えて少し触るところまでです。

Anacondaのインストール

Pythonを調べていると便利便利と書いてあるのでAnacondaというものを使用していきたいと思います。
Anacondaは「Python本体と、Pythonでよく利用されるライブラリ」がまとまっているパッケージらしいです。(開発ツールみたいなものと理解しておきました)
www.anaconda.com

Anacondaのダウンロードページから私はPython 3.6 version (64bit) のインストーラーをダウンロードしました。
途中に選択やチェックボックス等ありましたが一応そのまま「Next > Next > Install」といった感じでインストーラーに従いました。

※注意:インストール途中にcmd.exeが開きますが閉じてしまうとインストールできないものが出てきたりするので放置しましょう。

対話型実行環境 Jupyter Notebook

Pythonのコードを含んだWebページを作成できる機能(つまりGitをcmd.exeでやるかSourcetreeのようなGUIツール使うかみたいなものと認識しました)
これを使って可視化しながら簡単なものからやっていきたいと思います。

インストールしたツールはAnaconda-Navigatorから開けるようです。
f:id:nanokanato:20180228121348p:plain:w100f:id:nanokanato:20180228121829p:plain:w300

またJupyter Notebookの直接リンクがありますがブラウザの方が開かなかったことがあるのでAnaconda-Navigatorから手順を踏んで開いた方がよさそうです。

Jupyter Notebookを開くとJupyter NotebookのWindowとブラウザが立ち上がります。
f:id:nanokanato:20180228122544p:plain:w300

「Documents\Python Jupyter Note」のフォルダを作りそこで作業することにしました。

  1. Documentsをクリックして階層を移動。
  2. 右上のNewをクリックし「Folder」を選びリストにUntitled Folderを追加。
  3. Untitled Folderをチェックし、左上に出てくるRenameを押す。
  4. フォルダの名前が変更できますので適宜変更してください。

エクスプローラーでフォルダを作り、再度ブラウザを開くことでもOKです。

Pythonの基礎

作業フォルダの作成

Python Jupyter Noteのブラウザで先ほど作った「Documents\Python Jupyter Note」を開きます。
右上の「New」から「Folder」を選択し、Untitled Folderを作成します。
Untitled Folderを選択して、「Rename」でHello Pythonという名前に変更します。
今回は「Documents\Python Jupyter Note\Hello Python」を作業フォルダとして利用します。

Python3の作成

右上の「New」から「Python3」を選択します。
f:id:nanokanato:20180228133514p:plain:w300
作業フォルダにUntitled.ipynbが作成され、別のブラウザでチャットのような画面が開きます。
f:id:nanokanato:20180228133535p:plain:w300

Hello Python

作成したUntitled.ipynbの「In [ ]:」の横の入力欄に以下のコードを打って「Shift」+「Enter」を押してください。

print('Hello Python!!')

これで入力に足して「Hello Python!!」と出力されました。
f:id:nanokanato:20180228133835p:plain:w300

コードを入力し「Shift」+「Enter」で実行して出力という作業が、Jupyter NotebookでのPythonプログラミングの基本となります。

ちなみに一度入力した場所のコードを修正して実行することで出力結果も更新されます。

変数を用いた計算

変数を用いて計算なども可能です。

a = 12
b = 35
c = 53
a + b + c

aに12、bに35、cに53を代入してその3つを足した数、100を出力します。
f:id:nanokanato:20180228134726p:plain:w300

ファイルの読み込み

機械学習にはデータの入力が必要です。
いちいちデータを手で入力していてはキリがないのでファイルを読み込む機能を使用します。

まずは読み込むためのファイルを作成します。
作業フォルダにて右上の「New」から「Text File」を選択します。
その後作成されたUntitled.txtをチェックして左上の「Rename」でData.csvに名前を変更します。
Data.csvを開いてcsvのコンマ区切り形式で適当にデータを作ります。
f:id:nanokanato:20180228141432p:plain:w300

作成したファイルをUntitled.ipynbと同じフォルダに配置します。
f:id:nanokanato:20180228140100p:plain:w300

以下のコードを実行することでファイルを読み込んで出力することができます。

import pandas as pd
data = pd.read_csv('Data.csv')
data

f:id:nanokanato:20180228141704p:plain:w300
一番左の太字の0,1,2は何個目のデータかを表しており、その右側はファイルに入力したものと一致しています。
全て空白の行があった場合はスルーされます。

また、ファイルの拡張子がtxtの場合でもコンマ区切りになっていれば問題ないようです。
※しかし一般的には拡張子はcsvなので合わせた方が良いでしょう。
f:id:nanokanato:20180228142337p:plain:w300

「import pandas as pd」はpdにpandasというデータ分析に特化したライブラリをインポートしており、そこからデータを読み込むことでエクセルやCSVデータを簡単に扱うことが可能になっています。

ファイルの保存

画面上の「Untitled」という部分を任意の名前(HelloPython)に変更し、左上の「File」の下にある保存ボタン(フロッピーディスクのアイコン)を押して完了です。

保存したファイルをPythonとして書き出す

画面左上の「File > Download as > Python(.py)」を押すことで.pyファイルとして書き出せます。

人工知能についてのメモ③では教師なし学習を行います。

人工知能についてのメモ①「基礎知識の確認」

ほぼ個人メモなので雑です。理解していくために書き殴ります。
人工知能などに関しての技術進歩が速いので今のうちに基礎を理解しておかないとチンプンカンプンになると思ったのではじめました。(使用だけだと簡易ライブラリなどは出てきていますが...原理的な部分)
定期で何かわかったことがあれば追記したりします。
解釈の違いなど当然あると思いますのでご指摘いただけると嬉しいです。

そもそも人工知能(AI)とは

膨大なデータを元に分析して分析・判断し、検出・抽出を行う機能、仕組み。
今回のような分析・判断するAIを強いAIと呼び、
プログラムで書かれたような知能の低いAIのことを弱いAIと呼ぶそうです。

よく出てくる単語

  • アルゴリズム
    • 学習したデータから検出・抽出する方法のこと。
  • データ
    • 主に学習の材料になるデータのこと。
      画像解析なら同じ対象が映った複数画像の集合や、Excelのような表など。
  • 入力
    • 機械学習のプログラムに対してデータを入力すること。
  • 出力
    • 入力から機械学習のプログラムが導き出した答えのこと。
  • ラベル
    • 入力とそれに対応すべき出力を人間が例として示した見本のこと
  • データマイニング
    • 様々なデータ解析の技法を大量のデータに網羅的に適用することで知識を取り出す技術のこと。
      通常では想像が及びにくい、発見ができる可能が多い。

人工知能を実現する方法

  • 機械学習
    機械学習を一言で表すと「明示的にプログラムしなくても学習する能力をコンピュータに与える研究分野」
    • 教師あり学習(Supervised Learning)
      • 入力と出力の関係を学習する方法。
        ラベルを提示することでそこから解釈されるアルゴリズムを生成する。
    • 教師なし学習(Unsupervised Learning)
    • 強化学習(Reinforcement Learning
      • 価値・評価を最大化するような行動を学習する方法。
        人間が与えた環境を観測し、どう行動すべきかを学習する。行動によって環境に影響が出て、環境から報酬という形でフィードバックを得ることで学習アルゴリズムのガイドとする。
        例として「将棋をして、より勝率の高い戦略を学習させる」などという方が個人的にはわかりやすい。
  • 深層学習(ディープラーニング
    ディープラーニング機械学習をさらに発展させたもの。主にデータを分析する際に使う枠組みが異なっていて、人間の神経を真似て作った「ニューラルネットワーク」で、コンピューターによるデータの分析と学習を強化している。
    機械学習ではデータに対して「入力と出力の関係」や「データの構造」「価値・評価を最大化する」など重点を人間が用意したのに対し、その部分もディープラーニングに学習させ判断させようとしているらしい....。(相手が何を求めているかを考えることでより人間に近くなっている!)

人工知能を作るにあたって選べる言語

  1. Python
  2. Java
  3. R
  4. C++
  5. C
  6. JavaScript
  7. Scala
  8. Julia

個人的にはJava,Js,C++あたりがうれしいのですが、一般的にはPythonが多く、記事も多いのでまずはPythonではじめるのが無難な感じでした。

人工知能についてのメモ②ではPythonの環境を整えます。

【Unity】WindowsPCのスクリーンをキャプチャして再生する

以下のほうが高機能で高速です。
tips.hecomi.com

しかし...GTX10XX系を搭載したゲーミングPCでは動かないそうで...??
実際、GTX1070を積んだノートで動作しなかった...

なので動作としては遅いが何となくPC画面をUnity内に描画したくてやってみました。
他に解決策があればぜひ教えていただきたいです。

ソースはこちら、サンプルをいくつか用意したのでどうぞ
github.com

実際に動かしてみるとこんな感じです。
exeでもWindowsであればもちろん動きます。
・Windows10
・Unity2017.3.1

【Maya】Pythonで線を描画する

MayaをPythonでいじれるということなので手始めに検証です。
今回は線を引いてみました。

import maya.cmds as cmds

transform = cmds.createNode('transform', n='curve1')
nurbsCurve = cmds.createNode('nurbsCurve',n='curveShape1', p=transform)
cmds.curve(nurbsCurve, p=[(0, 0, 0), (3, 5, 6), (10, 12, 14), (9, 9, 9)])
  1. transformのcurve1を作成
  2. curve1を親にしてnurbsCurveのcurveShape1を作成
  3. curveShape1にカーブの値を設定

表示結果はこんな感じ
nurbsCurveで指定したので綺麗な曲線になるかと思ったのですがカクカクになってますね....
f:id:nanokanato:20180118155418p:plain:w400

【GAS】WebアプリケーションでWebページを作る

GoogleAppScript(以下GAS)のWebアプリケーションの機能を使ってWebページを作ります。

GSuiteのアカウントではGSuiteメンバーのみが閲覧できるページしか作れないのでご注意ください。
通常のGoogleアカウントでは全体公開、自分のみ、Googleにログインしている人のみなど選べます。

GASの準備

過去記事を参考にGASの準備をお願いします。
madgenius.hateblo.jp

GASの記入

以下がページを表示するのに使うGASです。

  • doGetが必須のメソッドで、HTMLページを読み込むのに使います
    • ここではindex.htmlを読み込んで表示している
  • GetTextはHTMLページから情報を引き出すためのメソッドです。
    • 特定のKeyを送ることでそれに合った文字列を返します。
    • 本来、ここで通信したりスプレットシートの中身を見てページの表示内容を変更する
  • GetCSSではCSSの名前を送ることでそのCSSの中身を取得して返します。
function doGet() {
    var html = HtmlService.createTemplateFromFile("index");   
    return html.evaluate();
}

function GetText(key) {
    if (key == "title") {
        return "私が好きなプリパラアイドル";
    } else if (key == "rank1") {
        return "紫京院ひびき";
    } else if (key == "rank2") {
        return "北条そふぃ";
    } else if (key == "rank3") {
        return "東堂シオン";
    } else if (key == "rank4") {
        return "白玉みかん";
    } else if (key == "rank5") {
        return "み〜んなアイドル、み〜んな大好き";
    }
}

function GetCSS(filename) {
    return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

CSSの作成

GetCSSで取得するCSSを作成します。
「GASのファイル > 新規作成」でHTMLファイルを作成します。
ファイル名は「css.html」です。
f:id:nanokanato:20170905161955p:plain:w300

<style>
body{
    font-family:Verdana,Arial;
    font-size:14px;
}
 
#header{
    margin-bottom:15px;
    border-bottom: 1px solid #ccc;
}
 
#contents{
}
 
#footer{
    font-size:12px;
    color:#ccc;
    text-align:center;
    border-top:1px solid #ccc;
    padding:10px 0 20px;
}
 
h2,h3{
    font-size:bold;
}
 
h2{
    font-size:16px;
    border-left:5px solid #ccc;
    padding:3px 0 3px 10px;
    margin-bottom:10px;
}
 
h3{
    border-bottom:1px solid #ccc;
    padding:3px 0;
    margin-bottom:10px;
}
 
p{
    margin-bottom:14px;
}
</style>

HTMLの作成

CSSの時と同じ手順でindex.htmlを作成します。

  • 「<head>」の中に「<?!= GetCSS('css'); ?>」とありますが、css.htmlの内容を<head>の中に追加しています。
    • 「<?= ?>」でメソッドの返り値をそのまま表示するとHTMLタグなどはエスケープされてWebページに文字列として表示されます。
      「<?!= ?>」を使うことによりエスケープをせずにタグとしてHTMLに埋め込むことが可能です。
  • 「<?= GetText("title"); ?>」はGetTextメソッドから"title"という文字に紐付けられた文字を返す。
<!DOCTYPE html>
<html>
    <head>
        <base target="_top">
        <?!= GetCSS('css'); ?>
    </head>
    <body>
        <section id="header">
           <h2><?= GetText("title"); ?></h2>
        </section>
        <section id="contents">
            <ul class="products">
                <li>
                    1位:<?= GetText("rank1"); ?>
                </li>
                <li>
                    2位:<?= GetText("rank2"); ?>
                </li>
                <li>
                    3位:<?= GetText("rank3"); ?>
                </li>
                <li>
                    4位:<?= GetText("rank4"); ?>
                </li>
                <li>
                    5位:<?= GetText("rank5"); ?>
                </li>
            </ul>
        </section>
        <section id="footer">
            <a href="https://twitter.com/ele_enji" target="_blank">園児ニアの技術メモ</a> / <a href="https://twitter.com/ele_enji" target="_blank">@ele_enji</a>
        </section>
    </body>
</html>

Webページを表示する

doGetメソッドを実行してエラーが出なければOKです。
たまにHTML内でのメソッド名などの入力ミスでエラーが出ます。

では、WebページとしてのURLアドレスを取得しましょう。

「公開 > ウェブ アプリケーションとして導入...」を選択。
f:id:nanokanato:20170905164041p:plain:w300

ウェブアプリケーションとして導入というポップアップが出ます。

  • アクセスユーザーを「自分だけ」から「全員(匿名ユーザーも含む)」にしました。
    • GSuiteの場合、「全員(hogehogeのメンバー)」など最大でもグループのユーザーしかアクセスできません。
  • もしアクセスしたユーザーのアカウントを使う必要がある場合は「次のユーザーとしてアプリケーションを実行」の実行ユーザーを「自分(hogehoge@hoge.com)」ではなく「ウェブアプリケーションにアクセスしているユーザー」にしましょう。

f:id:nanokanato:20170905164405p:plain:w300

更新を押すと実際のWebサイトのアドレスが取得できます。
f:id:nanokanato:20170905165018p:plain:w300

実際に今回作ったページがこちら
script.google.com

CSSも入れれるので結構いい感じのデザインにはできるのですが、URLの部分はGoogleAppScriptなんだなって感じなのでHPには向いてないです。
ツールサイトや、個人用として使うぶんには十分だと思います。
f:id:nanokanato:20170905165239p:plain:w300

【GAS】電車遅延情報をツイート

GoogleAppScript(以下GAS)にて電車遅延の情報をツイートする方法です。

過去記事でGASと連携してTwitter投稿を行う記事を書いてますのでどうぞ
madgenius.hateblo.jp

今回は上の定期ツイートの内容に電車遅延の情報を追加するのみになります。

電車遅延情報を取得してツイート

鉄道遅延情報のjsonを使って電車遅延情報を取得します。
ここがもし使えなくなった場合は各種運行会社のRSSを取得したり、公式Twitterを監視するのも使えます。(RSSなど公式の方が遅延理由もあるので便利ですが今回は簡単な方で...)

以下がソースです。10分毎のタイマーをセットしています。

//10分毎に鉄道遅延情報のjsonを取得して10分以内に更新された遅延情報をツイートする
function TrainDelayTweet() {
    var response = UrlFetchApp.fetch("https://rti-giken.jp/fhc/api/train_tetsudo/delay.json");
    var result = JSON.parse(response.getContentText());
    for(var i = 0; i < result.length; i++) {
        var company = result[i].company; //会社
        if (company == "東京メトロ" || company == "都営地下鉄") {
            var name = result[i].name; //路線
            var date = new Date(Number(result[i].lastupdate_gmt) * 1000); // 最終更新日時
            
            var Nowymdhms = new Date();
            if ((Nowymdhms.getTime()-date.getTime())/1000/60 < 10) {
                var ResultYear = date.getYear();
                var ResultMon = date.getMonth() + 1;
                var ResultDay = date.getDate();
                var ResultHour = date.getHours();
                var ResultMin = date.getMinutes();
                CS_Twitter.TwitterSend(name+"("+company+")が遅延しました。\n情報更新日時:"+ResultYear+"/"+ResultMon+"/"+ResultDay+" "+ResultHour+":"+ResultMin);
            }
        }
    }
}

10分毎に遅延情報を取得して更新時間が10分以内だったらツイートします。(しかし、普通に10分ごとに更新とかもあるので同じ路線が何度もツイートされることもある...)

私が都内に住んでいるので「JR九州」などを出さないために運行会社でフィルターをかけています。
ここでは東京メトロ都営地下鉄のみにしています。
JR東日本で山手線などを入れたいのですがJR東日本には東北も含まれますので、「JR東日本の山手線」の時はフィルターをスルーするようにしてやればOKです。

こちらのBotは私のTwitterにて動作確認が可能です。
以下、フィルターをかけていない時のツイートですが...

【GAS】Twitterの相互じゃない人を自動で定期的にリムーブ

はじめに

Twitterをやっていると前は相互フォローだったのに片思いになってしまった人などがいると思います。
片思いというのは、自分は相手をフォローしているが相手は自分をフォローしていない状態のことです。

しかし、誰と片思いになったかどうかを確認して1人1人リムーブするのは意外と手間がかかりますよね。

かといって一括ツールを使うと公式アカウントなど片思いと知っておきながらフォローしているアカウントまで解除されてしまいます。

GoogleAppScript(以下GAS)でやれば自分の思い通りにできるのでやってみました。

一応、手順とメソッド単位での説明がありますが、最後にソースだけをまとめています。

GASとTwitterAPIの連携

過去記事でGASとTwitterAPIを連携させるライブラリを作成しています。
こちらを参考に導入までどうぞ
madgenius.hateblo.jp

フォロー、フォロワーの取得

片思いかどうかを調べるためには自分のフォローとフォロワーのリストが必要です。

フォローの取得

CS_TwitterのTwitterFollowListを使用します。

//フォローしている人のリストを作成
function GetFollowList(screen_name,user_id) {
    var followCursor = -1;
    var followList = [];
    while (followCursor != 0) {
        var follow_list = CS_Twitter.TwitterFollowList(5000,screen_name,user_id,followCursor);
        followCursor = 0;
        if (follow_list != null) {
            if (follow_list.errors == null) {
                followCursor = follow_list.next_cursor;
                followList = followList.concat(follow_list.ids);
            } else {
                break;
            }
        }
    }
    return followList;
}
フォロワーの取得

CS_TwitterのTwitterFollowerListを使用します。

//フォロワーのリストを作成
function GetFollowerList(screen_name,user_id) {
    var followerCursor = -1;
    var followerList = [];
    while (followerCursor != 0) {
        var follower_list = CS_Twitter.TwitterFollowerList(5000,screen_name,user_id,followerCursor);
        followerCursor = 0;
        if (follower_list != null) {
            if (follower_list.errors == null) {
                followerCursor = follower_list.next_cursor;
                followerList = followerList.concat(follower_list.ids);
            } else {
                break;
            }
        }
    }
    return followerList;
}

一応これでフォロー全件とフォロワー全件が取得できるはずです。
1回の通信で5000人分なので超有名人でもないかぎりAPI規制がかかったりすることはないはずです。

片思いのユーザーのリストを作成

片思いを判定するのにフォローとフォロワーのリスト配列の差集合を求めます。(差集合でフォローにいてフォロワーにいない人だけのリストになる)

自力でもでき、処理スピード的には変わらないですが、Underscoreというライブラリを使うことにしました。
GASの「リソース > ライブラリ」を開き、Underscoreのプロジェクトキー「1PcEHcGVC1njZd8SfXtmgQk19djwVd2GrrW1gd7U5hNk033tzi6IUvIAV」を入力して追加を押してください。
f:id:nanokanato:20170906121147p:plain:w300

以下が片思いのリストを作るメソッドです。
引数

  • followList - フォローのリスト
  • followerList - フォロワーのリスト
//片思いのリストを作成
function GetUnrequitedList(followList, followerList) {
    var unrequitedList = [];
    //フォローしているのにフォロワーじゃないユーザーは解除する
    var _ = Underscore.load();
    unrequitedList = _.difference(followList,followerList);
    return unrequitedList;
}

user_idをscreen_nameに変更

片思いのリストを作ったのであとはリムーブするだけ....
残念、user_idでリムーブ可能と公式のTwitterAPIに書いてますがuser_idで探したけど見つからないみたいなエラーが出てほとんどリムーブできません。

なのでscreen_name(@hogehoge)に変換してあげます。
この変換の時はなぜか見つからないエラーが起きないのが救い...

CS_TwitterのTwitterUsersLookupで複数ユーザーを指定してその詳細からscreen_nameを取得します。

//user_idのリストをscreen_nameのリストに変更する
function ConvertScreenNameList(userIDList) {
    var screenNameList = [];
    while (userIDList.length > 0) {
        var idList = [];
        if (userIDList.length <= 100) {
            idList = userIDList.slice(0);
            userIDList = [];
        } else {
            idList = userIDList.slice(0,100);
            userIDList = userIDList.slice(100);
        }
        var ids = "";
        for (var i = 0; i < idList.length; i++) {
            var id = idList[i];
            if (ids != "") {
                ids += ",";
            }
            ids += id;
        }
        if (ids != "") {
            var lockups = CS_Twitter.TwitterUsersLookup(null,ids);
            if (lockups == null) {
                break;
            } else {
                if (lockups.errors == null) {
                    for (var i = 0; i < lockups.length; i++) {
                        var lockup = lockups[i];
                        if (lockup != null) {
                            if (lockup.screen_name != null) {
                                screenNameList[screenNameList.length] = lockup.screen_name;
                            }
                        }
                    }
                } else {
                    break;
                }
            }
        }
    }
    return screenNameList;
}

これで片思いのユーザーのscreen_nameのリストが取得できました。

「var lockup = lockups[i];」の部分でユーザーの詳細があるので特定のユーザーは片思いでも見逃してスルーするなどの処理を入れることができます。(リムーブ時にscreen_nameで判別してでも良い)

上のメソッドを利用してリムーブまで行う

こんどこそ片思いのリストが用意できたのであとはリムーブするだけです。

リムーブはを使用します。


以下が片思いを取得してリムーブするメソッドです。
このメソッドをタイマーなどで呼べば定期的にリムーブすることができます。

//片思いを解除する
function UnFollow() {
    var user_id = "0000000000";
    //フォローしているユーザーのリストを作成
    var followList = GetFollowList(null,user_id);
    if (followList != null && followList.length > 0) {
        //フォロワーのリストを作成
        var followerList = GetFollowerList(null,user_id);
        if (followerList != null && followerList.length > 0) {
            //片思いのリストを作成
            var unrequitedList = GetUnrequitedList(followList, followerList);
            if (unrequitedList != null && unrequitedList.length > 0) {
                //user_idのリストをscreen_nameに変更
                var screenNameList = ConvertScreenNameList(unrequitedList);
                if (screenNameList != null && screenNameList.length > 0) {
                    //フォローを解除する
                    for (var i = 0; i < screenNameList.length; i++) {
                        var screen_name = screenNameList[i];
                        if (screen_name != null) {
                            var unfollow = CS_Twitter.TwitterUnfollow(screen_name, null);
                            if (unfollow == null || (unfollow != null && unfollow.errors != null)) {
                                
                            }
                        }
                    }
                }
            }
        }
    }
}

全ソース

TwitterAPIの連携や通信周りをまとめたスクリプトファイル

AutoUnFollowを毎分のタイマーに設定することで毎日0時0分に片思いを解除してくれる。
Unfollow.gs

CS_Twitter.TWITTER_CONSUMER_KEY = 'CONSUMER_KEY';
CS_Twitter.TWITTER_CONSUMER_SECRET = 'CONSUMER_SECRET';
CS_Twitter.OAUTH_USER_KEY = 'Unfollow';

//片思いフォローしているユーザーへのフォローを解除する
//毎日0時0分
function AutoUnFollow() {
    var Nowymdhms = new Date();
    var NowHour = Nowymdhms.getHours();
    var NowMin = Nowymdhms.getMinutes();
    if (NowHour == 0 && NowMin == 0) {
        UnFollow();
    }
}

//片思いを解除する
function UnFollow() {
    var user_id = "your user id";
    //フォローしているユーザーのリストを作成
    var followList = GetFollowList(null,user_id);
    if (followList != null && followList.length > 0) {
        //フォロワーのリストを作成
        var followerList = GetFollowerList(null,user_id);
        if (followerList != null && followerList.length > 0) {
            //片思いのリストを作成
            var unrequitedList = GetUnrequitedList(followList, followerList);
            if (unrequitedList != null && unrequitedList.length > 0) {
                //user_idのリストをscreen_nameに変更
                var screenNameList = ConvertScreenNameList(unrequitedList);
                if (screenNameList != null && screenNameList.length > 0) {
                    //フォローを解除する
                    for (var i = 0; i < screenNameList.length; i++) {
                        var screen_name = screenNameList[i];
                        if (screen_name != null) {
                            var unfollow = CS_Twitter.TwitterUnfollow(screen_name, null);
                            if (unfollow == null || (unfollow != null && unfollow.errors != null)) {
                                
                            }
                        }
                    }
                }
            }
        }
    }
}

//user_idのリストをscreen_nameのリストに変更する
function ConvertScreenNameList(userIDList) {
    var screenNameList = [];
    while (userIDList.length > 0) {
        var idList = [];
        if (userIDList.length <= 100) {
            idList = userIDList.slice(0);
            userIDList = [];
        } else {
            idList = userIDList.slice(0,100);
            userIDList = userIDList.slice(100);
        }
        var ids = "";
        for (var i = 0; i < idList.length; i++) {
            var id = idList[i];
            if (ids != "") {
                ids += ",";
            }
            ids += id;
        }
        if (ids != "") {
            var lockups = CS_Twitter.TwitterUsersLookup(null,ids);
            if (lockups == null) {
                break;
            } else {
                if (lockups.errors == null) {
                    for (var i = 0; i < lockups.length; i++) {
                        var lockup = lockups[i];
                        if (lockup != null) {
                            if (lockup.screen_name != null) {
                                screenNameList[screenNameList.length] = lockup.screen_name;
                            }
                        }
                    }
                } else {
                    break;
                }
            }
        }
    }
    return screenNameList;
}

//片思いのリストを作成
function GetUnrequitedList(followList, followerList) {
    var unrequitedList = [];
    //フォローしているのにフォロワーじゃないユーザーは解除する
    var _ = Underscore.load();
    unrequitedList = _.difference(followList,followerList);
    return unrequitedList;
}

//フォローしている人のリストを作成
function GetFollowList(screen_name,user_id) {
    var followCursor = -1;
    var followList = [];
    while (followCursor != 0) {
        var follow_list = CS_Twitter.TwitterFollowList(5000,screen_name,user_id,followCursor);
        followCursor = 0;
        if (follow_list != null) {
            if (follow_list.errors == null) {
                followCursor = follow_list.next_cursor;
                followList = followList.concat(follow_list.ids);
            } else {
                break;
            }
        }
    }
    return followList;
}

//フォロワーのリストを作成
function GetFollowerList(screen_name,user_id) {
    var followerCursor = -1;
    var followerList = [];
    while (followerCursor != 0) {
        var follower_list = CS_Twitter.TwitterFollowerList(5000,screen_name,user_id,followerCursor);
        followerCursor = 0;
        if (follower_list != null) {
            if (follower_list.errors == null) {
                followerCursor = follower_list.next_cursor;
                followerList = followerList.concat(follower_list.ids);
            } else {
                break;
            }
        }
    }
    return followerList;
}

余談

1回の実行で片思いが多すぎる人などは途中でTwitterAPIの規制や、GASのURLFetchの規制で処理が途中で終わる場合があります。

過去記事の応用などもあり、省略部分があるかと思いますので不明点やエラーが出た場合はコメントにお願いします。

【GAS】時報をツイートする

はじめに

GoogleAppScript(以下GAS)を使って1時間ごとに時刻が変わったことをTwitterに報告するBotを作ります。

GASで定期ツイートをするまでは以下をどうぞ。
madgenius.hateblo.jp

今回は上の定期ツイートの記事に正確性と実行時間の取得を追加するだけになります。

ソース

特に変更が必要な場所はありません、このまま使用できます。
タイマーは毎分で設定してください。

//1時間ごとに時刻をツイートする
function HourTweet() {
    var Nowymdhms = new Date();
    var NowYear = Nowymdhms.getYear();
    var NowMon = Nowymdhms.getMonth() + 1;
    var NowDay = Nowymdhms.getDate();
    var NowHour = Nowymdhms.getHours();
    var NowMin = Nowymdhms.getMinutes();
    if (NowMin == 0) {
        CS_Twitter.TwitterSend(NowHour+"時になりました。\n("+ZeroNum(NowYear,4)+"/"+ZeroNum(NowMon,2)+"/"+ZeroNum(NowDay,2)+")");
    }
}

function ZeroNum(num,zero) {
    var str = ""+num;
    for (var i = 0; i < zero; i++) {
        str = "0"+str;     
    }
    return str.slice(-zero);
}

ただ、タイマーで毎分チェックして0分の時に時報を流すだけです。
ぴったりの時間にお知らせできました。
f:id:nanokanato:20170829121443p:plain:w300

余談

Twitterに投稿している部分を他のSNSに変更することもできます。
ChatWorkへの投稿については以下をどうぞ。
madgenius.hateblo.jp

【GAS】Twitterと連携して定期ツイート

GoogleAppScript(以下GAS)からTwitterと連携して定期ツイートするまでを書きます。

GASの用意

GASの始め方がわからない人は過去の記事を参考にしてね。
madgenius.hateblo.jp

とりあえず、用意したらTwitterAPIでPROJECT_KEYを取得するのに必要なので開いたまま次へ

TwitterAPIの作成

TwitterAPIを使用するにはKeyとSecretが必要です。
準備までの手順は以下を参考にどうぞ。
madgenius.hateblo.jp

CallBack URLの部分はGASなので「https://script.google.com/macros/d/{PROJECT_KEY}/usercallback」になるように気をつけてください。
その辺りも上の記事に書いています。

GASライブラリを用意

GASでTwitter連携するためにはKeyとSecretを利用してOAuth認証が必要です。
OAuth認証と言われても結構面倒なのでそれを簡単にするライブラリを用意しました。
導入方法や使い方も書いてありますのでどうぞ。

madgenius.hateblo.jp

GASでツイート

以下がソースです。
ライブラリが入ってないと動きませんが、上で追加していれば動きます。

CS_Twitter.TWITTER_CONSUMER_KEY = 'API_KEY';
CS_Twitter.TWITTER_CONSUMER_SECRET = 'API_SECRET';
CS_Twitter.OAUTH_USER_KEY = 'AutoTweetBot';

function AutoTweet() {
        CS_Twitter.TwitterSend("定期ツイートです。");
    }
}
  • API_KEY、API_SECRETはTwitterから取得したものに置き換えてください。

AutoTweet()をタイマーで呼んでやることでタイマーのタイミングで「定期ツイートです。」とツイートします。

TwitterAPIを利用するまでの登録など

Twitterと連携するアプリやウェブサイトを作るためにはTwitter DevelopersでAppとして登録する必要があります。
登録といってもTwitterアカウントがあれば無料でできるので安心してください。

TwitterDevelopersへ移動

以下より、TwitterAPIが作成できます。
Welcome — Twitter Developers

2017年8月現在ではこんなレイアウトです。
ちょっと前と変わりましたが基本的な配置は一緒です。
f:id:nanokanato:20170828134704p:plain:w300

右上のMy appsをクリック

My appsを開く

Twitterにログインしていない場合はこんな感じ。
ログインしましょう。
f:id:nanokanato:20170828135110p:plain:w300

まだ、Appを何も登録していない場合。
f:id:nanokanato:20170828134843p:plain:w300

すでにAppを登録している場合。
App名や説明は別に見られてもいいものですが、何をしているかバレて恥ずかしいので隠しました。
f:id:nanokanato:20170828135353p:plain:w300

画面の「Create New App」からAppを新規作成できます。

Appの新規作成

新規作成のページではApp名、利用目的の説明、ウェブサイト、CallBackURLの記入があります。
f:id:nanokanato:20170828135809p:plain

  • Name(必須)
    • App名
    • Twitter連携時に表示されます。
  • Description(必須)
    • 利用目的の説明
    • Twitter連携時に表示されます。
  • Website(必須)
    • 開発者のサイト
    • ブログがあればそれでもいいですし、簡単なのはTwitterのアカウントのページです。
    • Twitter連携時に表示されます。
  • CallBack URL
    • アプリ連携の場合は基本未記入です。
    • Botサービスなど外部サイトの場合、利用方法にCallBackにこのアドレスを記入と指示があると思います。
    • GoogleAppScriptの場合は「https://script.google.com/macros/d/{PROJECT KEY}/usercallback」を記入します。
      • PROJECT KEYはGoogleAppScriptで「ファイル > プロジェクトのプロパティ」で表示されたウィンドウのスクリプトIDと同じです。
        f:id:nanokanato:20170828143429p:plain:w300
      • また、PROJECT KEYは以下の画像の赤い部分です。
        f:id:nanokanato:20170828140953p:plain:w300
        URLは「script.google.com/a/hogehoge/d/{PROJECT KEY}/edit?usp=drive_web」となっています。
        最後の「/edit?usp=drive_web」はいりません。

入力したらDeveloper Agreementのところの利用規約に同意のチェックを入れて「Create your Twitter application」をクリック。

Appを登録するとその詳細ページに遷移すると思います。
遷移しない場合はMy Appsから探してください。
f:id:nanokanato:20170828141652p:plain:w300

Permissionsの確認

「Permissions」を選択することでそのAppが使えるTwitterAPIの権限が閲覧、編集できます。
f:id:nanokanato:20170828141920p:plain:w300

通常はRead and Writeですが、Twitterのデータを閲覧するだけでいいならRead only、DMの送信も行うならRead, Write and Access direct messagesに変更してください。
連携時にこの権限を許可しますか?という感じで表示されるので自分以外も使うのであれば適切なものを選びましょう。

KeyとSecretの取得

「Keys and Access Tokens」のタブを選択してください。
f:id:nanokanato:20170828142611p:plain:w300

ここのKeyとSecretの部分を使ってTwitterAPIを利用します。

【GAS】自動送信じゃないGmailをSNSに通知

私はそんなにメールを使わないのですが、
メールを使う営業の方からしてみればセミナーに参加した時などの自動送信メールに埋もれた個人宛の返信待ちのメールを見逃しがちなのでは?という発送で作りました。

GoogleAppScript(以下GAS)はGmailを持っていれば使えるのでエンジニア以外でも使えるのがいいですね。

機能としては

  • 個人宛の場合に必ず入っている単語、ここでは苗字を含む未読のメールを20件まで検索
  • 検索結果から特定のアドレスや単語を含むメールを排除し、10分以内に受け取ったものだけにする。
    • 残ったメール内容は何回も通知しないように既読にします。
  • 残ったメール内容をSNSに通知する。

GASとSNSの準備

以下、今回使うGASとChatWorkについての記事です。
GASを使用する準備は以下を参考にどうぞ。
madgenius.hateblo.jp

下のソースの最後にも同じ内容が入っていますが、GASでChatWorkに投稿する方法は以下を参考にしてください。
madgenius.hateblo.jp

その他APIが公開されているTwitterやSlackなども投稿部分の改変次第で利用可能です。

ソース

以下、実際にソース内容です。

function MinuteCheckGMail() {
    /*---------------------------------------*
     * 未読で苗字を含んでいるメールを新規100件取得
     *---------------------------------------*/
    var strTerms = 'is:unread "あなたの苗字"'; //未読で苗字を含むもの
    var myThreads = GmailApp.search(strTerms, 0, 20); //条件にマッチしたスレッドを100件取得
    var myMsgs = GmailApp.getMessagesForThreads(myThreads); //スレッドからメールを取得する →二次元配列で格納
    var valMsgs = [];
    var dateNow = new Date();
    
    /*---------------------------------------*
     * 10分以上前のものや自動配信のものを除外
     * 除外されないものはChatWorkに送るので既読にする
     *---------------------------------------*/
    var j = 0;
    for(var i = 0;i < myMsgs.length;i++){
        var myThread = myThreads[i];
        var myMsg = myMsgs[i];
        if (myMsg != null) {
            var date = myMsg[0].getDate();
            var from = myMsg[0].getFrom();
            var subject = myMsg[0].getSubject();
            var body = myMsg[0].getPlainBody();
            if (!from.match(/block_user@gmail.com/)) { //特定のアドレスからの通知を拒否
                //自動送信のニュースを拒否(Googleカレンダー,ChatWorkなど)
                if (!body.match(/配信停止/) && !body.match(/受信設定/) && !body.match(/通知設定/)) {
                    //受信日時が今じゃないなら拒否(10分前のものまで受けとる)
                    if (dateNow.getFullYear() == date.getFullYear()) {
                        if (dateNow.getMonth() == date.getMonth()) {
                            if (dateNow.getDate() == date.getDate()) {
                                if (dateNow.getHours() == date.getHours()) {
                                    if (dateNow.getMinutes()-10 <= date.getMinutes()) {
                                        myThread.markRead(); //ChatWorkに送信したものは既読にする
                                        valMsgs[j] = [];
                                        valMsgs[j][0] = date;
                                        valMsgs[j][1] = from;
                                        valMsgs[j][2] = subject;
                                        valMsgs[j][3] = body;
                                        j++;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    
    /*---------------------------------------*
     * 自分宛ての新着メールをチャットワークに送信する
     *---------------------------------------*/
    if (valMsgs.length > 0) {
        for (var i = 0; i < valMsgs.length; i++) {
            var valMsg = valMsgs[i];
            if (valMsg != null) {
                var strBody = "";
                strBody += "[info][title]"+valMsg[1]+"[/title]";
                strBody += "受信日時:"+valMsg[0].getFullYear()+"/"+valMsg[0].getMonth()+"/"+valMsg[0].getDate()+" "+valMsg[0].getHours()+":"+valMsg[0].getMinutes()+":"+valMsg[0].getSeconds()+"\n";
                strBody += "タイトル:"+valMsg[2]+"\n";
                strBody += "本文  :"+"\n"+valMsg[3];
                strBody += "[/info]";
                
                //ChatWorkに予定一覧を投稿する
                var client = ChatWorkClient.factory({token:"????????"});
                client.sendMessage({
                    room_id:00000000,
                    body:strBody
                });
            }
        }
    }
}
  • 実行時に認証するGoogleのアカウントに紐づくGmailを確認します。
    • タイマー設定は毎分にしてください。
  • 「'is:unread "あなたの苗字"';」の部分の「あなたの苗字」を苗字に変えてください。
  • 「if (!from.match(/block_user@gmail.com/)) {」の部分が特定のアドレスからの受信は通知しないように設定している部分です。
    • 複数設定する場合は「if (!from.match(/block_user@gmail.com/) && !from.match(/block_user_2@gmail.com/)) {」のようにしてください。
  • 「if (!body.match(/配信停止/)) {」の部分で「配信停止」を含むメールを除外しています。

動作

hogehoge@gmailから自分が認証したアカウントに「あなたの苗字」の部分を「苗字」に変えて送信した場合です。
f:id:nanokanato:20170828113954p:plain:w300

たまに2分ほど遅れて通知がきますが、未読であればちゃんと通知してくれます。
一応、同じhogehoge@gmailから「このメールは自動送信です。配信停止はこちら」みたいなメールを送りましたが、それは除外しているのでSNSに通知がきませんでした。

余談

GASでTwitterにツイートする記事です。
ツイート部分のアドレス(/update.json)を自分宛にDMを送信などにすれば使えます。
仕事のメールはChatWorkに、個人のメールはTwitterのDMになど変更可能です。
madgenius.hateblo.jp

【GAS】Googleカレンダーの予定をSNSへ通知

GoogleAppScript(以下GASと略称)でGoogleカレンダーの予定を自分のSNSにお知らせしてくれるツールを作りました。

普段、カレンダーを見る癖がない私ですが、SNSはよく見るのでSNSで自分宛に予定がくれば楽なので...

機能としては

  • 1週間後の予定まで確認(改変可能)
  • 予定の日時、または通知を設定した時刻(予定の10分前など)になったらSNSで自分宛に通知

SNSの部分は今回はChatWorkになっていますが、
仕事の予定はChatWork、個人はTwitterのDMなどの改変可能です。

GASを使用する準備は以下を参考にどうぞ。
madgenius.hateblo.jp

下のソースの最後にも同じ内容が入っていますが、GASでChatWorkに投稿する方法は以下を参考にしてください。
madgenius.hateblo.jp

以下、Googleカレンダーの予定をChatWorkに投稿するソース

/*------------------------------------*
 * 毎分カレンダーから1週間先までの予定を見る。
 * ↓
 * 予定が現在、または通知を登録していた時間だった場合お知らせする
 *------------------------------------*/
function MinuteCheckCalender() {
    //カレンダーからイベントの取得
    var myCals = CalendarApp.getCalendarById('hogehoge@gmail.com'); //特定のIDのカレンダーを取得
    if (myCals != null) { //権限がないなどの時はnullになるので処理をスルーする
        //カレンダーから現在〜1週間後までのイベントを取得
        var startDate = new Date();
        startDate = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate(), startDate.getHours(), startDate.getMinutes(), 0);
        var endDate = new Date();
        endDate.setDate(endDate.getDate()+7);
        var myEvents = myCals.getEvents(startDate, endDate);
        
        if (myEvents.length > 0) { //カレンダーに予定がない時はスルーする
            //お知らせが必要な予定一覧を作成する
            var strBody = "";
            for(var i = 0; i < myEvents.length; i++){
                var alertTime = -1;
                var strStart = myEvents[i].getStartTime(); //イベントの開始時刻
                //予定の日付と今日が同じかチェックする
                if (startDate.getFullYear() == strStart.getFullYear()) {
                    if (startDate.getMonth() == strStart.getMonth()) {
                        if (startDate.getDate() == strStart.getDate()) {
                            //予定の日時と今が同じ時分かチェックする
                            if (startDate.getHours() == strStart.getHours()) {
                                if (startDate.getMinutes() == strStart.getMinutes()) {
                                    //何分前の通知かを保持
                                    alertTime = 0;
                                }
                            }
                        }
                    }
                }
                        
                var intReminders = myEvents[i].getPopupReminders(); //何分前にお知らせするか
                if (intReminders != null) {
                    if (intReminders.length > 0) {
                        //予定の日時じゃなくても通知が登録されていれば通知時間にお知らせする
                        if (alertTime == -1) {
                            for (var j = 0; j < intReminders.length; j++) {
                                //通知として登録していた日時を取得
                                var minute = intReminders[j];
                                var alertDate = new Date(strStart.getTime());
                                alertDate.setMinutes(alertDate.getMinutes()-minute);
                                
                                //通知の日時と今日が同じかチェックする
                                if (startDate.getFullYear() == alertDate.getFullYear()) {
                                    if (startDate.getMonth() == alertDate.getMonth()) {
                                        if (startDate.getDate() == alertDate.getDate()) {
                                            //通知の日時と今が同じ時分かチェックする
                                            if (startDate.getHours() == alertDate.getHours()) {
                                                if (startDate.getMinutes() == alertDate.getMinutes()) {
                                                    //何分前の通知かを保持
                                                    alertTime = minute;
                                                    break;
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                
                if (alertTime >= 0) {
                    var strTitle = myEvents[i].getTitle(); //イベントのタイトル
                    var strEvent = "";
                    if (strBody != "") { //他の予定の後に追加する場合は空白行を追加する
                        strEvent += "\n\n";
                    }
                    if (alertTime == 0) {
                        strEvent += "[info][title]予定の時刻です[/title]";
                    } else {
                        if (alertTime < 60) {
                            strEvent += "[info][title]予定の"+alertTime+"分前です[/title]";
                        } else if (alertTime < 24*60) {
                            strEvent += "[info][title]予定の"+(alertTime/60)+"時間前です[/title]";
                        } else {
                            strEvent += "[info][title]予定の"+(alertTime/60/24)+"日前です[/title]";
                        }
                    }
                    strEvent += "件名:"+strTitle+"\n";
                    if (myEvents[i].isAllDayEvent()) {
                        strEvent += "時間:終日\n";
                    } else {
                        var strEnd = myEvents[i].getEndTime(); //イベントの終了時刻
                        strEvent += "時間:"+Utilities.formatDate(strStart,'JST','HH:mm')+"~"+Utilities.formatDate(strEnd,'JST','HH:mm')+"\n";
                    }
                    var strLocation = myEvents[i].getLocation(); //場所
                    strEvent += "場所:"+strLocation+"\n";
                    var strDescription = myEvents[i].getDescription(); //説明
                    strEvent += "説明:"+strDescription;
                    strEvent += "[/info]";
                    if (strEvent != "") {
                        strBody += strEvent;
                    }
                }
            }
            
            if (strBody != "") {
                //ChatWorkに予定一覧を投稿する
                var client = ChatWorkClient.factory({token:"???????????????"});
                client.sendMessage({
                    room_id:00000000,
                    body:strBody}
                );
            }
        }
    }
}
  • 最初のほうの「'hogehoge@gmail.com'」を自分のカレンダーを使っているGoogleのアドレスにしてください。
    • GSuiteなど企業用のアカウントで独自のドメインのものでも可能です。
  • 最後のChatWorkに投稿部分のAPIトークンとルームIDを変更してください。
    • わからない方はソースの上の「GASでChatWorkに投稿する方法」の記事の中で説明していますので参考に。

動作

実際に動かすとこんな感じ。
f:id:nanokanato:20170828110549p:plain:w300
自分から自分に送るのではなく、Bot用のアカウントから自分に送ると通知がくるのでオススメです。
さらにSNSスマホ用アプリを入れておくとPush通知でも確認できます。

現在、終日の場合は「時間:終日」となりますが予定日の0:00に通知がくるのでそこらへんは改変して使ってください。

余談

GASでTwitterにツイートする記事です。
ツイート部分のアドレス(/update.json)を自分宛にDMを送信などにすれば使えます。
仕事の予定はChatWorkに、個人の予定はTwitterのDMになどと使い分けが可能です。
madgenius.hateblo.jp

【GAS】ChatWorkに投稿

GoogleAppScript(以下GASと省略)でChatWorkに投稿する方法です。

ChatWorkAPIトークンの発行

まずはChatWorkAPIを使用するためにChatWorkからAPIトークンを発行する必要があります。

以下がChatWork公式のAPIドキュメントです。
developer.chatwork.com

ドキュメントの通り、動作設定を開き
f:id:nanokanato:20170828102714p:plain:w300

出てきたウィンドウのAPI発行のタブを表示して、パスワードを入力すればAPIトークンが表示されます。
f:id:nanokanato:20170828103023p:plain:w300

初回はAPIトークンが発行されていないので発行待ちがあるのと、ChatWorkに個人ではなく企業で契約していると管理者しかAPI発行のタブが表示されませんのでご注意ください。

GASに書き込み

APIトークンの取得に成功したら、GASで投稿してみましょう。
GASの準備などはこちらにまとめていますので参考にどうぞ。
madgenius.hateblo.jp

ChatWorkClientというライブラリを使用しますので「リソース > ライブラリ」を開き、プロジェクトキーに「1nf253qsOnZ-RcdcFu1Y2v4pGwTuuDxN5EbuvKEZprBWg764tjwA5fLav」を入力してライブラリを追加してください。
f:id:nanokanato:20170829123103p:plain:w300

以下のメソッドに引数を渡してあげるとその引数の文字列を投稿してくれます。

function ChatWorkSend(strBody) {
    //ChatWorkに予定一覧を投稿する
    var client = ChatWorkClient.factory({token:"????????"});
    client.sendMessage({
        room_id:00000000,
        body:strBody
    });
}
  • 「token:"????????"」の????????の部分に先ほど取得したAPIトークンを書き換え。
  • 「room_id:00000000」の00000000の部分にChatWorkのルームIDを設定してください。
    • ルームIDはAPIトークンを発行したアカウントが投稿できるルームを開きブラウザのURLの「kcw.kddi.ne.jp/#!rid00000000」のrid以下の数字になります。
    • また、アプリ版を使っている場合はURLが見れませんが、メッセージのURLをコピーするリンクボタンを押すとridが取得できます。

利用方法

簡単な例としては...
「ChatWorkSend("おはよう")」などをトリガーで日タイマー,午前7時~8時で登録すれば毎朝7時~8時に「おはよう」とChatWorkに投稿してくれます。

これだけではただのBotですが、GASはGoogleのサービスはもちろんTwitterなどAPIが公開されているサービスと連携できるのでいろいろな使い道があります。
面白い使い道があればぜひ私にも教えてください。

【Unity】Xcodeから実機ビルド時に「App Installation Failed」

はじめに

Unityから書き出したプロジェクトをXcodeで開きiPhoneへインストールしようとした時、以下のエラーが起きた。

`[App Installation Failed] Could not write to the device.`

`[App Installation Failed] This application does not support this device’s CPU type.`

XcodeでRun時にメッセージウィンドウとして表示されます。
頻度としては4回実行して上のエラーが3回に対して、下のエラーは1回くらいです。

動作環境

iPhone7(iOS11 beta)
XCode 9.0 Beta 3
MacBook PromacOS Sierra10.12.5)
UnityPro(5.6.2f1)

そうなった経緯

  1. 朝、問題になっているプロジェクトにて実機インストールができる状態であったことを確認。
  2. デバッグのためUnityのBuild SettingsにてDevelopment BuildとAutoconnect Profilerにチェックを入れる。
  3. その後、以下の記事を参考にIl2cppだったプロジェクトをMono2xに変更する。

www.f-sp.com

すると、エラーが出てインストールができなくなりました。

解決のためにやったこと

Mono2xからIl2cppに戻す

効果なし...

Unity Development Buildを解除

UnityのBuild SettingsにてDevelopment BuildとAutoconnect Profilerにチェックが入っていたので外したが効果なし。

以下は普通にエラー文で検索してみていろいろやってみました

正しいArchitecturesの設定かどうか

teratail.com
Targetの「Build Settings > Architectures > Valid Architectures」にiPhone7のarm64が追加されているか確認したが追加されていた...

容量

gootara.org
iPhoneの容量が足りず入れられない時にエラーが出るとあり、
iPhoneの容量が足りているか確認したが121.6GBも残っていた...

stackoverflow.com
Macの容量が足りないとインストールできずにエラーが出る的なことが英語で書いてあったのでMacの容量が足りているか確認したが67.09GB残っていた...

インストールしてある同じアプリを消す

qiita.com
インストールしているアプリを削除し、再度XcodeでRunしてインストールしようとしたが同じエラーが出た...

CocoaPods

neighborhood.bluz.io
CocoaPodsを使用している場合は.xcodeprojファイルではなく、.xcworkspaceファイルから起動しなくてはいけない。

forums.developer.apple.com
CocoaPodsを更新することで解決すると書かれている。

しかし、このプロジェクトはUnityでCocoaPodsは未使用です...

記事がなくなってきたので汎用的な祈りの儀式を始めます

再起動

iPhone,Macの再起動 & Xcodeのプロジェクトを消してUnityから再度ビルドしたが同じエラーは健在...

Gitから復元

GitからUnityのプロジェクトを復元しました。
そしてバックアップしていたその後の編集内容を手動でマージ....
動いた!

まとめ

  1. いろいろやってみて解決しなかったらGitから復元しよう
  2. 慣れないことをする前にはバックアップを取ろう

おそらく原因は「Il2cpp⇆Mono2x」だとは思いますが戻しても動かなくなるのは厄介ですね...