ワークフロー間の依存関係を可視化してみる #Alteryx

2024.03.01

はじめに

Alteryx では、汎用化可能な処理を「マクロ」として作成することで、ワークフロー(以下、WF とします)から呼び出すことが可能です。
これにより、既存 WF の可読性の向上や、組織においては、複雑なビジネスロジックを隠蔽し他のユーザーが実行可能とできる、といった利点があります。
マクロは、WF 内に複数含めることはもちろん、マクロ内にさらにマクロを含むような入れ子形式で使用することも可能です。
ただし、このような場合、WF とそこで使用されるマクロの依存関係を把握していないと、既存マクロに変更があると影響範囲がどこまで及ぶのかを把握できなくなります。
この依存関係をデータ化、または可視化できれば把握も容易になるので、検証してみた内容を記事としました。

マクロの追跡

WF ファイルの実体

Alteryx の WF の実体は XML ファイルとなっています。

WF ファイルをテキストエディタで開くと以下のようになっています。

<?xml version="1.0"?>
<AlteryxDocument yxmdVer="2023.2" RunE2="T">
  <Nodes>
    <Node ToolID="1">
      <GuiSettings Plugin="AlteryxBasePluginsGui.DbFileInput.DbFileInput">
        <Position x="210" y="198" />
      </GuiSettings>
      <Properties>
        <Configuration>
          <Passwords />
          <File RecordLimit="" SearchSubDirs="False" FileFormat="0" OutputFileName="">XXXX\SampleData\iris.csv</File>
          <FormatSpecificOptions>
            <HeaderRow>True</HeaderRow>
            <IgnoreErrors>False</IgnoreErrors>
            <AllowShareWrite>False</AllowShareWrite>
            <ImportLine>1</ImportLine>
            <FieldLen>254</FieldLen>
            <SingleThreadRead>False</SingleThreadRead>
            <IgnoreQuotes>DoubleQuotes</IgnoreQuotes>
            <Delimeter>,</Delimeter>
            <QuoteRecordBreak>False</QuoteRecordBreak>
            <CodePage>932</CodePage>
          </FormatSpecificOptions>
        </Configuration>
        <Annotation DisplayMode="0">
          <Name />
          <DefaultAnnotationText>iris.csv</DefaultAnnotationText>
          <Left value="False" />
        </Annotation>
        <Dependencies>
          <Implicit />
        </Dependencies>
        <MetaInfo connection="Output">
          <RecordInfo>
            <Field name="Sepal.Length" size="254" source="File: XXXX\SampleData\iris.csv" type="V_WString" />
            <Field name="Sepal.Width" size="254" source="File: XXXX\SampleData\iris.csv" type="V_WString" />
            <Field name="Petal.Length" size="254" source="File: XXXX\SampleData\iris.csv" type="V_WString" />
            <Field name="Petal.Width" size="254" source="File: XXXX\SampleData\iris.csv" type="V_WString" />
            <Field name="Species" size="254" source="File: XXXX\SampleData\iris.csv" type="V_WString" />
          </RecordInfo>
        </MetaInfo>
      </Properties>
      <EngineSettings EngineDll="AlteryxBasePluginsEngine.dll" EngineDllEntryPoint="AlteryxDbFileInput" />
    </Node>
  </Nodes>
  <Connections />
  <Properties>
    <Memory default="True" />
    <GlobalRecordLimit value="0" />
    <TempFiles default="True" />
    <Annotation on="True" includeToolName="False" />
    <ConvErrorLimit value="10" />
    <ConvErrorLimit_Stop value="False" />
    <CancelOnError value="False" />
    <DisableBrowse value="False" />
    <EnablePerformanceProfiling value="False" />
    <RunWithE2 value="True" />
    <PredictiveToolsCodePage value="932" />
    <DisableAllOutput value="False" />
    <ShowAllMacroMessages value="False" />
    <ShowConnectionStatusIsOn value="True" />
    <ShowConnectionStatusOnlyWhenRunning value="True" />
    <ZoomLevel value="0" />
    <LayoutType>Horizontal</LayoutType>
    <MetaInfo>
      <NameIsFileName value="True" />
      <Name>sample</Name>
      <Description />
      <RootToolName />
      <ToolVersion />
      <ToolInDb value="False" />
      <CategoryName />
      <SearchTags />
      <Author />
      <Company />
      <Copyright />
      <DescriptionLink actual="" displayed="" />
      <Example>
        <Description />
        <File />
      </Example>
      <WorkflowId value="6c5eb421-7b4e-42a5-b115-28f44b57d42d" />
      <Telemetry>
        <PreviousWorkflowId value="831b66d2-a24d-4777-9a19-256525cd260d" />
        <OriginWorkflowId value="831b66d2-a24d-4777-9a19-256525cd260d" />
      </Telemetry>
      <PlatformWorkflowId value="" />
    </MetaInfo>
    <Events>
      <Enabled value="True" />
    </Events>
  </Properties>
