XMLアクティビティを使ってプロセスの情報を抽出しよう Part2

## はじめに

前回は、XMLの概念及びUiPath StudioにおけるXMLの操作方法についてご紹介しました。実際に、RPA開発者が日々触っているワークフロー（xmalファイルとして保存）もXML言語で記述されています。XML言語を使いこなせば、ワークフローを開かなくても、プロセスの編集と情報抽出が可能になります。特に、たくさんのプロセスに対して、一括で情報抽出の際に、この方法は非常に効率的なので、本記事は、Part 1の内容を土台にしてXMLによるxamlファイル情報抽出の方法について紹介しようと思います。まだPart１がご覧になっていない方はぜひ目を通していただければ幸いです。

https://qiita.com/Ouyang/items/59ce8f27778295799f74

## 環境準備

| 項目 | バージョン |
|:-----------|------------:|
| UiPath Studio| 2024.10.6        |
| UiPath.WebAPI.Activities     | 1.21.1      |
| 対応OS     | Windows      |

## XMLで記述されるxamlの構造

簡単なワークフローを作成してxmalファイルの実態を確認してみましょう。以下の手順に従って作成します。

①xmlReaderという名前を付けてプロジェクトを作成します。
②sampleという名前を付けて新しいシーケンスを作成します。
③メッセージボックスを配置して、"Hello World!"のテキストを記述します。
④シーケンスを保存して、プロジェクトフォルダでsample.xmalをNotepad++で開きます。

すると、以下のXML文章が表示されます。長いため、同じタグの一部のChild Elementを削除しました。

```ruby:sample.xamlのXML表示
<Activity mc:Ignorable="sap sap2010" x:Class="sample"
 VisualBasic.Settings="{x:Null}"
 sap2010:WorkflowViewState.IdRef="sample_1"
 xmlns="http://schemas.microsoft.com/netfx/2009/xaml/activities"
 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
 xmlns:sap="http://schemas.microsoft.com/netfx/2009/xaml/activities/presentation"
 xmlns:sap2010="http://schemas.microsoft.com/netfx/2010/xaml/activities/presentation"
 xmlns:scg="clr-namespace:System.Collections.Generic;assembly=System.Private.CoreLib"
 xmlns:sco="clr-namespace:System.Collections.ObjectModel;assembly=System.Private.CoreLib"
 xmlns:ui="http://schemas.uipath.com/workflow/activities" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <TextExpression.NamespacesForImplementation>
    <sco:Collection x:TypeArguments="x:String">
      <x:String>System.Activities</x:String>
      <x:String>System.Activities.Statements</x:String>
      <!-- 長いので省略 -->
    </sco:Collection>
  </TextExpression.NamespacesForImplementation>
  <TextExpression.ReferencesForImplementation>
    <sco:Collection x:TypeArguments="AssemblyReference">
      <AssemblyReference>Microsoft.VisualBasic</AssemblyReference>
      <AssemblyReference>mscorlib</AssemblyReference>
      <!-- 長いので省略 -->
    </sco:Collection>
  </TextExpression.ReferencesForImplementation>
  <Sequence DisplayName="sample" sap:VirtualizedContainerService.HintSize="1120,786.04" sap2010:WorkflowViewState.IdRef="Sequence_1">
    <sap:WorkflowViewStateService.ViewState>
      <scg:Dictionary x:TypeArguments="x:String, x:Object">
        <x:Boolean x:Key="IsExpanded">True</x:Boolean>
      </scg:Dictionary>
    </sap:WorkflowViewStateService.ViewState>
    <ui:MessageBox Caption="{x:Null}" ChosenButton="{x:Null}" AutoCloseAfter="00:00:00" 
	 DisplayName="メッセージ ボックス" sap:VirtualizedContainerService.HintSize="416,108"
	 sap2010:WorkflowViewState.IdRef="MessageBox_2" Text="[&quot;Hello World&quot;]" />
  </Sequence>
</Activity>
```

読みにくいですね。もっと中身を簡略化して構造だけを残すと、このようになります。前回の復習ですね。

```ruby:sample.xamlの構造(簡略版)
<Activity(ルート要素) x:Class="sample"(属性)
 xmlns:ui="http://schemas.uipath.com/workflow/activities"(名前空間)>
  <TextExpression.NamespacesForImplementation(子要素)>
    <sco:Collection(孫要素) x:TypeArguments="x:String"(孫要素の属性)>
      <x:String(曾孫要素)>System.Activities(曾孫要素のテキスト)</x:String>
    </sco:Collection>
  </TextExpression.NamespacesForImplementation>
  <Sequence(子要素) DisplayName="sample"(子要素の属性)>
    <ui:MessageBox(孫要素) Caption="{x:Null}" ChosenButton="{x:Null}" AutoCloseAfter="00:00:00" 
	 DisplayName="メッセージ ボックス" sap:VirtualizedContainerService.HintSize="416,108"
	 sap2010:WorkflowViewState.IdRef="MessageBox_2" Text="[&quot;Hello World&quot;]"(孫要素の属性)/>
  </Sequence>
</Activity>
```

