[CAIレシピ] CSVファイル読み込み・CSVファイル書き出し

## はじめに

この記事では、いつもよりも少し丁寧にCAIプロセスでCSVファイルを読み込む実装、CSVファイルを書き出す実装を確認します。

なお、この記事は次の記事の内容を理解していることを前提としています。

- [はじめてCAIを使う際に](https://qiita.com/shunsuzu/private/12b8dbd08a30efa0003a)
- [プロセスの作成と実行(匿名認証と基本認証)](https://qiita.com/shunsuzu/private/01d91caedb92bd7781ea)
- [割り当てステップと、各種変数フィールドの利用](https://qiita.com/shunsuzu/private/fae696539cd245828848)
- [プロセスオブジェクトによるユーザー定義変数の利用](https://qiita.com/shunsuzu/private/70f1c56f95536645e62f)
- [CAIでXQueryを利用する時に覚えておきたいこと](https://qiita.com/shunsuzu/private/00f351d0ab9896fa36dc)
- [テキストファイルの作成](https://qiita.com/shunsuzu/private/7b460174a8da9a31c228)

## CSVファイル読み込み・CSVファイル書き出し

CAIプロセスの実装手順を確認する前に、実装方針について簡単に説明します。

- __CSVファイルの読み込み__
CAIでは、CDIのように任意のタイミングでCSVファイルを読み取って、少し変換して、CSVファイルに簡単に出力する手段が用意されていません。ではどのように実装するのかと言うと、XQueryの標準関数`fn:unparsed-text`や`fn:tokennize`を利用します。
    - fn:unparsed-text ... テキストファイル全体を文字列として読み込む
    - fn:tokenize ... 指定文字(改行コード・カンマ)で文字列を分割してシーケンス(配列)を取得する
- __CSVファイルへの書き出し__
CSVファイルへの書き出しも、やはりCDIのように簡単には設定できません。ヘッダー部はXMLで定義します。CSVのデータ部については動的にXMLを生成するためにXQueryによるコーディングが必要になります。

### CAIプロセスの作成

最初に、今回CAIプロセスの動作確認をするために利用するCSVファイルを生成します。次のコマンドではカレントディレクトリに動作確認用のCSVファイルを生成しています。
```
cat << ___EOF___ > fruite.csv 
id, name, color
1, orange, orange
2, kiui, green
3, banana, yellow
4, mango, yellow
5, watermelon, red
6, melon, green
7, lemon, yellow
8, apple, red
9, grape, purple
___EOF___
```

次の手順では、CSVファイルを読み込んで[テキストファイルの作成](https://qiita.com/shunsuzu/private/7b460174a8da9a31c228)で定義したアプリケーション接続 __recipe-appConn-FileWrite__ のイベント __Delimited Content Writer__ を使ってCSVファイルを出力するCAIプロセスを作成しています。
`読み込んだCSVファイルのid/name列を、File接続で定義したディレクトリに書き出します、もとのCSVファイルで定義しているcolor列は意図的に欠落させています`

1. CAIプロセスを次の設定で作成します。
    - 名前を __recipe-psa-fileWriteCsv2Csv__ とする
    - __匿名アクセス__ を許可する
    - __Secure Agent__ にデプロイする
1. 一時フィールドとして __tmpFile__ を定義します。__定義された接続タイプ__ にはアプリケーション接続 __recipe-appConn-FileWrite__ を指定、__タイプ__ として __Delimited Content Serialization Task__ を指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074553/849ebfc8-f086-3c07-d532-ea1d512e7362.png)

1. 出力フィールドとして __output__ を定義します。__定義された接続タイプ__ にはアプリケーション接続 __recipe-appConn-FileWrite__ を指定、__タイプ__ として __Delimited Content Serialization Result__ を指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074553/09229f83-e1d7-4de8-0a94-3d54fb7789fc.png)



1. CAIプロセスが次の順に実行されるように各種ステップを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074553/28d83d94-699d-a7a0-e626-747f85ff8201.png)

1. 割り当てステップを選択して、次のように設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074553/d7a891f7-3ab4-7c67-353b-90e3361eb029.png)

   <table  style="margin: 0;">
        <tr>
            <th>フィールド</th>
            <th>割り当て</th>
            <th>値</th>
        </tr>
        <tr>
            <td>tmpFile > File name</td>
            <td>計算式</td>
            <td>fn:tokenize(\$inputPath , '/')[last()]<br>入力フィールドのファイルパスからファイル名を取得しています。</td>
        </tr>
        <tr>
            <td>tmpFile > Custom headers</td>
            <td>計算式</td>
            <td>(<br>&lt;Header><br>   &lt;fieldIndex>1&lt;/fieldIndex><br>   &lt;name>id&lt;/name><br>&lt;/Header>,<br>&lt;Header><br>   &lt;fieldIndex>2&lt;/fieldIndex><br>
   &lt;name>name&lt;/name><br>&lt;/Header><br>)<br>ヘッダーをXMLで指定しています。</td>
        </tr>
        <tr>
            <td>tmpFile > Custom objects</td>
            <td>計算式</td>
            <td>let \$text := fn:unparsed-text(\$input.inputPath )<br>let \$lines := tokenize(\$text , '\n')<br>let \$fieldnames:=tokenize(\$lines[1], '\s*,\s*')<br>return<br>  for \$line at \$i in \$lines<br>  where \$i gt 1<br>  let \$fields := tokenize(\$line, '\s*,\s*')<br>  return<br>  &lt;row>{<br>    for \$fieldname at \$j in \$fieldnames<br>    return<br>     element {fn:replace(\$fieldname,'"','')} {fn:replace(\$fields[\$j],'"','')}<br>  }&lt;/row><br>XQueryによるコーディングで、大きく次の2つの処理、『CSVファイルの読み込み』と『CSVファイル出力用のデータ生成』を行っています。コードについては<a href="#csvデータ部を生成するxqueryコードの説明">CSVデータ部を生成するXQueryコードの説明</a>に補足を書いてあります。</td>
        </tr>
    </table>


1. サービスステップを選択して __サービスタイプ=接続__、__接続=recipe-appConn-FileWrite__、__アクション=DelimitedContentWriter__ を指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074553/b9e311d6-a9eb-a667-8699-8c9c9d6b509d.png)

1. サービスステップの __入力フィールド__ タブを選択して、次のように設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074553/710ea7b7-6fe6-a9e4-5149-4c568f8e84a3.png)

1. 割り当てステップを選択して、出力フィールド __output__ に イベント __serializationResult__( __Delimited Content Writer__ の実行結果)を指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074553/67f0c80b-ddfe-7f4b-0879-4515e5caf28d.png)