</AlteryxDocument>

この例ですと、ファイルからデータを読み込んでいることやデータファイル内の各カラムの情報などが見て取れます。

マクロを含む WF

マクロを含む WF をテキストエディタで開いてみます。

<?xml version="1.0"?>
<AlteryxDocument yxmdVer="2023.2" RunE2="T">
  <Nodes>
    <Node ToolID="2">
      <GuiSettings>
        <Position x="438" y="114" />
      </GuiSettings>
      <Properties>
        <Configuration>
          <Value name="リストボックス (8)" />
        </Configuration>
        <Annotation DisplayMode="0">
          <Name />
          <DefaultAnnotationText />
          <Left value="False" />
        </Annotation>
        <Dependencies>
          <Implicit />
        </Dependencies>
      </Properties>
      <EngineSettings Macro="D:\macros\Common\common-process-select.yxmc" />
    </Node>
  </Nodes>
  <Connections />
  <Properties>
    <Memory default="True" />
    <GlobalRecordLimit value="0" />
    <TempFiles default="True" />
    <Annotation on="True" includeToolName="False" />
    <ConvErrorLimit value="10" />
    <ConvErrorLimit_Stop value="False" />
    <CancelOnError value="False" />
    <DisableBrowse value="False" />
    <EnablePerformanceProfiling value="False" />
    <RunWithE2 value="True" />
    <PredictiveToolsCodePage value="932" />
    <DisableAllOutput value="False" />
    <ShowAllMacroMessages value="False" />
    <ShowConnectionStatusIsOn value="True" />
    <ShowConnectionStatusOnlyWhenRunning value="True" />
    <ZoomLevel value="0" />
    <LayoutType>Horizontal</LayoutType>
    <MetaInfo>
      <NameIsFileName value="True" />
      <Name>sample</Name>
      <Description />
      <RootToolName />
      <ToolVersion />
      <ToolInDb value="False" />
      <CategoryName />
      <SearchTags />
      <Author />
      <Company />
      <Copyright />
      <DescriptionLink actual="" displayed="" />
      <Example>
        <Description />
        <File />
      </Example>
      <WorkflowId value="6c5eb421-7b4e-42a5-b115-28f44b57d42d" />
      <Telemetry>
        <PreviousWorkflowId value="831b66d2-a24d-4777-9a19-256525cd260d" />
        <OriginWorkflowId value="831b66d2-a24d-4777-9a19-256525cd260d" />
      </Telemetry>
      <PlatformWorkflowId value="" />
    </MetaInfo>
    <Events>
      <Enabled value="True" />
    </Events>
  </Properties>
</AlteryxDocument>

この例では、「EngineSettings」という名前のタグがあり、Macroという属性を持っています。そして、この属性にはD:\macros\Common\common-process-select.yxmcという値が割り当てられています。