たくさんの情報が入っていますが、今回の抽出作業に関する項目だけ、ご理解いただければと思います。

①`<Activity>`タグ：UiPathワークフローのルート要素であり、ワークフロー全体を囲んでいます。属性には、ワークフローの名前空間や、Visual Basic設定、ワークフローのIDなどが含まれます。

②`xmlns:ui="http://schemas.uipath.com/workflow/activities"`：UiPathのワークフローアクティビティを識別する名前空間です。ui プレフィックスを使って、UiPathのUI操作に関連するアクティビティ（例：メッセージボックス、ダイアログ、フォーム、UI操作など）を参照します。

③`<TextExpression.NamespacesForImplementation>`タグ：ワークフロー内で使用する名前空間のリストを定義します。ここには、System.ActivitiesやUiPathのアクティビティや基本的な.NET名前空間が多数含まれています。

④`<Sequence>`タグ：UiPathのシーケンスアクティビティを表します。シーケンスアクティビティは、順番に実行されるアクティビティの集合です。`DisplayName="sample"`で、シーケンスの名前が指定されています。

⑤`<ui:MessageBox>`タグ：UiPathの「メッセージボックス」アクティビティを定義します。中の`Text="[&quot;Hello World&quot;]"` は、メッセージボックスに表示するテキスト`"Hello World"`を指定しています。

## ワークフローの実装

今回の目標は、UiPathフォルダ（C:\Users\UserID\Documents\UiPath）配下における、すべてのプロセスの中のOpenBrowserで記録されているウェブサイトのURLを抽出しようと思います。以下はイメージです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334103/92a37da2-2748-307c-6556-bae93d92ed51.png)

ワークフローが少し長いため、まず完成図を頭に入れて、ステップバイステップで作成していきましょう。

![無題.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334103/0937fefb-78ce-ce9b-58eb-db396424e0e9.jpeg)

では、始めましょう。

①プロセスとそのURLを記録するために、「データテーブルを構築」を使います。ProcessNameとURLという2つの列を構築してdtUrlInfoとして保存します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334103/dd88d6b6-fdd1-858a-e5d9-23e6d282be9f.png)

②UiPathフォルダ配下のプロジェクトフォルダの情報を取得します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334103/b8999c56-2f50-3c3f-e958-76853ef609bb.png)

上図の赤枠の情報を取得するために、System.IO.DirectoryクラスのGetDirectoriesメソッドを利用します。

```ruby:コード
Dim fileDirectories() As String
fileDirectories = System.io.Directory.GetDirectories("C:\Users\UserID\Documents\UiPath")
```

次に、各フォルダ内のxmalファイルを探しだすために、「繰り返し（コレクションの各要素）」を使います。

```ruby:コード
For Each dire In fileDirectories
  Body   
Next
```
完成図は以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334103/1c0851f2-7066-0041-4250-8200fdbc61c7.png)

③Bodyを展開します。

まず、各プロセスの名前を取得します。Studioでプロセスを作成したら、UiPath配下でプロジェクトフォルダが作成され、そのプロジェクトフォルダの名前はプロセスの名前になります。

```ruby:コード
Dim fileName As String
fileName = System.IO.Path.GetFileName(dire)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334103/56c47ce3-6cee-d505-c81d-1c8f5fde8e1b.png)

次に、取得したプロジェクトフォルダの配下のxmalファイルを探し出します。一つのプロジェクトフォルダには複数のxmalが存在可能なので、配列の変数xamlFilesを作成して情報を保存します。

```ruby:コード
Dim xamlFiles() As String
xmlFiles = System.IO.Directory.GetFiles(dire,"*.xaml")
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334103/9f749cd1-c166-7212-d897-e3faa4b0bd94.png)

さらに、xmalファイルをループし、URLの取得を準備します。