### CAIプロセスの実行

curlコマンドを例とした動作確認結果です。ヘッダーの設定(Custom headers)にて、color列を含めていないため、出力ファイルにもcolor列が含まれない動作を確認できます。

```:curlコマンドと実行結果
curl -k https://localhost:7443/process-engine/public/rt/recipe-psa-fileWriteCsv2Csv \
-H 'Content-Type: application/json' \
-d '{"inputPath": "/opt/infaUSW5/ff/fruite.csv"}'

//実行結果
{
  "output": {
    "fileInfo": {
      "fullName": "fruite.csv",
      "name": "fruite",
      "ext": "csv",
      "path": "/opt/infaUSW5/ff/tgt/fruite.csv",
      "dir": "/opt/infaUSW5/ff/tgt",
      "size": 96,
      "lastModified": "2024-12-15T07:06:56Z"
    },
    "processedRecordsCount": 10,
    "writtenRecordsCount": 9,
    "success": true,
    "message": null
  }
}

//出力ファイルの確認
$ cat fruite.csv
id,name
1,orange
2,kiui
3,banana
4,mango
5,watermelon
6,melon
7,lemon
8,apple
9,grape
```

## CSVデータ部を生成するXQueryコードの説明

次のXQueryコードについての説明です。

```XQuery:CSVデータ部を生成するXQueryコード
01: let $text := fn:unparsed-text($input.inputPath )
02: let $lines := tokenize($text , '\n')
03: let $fieldnames:=tokenize($lines[1], '\s*,\s*')
04: return
05:   for $line at $i in $lines
06:   where $i gt 1
07:   let $fields := tokenize($line, '\s*,\s*')
08:   return
09:   <row>{
10:     for $fieldname at $j in $fieldnames
11:     return
12:      element {$fieldname} {$fields[$j]}
13:   }</row>

```

- 1行目: 入力フィールド __inputPath__ で指定されたファイルのテキストデータを取得しています。
- 2行目: 取得したテキストデータを改行コードで区切りシーケンス(配列)として取得しています。
- 3行目: テキストデータの1行目(`$lines[1]`)、つまりヘッダー行をカンマで区切りシーケンス(配列)としてからフィールド名を取得しています。なお、ヘッダー行のフィールド名前後には空白が入っていることが想定されるため、tokenizeによる分割処理時には、正規表現 `\s*,\s*` を指定しています。
- 5-6行目: テキストデータを改行コードで区切り、シーケンス(配列)の2行目以降(`where $i gt 1`)、つまりCSVファイルのデータ部を対象に処理します。
- 9,13行目: 行データを示すrow要素の開始・終了を記述しています。
- 10行目: 各フィールドを対象に処理します。この際、フィールド名に対応した位置(インデックス)を$jとして使えるように`for ... at ... in`という形式のfor文を使用しています。
- 12行目: この処理は `element {フィールド名} {フィールド値}` の形式になっています。例えば`element {"example"} {"This is content"}`は`<example>This is content</example>`を返します。

なお、このXQueryでは次のようなXMLデータを生成しています。

```XML
<row>
  <id>1</id>
  <name>orange</name>
  <color>orange </color>
</row>
<row>
  <id>2</id>
  <name>kiui</name>
  <color>green </color>
</row>
...略
```


## 修飾子付きのCSVファイル形式に対応するには?

CSVファイルはシンプルにカンマで区切られている形式のほかに、列情報をダブルクォートで囲んでいる修飾子付きのCSVファイル形式もあります。今回の実装例では、前者のシンプルな形式のCSVを使っていましたが、後者の修飾子付きのCSVを扱う場合には、実装に工夫が必要です。

参考までの情報となりますが(`CAIプロセスでは動作確認をしていませんが`)、Javaでは正規表現`,(?=(([^\"]*\"){2})*[^\"]*$)`により文字列を分割して修飾子付きのCSVファイルの各列を取得できる動作を確認しています。


## 参照
- [Qiita - IICS CAIの実装レシピ - マスターページ](https://qiita.com/shunsuzu/private/3d1971ef6e3b993516aa)