WF にマクロが含まれる場合、このMacro属性で、マクロファイル(ここではcommon-process-select.yxmc)へのパスを指しています。ただし、マクロが入れ子になっている場合、そのさらに内側のマクロを使用しているかどうか、までの情報を得ることはできません。

そのため、ここでは WF にマクロが含まれている場合、再帰的にマクロ属性値に関するパス情報を辿っていくことで、依存関係の追跡を試みます。

パスの表記について

上記の通り、Macro属性から依存関係を追跡するにあたり、再帰的な探索が必要なため、追跡の下となるプロジェクト等から各マクロパスに関して、以下の点に注意します。

  • マクロリポジトリの使用有無
    • マクロリポジトリからマクロを呼び出している場合、Macro属性値にはマクロの名称のみが含まれ、パスが含まれていません。マクロ内にさらにマクロが含まれているかどうかを探索するには、再度マクロファイルを XML として読み込む必要があるため、マクロリポジトリを使用する場合、マクロの名称と保存先のマッピングが可能な仕組みが必要です。
  • WF 依存関係として相対パスを使用している場合
    • 詳細オプションから、WF 依存関係に相対パスを使用している場合、対象の WF やマクロファイルを起点とした相対パス表記になります。再帰的にマクロファイルを読み込む際は、探索元ディレクトリからアクセス可能な仕組みを設ける必要があります。

前提条件

検証環境

上記より、本記事では以下の条件で検証を行いました。

  • Windows 10
  • Alteryx Designer 2023.2.1.7
  • マクロのパス設定
    • 検証に使用する WF やマクロファイルにマクロを追加する際は、その依存関係として「絶対パス」を使用します

また、依存関係の追跡には再帰的な探索が必要なので、ここでは R を使用しました

設定した依存関係

本記事では、入れ子になっているマクロを含む WF(prj1-WF1.yxmd)を用意しました。この WF を起点とし、この依存関係を可視化します。また、マクロファイルは以下のような関係です。

  • prj1-WF1.yxmd
    • common-process-select.yxmc
    • prj1-sub-process1.yxmc
      • prj1-process1.yxmc
      • common-process-input.yxmc
    • prj1-process2.yxmc
      • common-process-input .yxmc

可視化すると下図のイメージです。

prj1-WF1.yxmd には、3つのマクロファイルが含まれています。このうち prj1-sub-process1.yxmc , prj1-process2.yxmc にはさらにマクロファイルが含まれているような設定です。
また、注意点として、マクロ間で双方を参照し合う関係は想定していません。再帰的に見ていくにあたり、下図のような関係があるとループしてしまうので、ここではそのような依存関係はないと仮定し、検証します。

依存関係の探索

依存関係の探索には、R を使用しました。

> R.version
               _                                
platform       x86_64-w64-mingw32               
arch           x86_64                           
os             mingw32                          
crt            ucrt                             
system         x86_64, mingw32                  
status                                          
major          4                                
minor          2.2                              
year           2022                             
month          10                               
day            31                               
svn rev        83211                            
language       R                                
version.string R version 4.2.2 (2022-10-31 ucrt)
nickname       Innocent and Trusting

XML の解析にはxml2を使用しました。

> packageVersion("xml2")
[1] ‘1.3.6’

可視化にはigraphを使用しました。

> packageVersion("igraph")
[1] ‘2.0.2’

作業時のフォルダ構成は以下です。

D:.
├─macros #マクロは「macros」フォルダにまとめて配置
│  ├─Common
│  │      common-process-input.yxmc
│  │      common-process-select.yxmc
│  │
│  ├─macro_prj1
│         prj1-process1.yxmc
│         prj1-process2.yxmc
│         prj1-sub-process1.yxmc
│
│
├─WFdependency #R プロジェクト
│      WFdependency.Rproj
│
├─SamplePrj1 #WF の配置先
   │  prj1-WF1.yxmd

探索には、以下のコードを使用しました。