```ruby:コード
For Each xamlFile in xamlFiles 
 Body
Next
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334103/e149f9fd-e497-6d6f-6e7f-5009ee3f70fb.png)

④Body2を展開します。いよいよXML操作が始まりますね！

i.「テキストファイルを読み込み」でxamlFileをstXamlとして出力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334103/ce5eea0b-652d-63e1-11aa-97b698c9acce.png)

ii.「XMLを逆シリアル化」でstXamlをXDocumentに変換します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334103/48bca13a-145d-54c8-1e9f-5d1bf1c8ca64.png)

iii.名前空間の取得

名前空間は、XML文書内で同じ要素（もしくは、同じ種類の要素）や属性が重複しないように区別するための仕組みです。XMLでは、同じ要素（もしくは、同じ種類の要素）が異なる名前空間に属していることがよくあります。そのため、名前空間を管理することが非常に重要です。通常、名前空間はxmlns属性を使って定義され、XMLタグで指定されたプレフィックス（例えば`ui`や`sap`）が、実際にどの名前空間に属するかを示します。イメージとして、名前空間は要素の住所のように、その要素の取得の前提となります。

さきほど説明した通り、`ui`というプレフィックスは、UiPathのアクティビティに関連する名前空間を指します。「OpenBrowser」もこの名前空間を利用しているため、XMLで「OpenBrowser」の情報を抽出するために、`ui`プレフィックスの指定が前提となります。

```ruby:Open Browserはuiプレフィックスと紐づいています
 <ui:OpenBrowser AutomaticallyDownloadWebDriver="{x:Null}" BrowserType="{x:Null}" CommunicationMethod="{x:Null}" Hidden="{x:Null}" NewSession="{x:Null}" Private="{x:Null}" UiBrowser="{x:Null}" DisplayName="ブラウザーを開く" sap:VirtualizedContainerService.HintSize="467,237" sap2010:WorkflowViewState.IdRef="OpenBrowser_1" Url="https://www.apple.com/jp/">
      <ui:OpenBrowser.Body>
        <ActivityAction x:TypeArguments="x:Object">
          <ActivityAction.Argument>
            <DelegateInArgument x:TypeArguments="x:Object" Name="ContextTarget" />
          </ActivityAction.Argument>
          <Sequence DisplayName="実行" sap:VirtualizedContainerService.HintSize="416,89" sap2010:WorkflowViewState.IdRef="Sequence_2">
            <sap:WorkflowViewStateService.ViewState>
              <scg:Dictionary x:TypeArguments="x:String, x:Object">
                <x:Boolean x:Key="IsExpanded">True</x:Boolean>
              </scg:Dictionary>
            </sap:WorkflowViewStateService.ViewState>
          </Sequence>
        </ActivityAction>
      </ui:OpenBrowser.Body>
```
上記をご理解いただいた上で、以下のコードを使って名前空間を取得します。
```ruby:コード

Dim uiNameSpace As System.Xml.Linq.XNameSpace
uiNameSpace = xDocument.Root.GetNamespaceOfPrefix("ui")
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334103/6c667ac7-58c9-9a4e-45c1-3d6ce58a7892.png)

iv.条件分岐を配置します

まず、xmalファイルにより、`ui`の名前空間が必ずしも存在するわけではありません。そのため、`ui`の名前空間が存在しない場合、現在の繰り返しをスキップして、次のxmalにいかないとエラーになるので、「Else」のところに「繰り返しをスキップ」を配置します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334103/6e73d0bd-080c-9e6c-2a1d-98cc8beea186.png)

次に、`ui`の名前空間が存在する場合の処理は、「Then」の中で書きます。

v.「Then」を展開してコードを書きます

まず、`ui`の名前空間に所属するOpenBrowserの要素を取り出します。Descendantsのメソッドを使いますので、まだわからない方はPart 1をご参照ください。

```ruby:コード
Dim elems As IEnumerable(Of XElement) = xDocument.Descendants(uiNameSpace+"OpenBrowser")

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334103/7d33eb40-c971-2bd6-b333-5a9df0cd8aa7.png)

次に、一つのxamlファイルに複数のOpenBrowserが存在する場合、複数のOpenBrowser要素が返ってくるので、elemsをループして個々のOpenBrowserのURLを取得する必要があります。

```ruby:コード
For Each currentXElement In elems
 Body3
Next
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334103/14de5887-f705-b510-b445-e831eea062ce.png)

vi.Body3を展開してURLを取得します

URLはOpenBrowser要素のどこで書いてあるのかを確認しましょう。

```ruby:URLの存在場所
 <ui:OpenBrowser 
 AutomaticallyDownloadWebDriver="{x:Null}"
 BrowserType="{x:Null}"
 CommunicationMethod="{x:Null}"
 Hidden="{x:Null}"
 NewSession="{x:Null}"
 Private="{x:Null}"
 UiBrowser="{x:Null}"
 DisplayName="ブラウザーを開く"
 sap:VirtualizedContainerService.HintSize="467,237"
 sap2010:WorkflowViewState.IdRef="OpenBrowser_1"
 Url="https://www.apple.com/jp/">
```

長いですが、`<ui:OpenBrowser>`に入っているので、Urlは`<ui:OpenBrowser>`の属
性ですね！そうしたら、Attributeのメソッドを使ってUrlを抽出してみましょう。

```ruby:Body3の中身
Dim Url As String
Url = currentXElement.Attribute("Url").Value.ToString
```

抽出した後、xmal名と共にdtUrlInfoに行として追加します。

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334103/d9df6da1-91e9-d59d-ce75-b0d792315a54.png)

最後はdtUrlInfoを出力して結果を確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3334103/3d5f692a-115d-8ef5-d8db-ed05f41ec35a.png)

## 終わりに
今回はxmalファイルをXDocumentに変換して中身の情報を抽出する方法をご紹介しました。活用シーンとしては、例えば、稼働中の数千本のプロセスから特定のURLに紐づくシステムの利用状況を洗い出すことが挙げられます。いちいちワークフローを開いて確認する必要はなく、わずか数秒で情報が入手できるため、ぜひ試してみてくださいね！