UiPathトラップ（ゲームブック風）

この記事は、[All About Group（株式会社オールアバウト） Advent Calendar 2022](https://qiita.com/advent-calendar/2022/allabout) 7日目の記事です。

初めての方ははじめまして、そうでない方はお久しぶりです。
@NACK と申します。

今年も「毎月７日は罠（わな＝０７）の日」……と言い張ってみます。
※記念日認定されているわけではありませんので、ご注意ください。

というわけで、今年も罠の話を、昔の[ゲームブック風](https://ja.wikipedia.org/wiki/%E3%82%B2%E3%83%BC%E3%83%A0%E3%83%96%E3%83%83%E3%82%AF)に書いてみます。
今年のお題はUiPathです。

「そういうお遊びは興味無いから！」という方は、直接 [罠の解説](#罠の解説)まで飛んでください。
罠の話が列挙されています。

# UiPathとは

ステキなRPAツールです。コードがかけなくてもアルゴリズムが組めれば、やりたいことがある程度実装できます（いわゆる「ノーコード・ローコード」なツール。ちょっと複雑なことがしたい場合はVB.NETでコードも書けます）。

ちなみに、基本的には有料ですが、個人や小規模なチーム・企業で使う場合は無料です（無料になる条件は色々ありますので、詳細な条件は必ず公式サイト他でご確認を）。
私も、個人でも使っています。

## 公式
https://www.uipath.com/ja

## 解説ページ
https://www.cresco.co.jp/blog/entry/19396/

https://penrako.com/communityinstall/


---
# UiPathトラップ（ゲームブック風）

----


### 1
あなたが道を歩いていると、祭囃子が聴こえてきた。
祭囃子に誘われるまま進んでいくと、「UiPath縁日」という看板を見つけた。

さてどうしようか？

- 寄ってみる（[#9へ](#9)）
- 寄らない（[#14へ](#14)）

----
### 2
わたあめがそれなりに大きくなってきたところで、機械が急に止まってしまった。
「あー、故障するの、こっちの機械でしたか。スイッチを入れなおしますね」
あなたはそれを断って、わたあめ屋を後にする（[#14へ](#14)）。

※[解説](#ワークフローファイルを呼び出しアクティビティの分離プロパティ)


----
### 3
あなたは帰路に就くことにした。

会場を出たところで、近くから「どーん」という音が聞こえてきた。
空を見上げると、大きな花火が打ち上げられている。
縁日を堪能したことを祝われているようで、あなたは少し嬉しくなった……かも知れない。

（お疲れ様でした！）

----
### 4	
無事に最後まで飲み切れた。ストローとカップを返却して [#13へ](#13)

----
### 5	
何もでてこない……。
「残念でしたね」

（[#14へ](#14)）

※[解説](#データテーブルのフィルタ結果が型によっては期待した結果にならない)

----
### 6
「ああ、ちょうど焼けたところですよ。どうぞ」

焼きたてのたこ、いか、ほたてが載ったパックを渡された。
あなたは、それをおいしく食べても良いし、お土産にしても良い。
いすれにせよ、海鮮焼き屋は堪能したので [#20へ](#20)

----
### 7	
ポイは流れるように水に差し込まれ、無事にスーパーボールをすくうことができた。
あなたは戦利品をポケットに入れ、次の屋台へ行くことにする（[#18へ](#18)）

----
### 8	
何もでてこない……。
「残念でしたね」

（[#14へ](#14)）

※[解説](#データテーブルのフィルタ結果が型によっては期待した結果にならない)

----
### 9
スーパーボールすくいをやっているようだ。

「スーパーボールまでポイを移動したらクリックしてください。うまくすくうコツは、流れるようにポイを水に入れることですよ」

どうやら、ポイの移動はUiPathのクリックアクティビティを使用する必要があるらしい。
ちなみに、「カーソルの動きの種類」プロパティのデフォルト値は `Instant` で、他に選べる値としては `Smooth` がある。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57720/8f9977b4-1ffc-f375-3ad1-4cbd6a459313.png)

さて、「カーソルの動きの種類」プロパティに何を設定しようか？

- `Instant` を設定する（[#19へ](#19)）
- `Smooth` を設定する（[#7へ](#7)）

----
### 10
くじが１枚でてきた。番号は"9"だ！
「おめでとうございます。こちら、特賞になります」

特賞として「温泉旅行券」をもらった。喜びをかみしめながら [#11へ](#11)

----
### 11
海鮮焼きの屋台だ。
「今から、たこ、いか、ほたてを焼いていきます。全部焼けた頃にまた来てください」
なお、焼くためのロジックはこうなっているらしい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57720/cba0ddec-c155-0f61-a239-97548b4747b6.png)

さて、いつ来ようか？

- 10分後（[#6へ](#6)）
- 30分後（[#17へ](#17)）

----
### 12
くじが２枚出てきた。番号は"9"と"10"のようだ。
「困りますね、くじは１人１枚ですよ」
叱られてしまったので、あなたはとぼとぼとその場を後にする。

（[#14へ](#14)）

※[解説](#データテーブルのフィルタ結果が型によっては期待した結果にならない)

----
### 13
わたあめの屋台を見つけた。
「わたあめを作っていきませんか？お好きな機械をどうぞ。ただ、どちらかの機械、使い続けているとメモリオーバーフローで故障するんですよね……」
機械は２台あるが、よく見ると「わりばしを１回まわす」というワークフローファイルを呼び出す際の「分離」プロパティのチェック状況が違っているようだ。
ちなみに「わりばしを１回まわす」ワークフローファイルは、内部でかなりのメモリを消費する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57720/f7cf4500-91be-3ad9-82e1-4d082031b1fa.png)

さて、どちらを使おうか？

- チェックが付いている機械を使う（[#15へ](#15)）
- チェックが付いていない機械を使う（[#2へ](#2)）

----
### 14
あなたは帰路に就くことにした。

帰宅後、外から「どーん」という音が聞こえてきた。
どうやら花火が打ち上げられているようだが、自宅からは何も見えない。
おそらく、さっきの縁日の会場辺りで上がっているのだろう。
もったいないことをしたような……。

（失敗。またの挑戦をお待ちしております）

----
### 15
おおきなわたあめを作ることができた。
あなたは、それをおいしく食べても良いし、お土産にしても良い。
いすれにせよ、わたあめ屋は堪能したので [#3へ](#3)

----
### 16
飲んでいる途中で、ストローがふやけて折れてしまった……。

（[#14へ](#14)）

※[解説](#データテーブルにフィルタをかけた結果が0件の場合)

----
### 17
「すみません、もう売り切れちゃったんですよ」
あなたはがっかりしながら海鮮焼き屋を後にする（[#14へ](#14)）。

※[解説](#繰り返しと並列繰り返しの使い分け)

----
### 18
「くじびきをやっていきませんか？特賞はよいものですよ！ただし、１人１枚までです。２枚以上引かないでくださいね」

くじには番号が振られていて、"1"～"10"までの10枚があるようだ。
ただし、くじの番号は「文字列型（String）」になっているので注意すること。

```vb:dtくじ
番号(String)
----------
”1”
”2”
”3”
”4”
”5”
”6”
”7”
”8”
”9”
”10”
```

なお、壁に貼ってある表を見る限り、特賞のくじは"9"のようだ。
さて、"9"だけを引くにはどうしたら良いだろうか？

-  「代入」で下記の式を設定する（[#10へ](#10)）
```vb
dtくじ.AsEnumerable _ 
    .Where(Function(row As DataRow) row("番号").ToString > "8") _
    .CopyToDatatable()
```
-  「代入」で下記の式を設定する（[#12へ](#12)）
```vb
dtくじ.AsEnumerable _ 
    .Where(Function(row As DataRow) CInt(row("番号")) > 8) _
    .CopyToDatatable()
```
- 「データ テーブルをフィルター」で"番号"が"9"以上 を保持（[#8へ](#8)）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57720/d7378a61-7d89-f838-f3df-6ab1baa11b7d.png)

- 「データ テーブルをフィルター」で"番号"が9（※Int型）を保持（[#5へ](#5)）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57720/ce4f1439-592a-6b39-454f-0c440dfaf1b8.png)



----
### 19
ポイは瞬時に水に突っ込まれ、その衝撃で破けてしまった……（[#14へ](#14)）。

※[解説](#クリックアクティビティのカーソルの動きの種類プロパティ)

----
### 20
喉が渇いてきたので飲み物を買うことにした。わらび餅ドリンクが売っていたのでそれにしよう。

```vb:dtわらび餅ドリンク
中身(String)
----------
”わらび餅”
”飲み物”
```

ストローが２種類あり、片方は途中で壊れそうだ。
全て飲み干して、入れ物を空にしたいのだが……さて、どちらを選ぼうか？

-  「代入」で下記の式を代入（[#16へ](#16)）
```vb
dtわらび餅ドリンク.AsEnumerable _ 
    .Where(Function(row As DataRow) row("中身") Is Nothing) _
    .CopyToDatatable()
```
- 「データ テーブルをフィルター」で"中身" IsEmpty を保持（[#4へ](#4)）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57720/7cc95d10-6567-50dc-a7a2-39e8d35a5d4c.png)


# 罠の解説

## クリックアクティビティのカーソルの動きの種類プロパティ
参考： https://docs.uipath.com/activities/lang-ja/docs/click

カーソルの動きの種類 には`Instant`と`Smooth`があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57720/8f9977b4-1ffc-f375-3ad1-4cbd6a459313.png)

デフォルトは`Instans`で、これを選ぶとクリックしたいオブジェクトまでカーソルが瞬時に移動します（カーソルがテレポートするイメージです）。
通常はこれで問題ないのですが、「マウスホバーした状態でしか表示されないメニューを表示した状態で、そのメニュー上のボタンをクリックする」場合、稀にクリックが失敗することがあります（クリックする前にカーソルが外れた結果、メニューが消えてしまう）。
そういう場合は`Smooth`を使うと、人間が操作した時と同様、前のオブジェクトから次のオブジェクトまで実際にカーソルを移動してくれるので、メニューが消えずにクリックできます。

私の場合、Googleスプレッドシートの「拡張機能」内のサブメニューを操作している時にこの現象が発生し（毎回失敗するわけではなく、１０回に１回くらい失敗する）、地味に困っていました（仕方ないのでリトライ入れたりして何とか……）。
`Smooth`に変更してからは一度も失敗していません。


## データテーブルのフィルタ結果が型によっては期待した結果にならない
スプレッドシートから値を取得した場合等、「データテーブルの値が全て文字列型」というケースはよくあるかと思います。
それらのデータテーブルに「データ テーブルをフィルター」アクティビティを使う場合、型の違いによる注意が必要です。

- 「データ テーブルをフィルター」の場合、文字列型の大小比較（ `<` や `>` など）はできないようです（少なくとも、自分がやってみた限りはそうでした。例えば、文字列であっても「"2">"1"」は成立するはずなのですが、成立するはずのフィルタを設定しても、取得結果が0件になります）。
- 「データ テーブルをフィルター」でも `=` は使えますが、比較対象の値と型が合っていない場合、正しく比較できません（データテーブル側の値が文字列の時にフィルター条件に数値を比較したところ、取得結果が０件になりました）。

### 解決方法
- データテーブルに「フィルタをかけたい型の項目」を追加し、そちらに型を変換した値を格納→「追加した項目」を条件に「データテーブルをフィルタ」を実施する。
例： `価格（文字列）`という項目があり、これを数値としてフィルタリングしたい場合
    - `価格（数値）` という項目を新規作成（型はInt型）
    - そちらに`価格（文字列）`の値をInt型に置換して格納
    - `価格（数値）` でフィルタリングを実施。

　⇒この方法の場合、UiPathの標準機能だけでできますが、データ件数が大量にある場合、時間がかかりますし、可読性も落ちます。また、価格に変更があった場合、複数の項目を修正する必要があります。

- LINQのWhereメソッドでフィルタをかける（オススメ）
このやり方の場合、文字列のまま大小比較してフィルタリングすることもできますし、別の型（Int型など）に変換した状態でフィルタリングすることも可能です。とても便利なのですが、フィルタリングした結果が0件になる場合はエラーになるので注意してください（詳細は後述）。

```vb:例）価格が100以上のデータをフィルタリング
dt.AsEnumerable _ 
    .Where(Function(row As DataRow) CInt(row("価格（文字列）")) >= 100) _
    .CopyToDatatable()
```

## データテーブルにフィルタをかけた結果が0件の場合
参考： https://it-strategist.blogspot.com/2012/02/datarow.html

「データテーブルをフィルター」アクティビティを使った場合、結果が０件でも特に問題は起きません。
が、前項でご紹介した「LINQのWhereメソッドでフィルタをかける」場合、フィルタをかけた結果が0件だと`ソースには、DataRow が含まれていません。`というエラーになってしまいます。

エラー回避方法としては、参考記事で触れていらっしゃるように「事前にCountしておき、0件以上だったらCopyToDatatableする」のが素直なように思います。
別案として「try-catchで囲い、エラーになったら0件とみなす」という方法もあります（ただ、こちらの場合、他の原因でエラーになった時に誤認する危険性もあるので、catchするExceptionの種類は`System.InvalidOperationException`に限定するのがオススメです）。
どちらを採用するかはお好みでどうぞ。


## 繰り返しと並列繰り返しの使い分け
コレクションの各要素を繰り返す場合、「繰り返し (コレクションの各要素)」と「並列繰り返し (コレクションの各要素)」の２種類があります。

例えば、（ゲームブック部分にも書きましたが）こんな処理があったとします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57720/cba0ddec-c155-0f61-a239-97548b4747b6.png)

これの「並列繰り返し(コレクションの各要素)」のところが「繰り返し(コレクションの各要素)」だった場合、出力結果は下記のようになります。それぞれの処理を順に行うので、所要時間は約３０分です。

```
たこ
たこ焼き
いか
いか焼き
ほたて
ほたて焼き
```

ですが、「並列繰り返し(コレクションの各要素)」の場合はこうなります。

```
ほたて
いか
たこ
ほたて焼き
いか焼き
たこ焼き
```

何故かというと、並列繰り返しの場合、コレクションの各要素に対する処理が同時にまとめて行われるのです。
「全ての要素が同時に処理される」ので、要素の定義順等も関係ありません（そのため、最初に定義した「たこ」より先に「ほたて」が焼かれたりします）。所要時間も約１０分に短縮されます（「要素１つにかかる時間」だけで済むので）。

### 並列繰り返しのメリット
何と言っても処理が早いことです。
コレクションに100個データが設定されていて、1つ辺りの処理に1秒かかる場合、通常の繰り返しなら100秒かかりますが、並列繰り返しなら1秒で終わります。

### 並列繰り返しのデメリット
残念ながら、実は色々あります。
- 処理が直感的でなくなる
通常、繰り返し処理は「１つずつ順番に実行される」と思っているので、ここだけ「同時に実行される」となると、コードを読んでいる時に若干混乱します（これは、まあ「覚えようよ」で済む話ではありますが……）
- 順番が制御できない
例えば「コレクションのデータを加工して、データテーブルに行追加する」といった処理を行う場合、コレクションのデータ順にデータが登録されるとは限りません。全ての処理は非同期で行われますので、「処理が早く終わったもの」から順にデータテーブルに行追加されます。
- メモリ・プロセスの使用量が激増する
コレクションに100個データが設定されている場合、この100個の処理を一度に行いますので、その分、メモリもプロセスも使用量が増えます。
- UiPathで「並列繰り返し」に対応していない処理が多い
例えば、Excelを処理するアクティビティなどは非対応なので、「Excelファイルを同時に100個処理する」といったことはできないようです。

なお、（「並列繰り返し」の説明ではありませんが）類似処理である「並列」についての詳しい記事がありますので、そちらも参考にしていただければ。
https://www.uipath.com/ja/blog/developer/about-parallel-activities

## ワークフローファイルを呼び出しアクティビティの分離プロパティ
参考：http://aki.p-kin.net/uipath/rpa%E3%81%AE%E3%83%A1%E3%83%A2%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%AF%E5%AF%BE%E7%AD%96%E3%80%80%E8%A7%A3%E6%B1%BA%E7%B7%A8


とあるアプリで、「長時間動かしたままにすると、メモリ不足でエラーになる」という現象に見舞われました。
調べてみたところ、UiPathにおいて、プロセスが終了するまでメモリが解放されないケースがあるようです。

このケースに対処するため、当該アプリ内の、いくつかの「ワークフローファイルを呼び出し」アクティビティの「分離」プロパティにチェックを付けました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57720/f7cf4500-91be-3ad9-82e1-4d082031b1fa.png)

そうすると、そのワークフローファイルの処理は別プロセスで実行されます。そのワークフローファイルの処理が終わった時点で、「別プロセス」は終了するので、そのプロセス内で使用したメモリもその時点で解放されるのです。


なお、デメリットとしては「別プロセスを立ち上げるので、処理実行中の総メモリ使用量は少し増える」そうで……分離プロパティ自体の詳細な説明はこちらをどうぞ。
https://www.uipath.com/ja/blog/developer/invoke-workflow-file-tips

ついでに、「分離プロパティを変更してもメモリ不足が解消しないよ！」という場合に参考になりそうな記事もご紹介しておきます。
https://www.uipath.com/ja/resources/knowledge-base/out-of-memory



# （ゲームブック部分の）参考文献
特になし。
最近、古い海外ゲームブックが日本で続々復刻しているため、ちょっとネタとして使いにくくなってきました（小ネタ部分しか参考にしていないものの、絶版ではない本のネタバレは避けたいので……）。

せっかくなので、最近復刻された本のご紹介でも。

http://www.groupsne.co.jp/products/aff/affcolection1.html