library(xml2)
library(igraph)

# 再帰関数
# xml_node: 現在探索中のXMLノード
# parent_path: 探索の起点となる WF のパス
# res: 依存関係を格納するリスト
extract_macros <- function(xml_node, parent_path = NA, res = list()) {

  # 現在のノードからEngineSettingsタグを探索
  find_node <- xml_find_all(xml_node, ".//EngineSettings")
  
  # Macro属性を抽出
  macro_values <- xml_attr(find_node, "Macro")
  macro_values <- na.omit(macro_values)
  
  # 親と子マクロの対をリストに追加していく
  for (macro_value in macro_values) {
    res[[length(res) + 1]] <- c(parent_path, macro_value)
    # 入れ子のマクロを探索
    macro_xml <- read_xml(macro_value)
    res <- extract_macros(macro_xml, macro_value, res)
  }
  
  return(res)
}

# 起点となる WF ファイルの読み込み
wf_path <-"..\\SamplePrj1\\prj1-WF1.yxmd"
wf <- read_xml(wf_path)

# 依存関係の抽出
res <- extract_macros(wf,parent_path="prj1-WF1.yxmd")

# 結果を行列に変換
res_mat<-lapply(res,function(x) matrix(basename(x), 1,2))
combined_mat <- do.call(rbind, res_mat)
colnames(combined_mat) <- c("ParentMacro", "ChildMacro")

# 結果
combined_mat

コードについてはここで深く触れませんが、ポイントは以下です。

  • 結果の長さが探索前では不明なので、空のリストを用意
  • マクロが入れ子かつ、任意の数のマクロが含まれる可能性があるので、再帰的に探索
  • 可視化にあたり結果を行列形式に変換

結果のオブジェクトは以下のようになっています。
[ParentMacro] 列が親となる WF やマクロファイル、[ChildMacro] 列が各ファイルに含まれるファイル(子)になります。

> combined_mat
     ParentMacro              ChildMacro                  
[1,] "prj1-WF1.yxmd"          "common-process-select.yxmc"
[2,] "prj1-WF1.yxmd"          "prj1-process2.yxmc"        
[3,] "prj1-process2.yxmc"     "common-process-input.yxmc" 
[4,] "prj1-WF1.yxmd"          "prj1-sub-process1.yxmc"    
[5,] "prj1-sub-process1.yxmc" "prj1-process1.yxmc"        
[6,] "prj1-sub-process1.yxmc" "common-process-input.yxmc"

可視化

可視化には、igrah パッケージによるネットワークグラフを使用します。
行列に変換した結果オブジェクトからグラフオブジェクトを作成し、プロットします。

# グラフオブジェクトの作成
g <- graph_from_edgelist(combined_mat, directed=TRUE)

# ノードの背景を非表示にする
V(g)$shape <- "none"

# ネットワーク図の描画
plot(g, layout=layout_as_tree(g, root=1), edge.arrow.size=.5, vertex.size=20, vertex.label.cex=0.65)

はじめのイメージと同様のネットワークグラフが描画できました。

さいごに

WF の依存関係の可視化を試してみました。
WF やマクロファイルの実体が XML のため、コードベースで追えるというのは特徴かなと思います。

ここでは、一つの WF を対象としましたが、同様の形式であれば、インプットとして WF のリストを渡すことで一括で追跡もできなくはないのかなと感じました。
また、その他の注意点として以下の記事でも述べられていましたが、クレンジングツールなどもともとマクロとして用意されているツールも XML 上では同様の表記となるため、WF やマクロに含まれる場合は、これらを除くなどの処理も必要です。

本記事では R で試しましたが、ツール自体は何でもよいので、Python などで他のパッケージによる可視化も試してみてもよいかなと感じました。
また、可視化までせずとも依存関係をデータとして出力するだけでも機械的にできれば管理の手間も少なくなると思うので、こちらの内容が何かの参考になれば幸いです。

参考