法人番号データを扱う

日本にある企業データを管理するのであれば、国税庁が管理している法人等の基本３情報データをCSVで取得することができます。

本日はこのCSVを取り込むプログラムを書いています。

# 基本3情報

基本3情報とは

1. 法人名（団体名）：法人または団体の正式な名称です。
2. 本店所在地：法人または団体の主たる事務所の住所です。
3. 法人番号：法人に割り当てられる一意の識別番号で、13桁の数字で構成されています。法人番号は、商業登記などに基づいて法務省が発行するもので、法人が公的に識別されるために使用されます。

になります。

# 環境

環境はASP.NET8+Azure Tableになります。

# データを取り込む

データはこちらのページからダウンロードすることができます。

https://www.houjin-bangou.nta.go.jp/download/zenken/

全国ファイルをダウンロードすると219MB、数にすると500万件ほどになります。全件データの命名ルールは

`都道府県コード_都道府県名_all_作成年月日〔_一連番号〕.zip`

になります。　プログラムを書いて取得するというよりは、手動でダウンロード→解凍→解凍したCSVファイルを読込んでAzure Tableに書き込むとした方が手軽です。（差分データに関しては取得も自動化したほうがいいと思いますので、命名ルールに関しては後で触れます）

CSVにはヘッダーもないようなので、リソース定義書を見ながら、受け側のクラスを設計します。

# リソース定義書

## 定義

各リソースの定義は下記の通りです。

| 項番 | 項目名               | リソース名              | 形式 (凡例参照)  | 桁数 | コード | 項目値名称        | ダウンロード | Web-API | 項目の説明 |
|------|----------------------|-------------------------|-----------------|-----|-------|-----------------|--------------|---------|------------|
| 1    | ルート要素           | corporations            | -               | -   | -     | -               | ○            | ○       | ルート要素は、XML文書に必要な要素で、XML文書のデータ階層構造の最上位に位置する要素。このルート要素は、CSV形式で取得する場合は設定されない（XMLのみ）。 |
| 2    | 最終更新年月日       | lastUpdateDate          | YYYY‐MM‐DD       | 10  | -     | -               | ○            | ○       | Web-APIで情報を取得した際に、ヘッダー情報として出力される情報。この最終更新年月日は、公表用のデータベースを最後に更新した日付を表す。 |
| 3    | 総件数               | count                   | ZZZZZZZ9         | 1～8| -     | -               | ○            | ○       | Web-APIで情報を取得した際にヘッダー情報として出力される情報。総件数は、Web-APIで指定した条件に合致したデータの総件数を表す。 |
| 4    | 分割番号             | divideNumber            | ZZZZ9            | 1～5| -     | -               | ○            | ○       | Web-APIの取得期間や法人名を指定して情報を取得する場合の一度に取得することができるデータ件数を2,000件に制限するため、条件に合致するデータを取得する際にファイルが分割されることがある。 |
| 5    | 分割数               | divideSize              | ZZZZ9            | 1～5| -     | -               | ○            | ○       | Web-APIで情報を取得した際にヘッダー情報として出力される情報。分割数は、分割番号の分母を表すデータ項目。条件に合致する情報（ファイル）の取得において分割されない場合、値が「1」となる。 |
| 6    | 法人等要素           | corporation             | -               | -   | -     | -               | ○            | ○       | 法人等要素は、XML文書のデータ階層構造上、項番7「一連番号」から項番36「検索対象除外」までの上位に位置する親要素。法人等要素は、CSV形式で取得する場合は設定されない（XMLのみ）。 |
| 7    | 一連番号             | sequenceNumber          | ZZZZZZZ9         | 1～8| -     | -               | ○            | ○       | 全件データやWeb-APIによる法人番号を指定した最新情報の取得以外の場合には、同一法人に関する情報が複数存在することがある。その際に、法人単位に時系列の更新処理等を行う場合、一連番号が小さい値の情報から順番に処理することにより、時系列に順序性を保って更新処理を行うことが可能となる項目。 |
| 8    | 法人番号             | corporateNumber         | 9999999999999    | 13  | -     | -               | ○            | ○       | 法人番号の指定を受けた者の法人番号を示すデータ項目。 |
| 9    | 処理区分             | process                 | 99              | 2   | -     | -               | ○            | ○       | 法人番号の指定、商号又は所在地に変更等が発生した事由をコード値で表す項目。処理区分が「99：削除」の場合は、訂正区分はブランクとなる。内容に誤りがあった場合は、当該データ項目を活用し、誤った内容のデータを削除して訂正データを取り込むことにより保有データを訂正することができる。 |
| 10   | 訂正区分             | correct                 | 9               | 1   | -     | -               | ○            | ○       | 提供していたデータについて、次の場合に値を設定するデータ項目。履歴データの内容に誤りがあり、訂正が生じた場合や、履歴データの追加が生じた場合など。 |
| 11   | 更新年月日           | updateDate              | YYYY‐MM‐DD       | 10  | -     | -               | ○            | ○       | 法務省等からデータを受け取り、当庁でデータを更新した日付を表す。 |
| 12   | 変更年月日           | changeDate              | YYYY‐MM‐DD       | 10  | -     | -               | ○            | ○       | 事由が発生した日付を表すデータ項目。処理区分が新規（01）の場合、法人番号が指定された年月日を意味する。 |
| 13   | 商号又は名称         | name                    | 全角文字         | 150 | -     | -               | ○            | ○       | 法人番号保有者の商号又は名称を示すデータ項目。商号又は名称の文字数が、150文字を超過した場合、151文字目以降の文字は格納されないため、イメージファイルを閲覧することにより確認できる。 |
| 14   | 商号又は名称イメージID | nameImageId          | 99999999        | 8   | -     | -               | ○            | ○       | 商号又は名称イメージIDは、イメージファイルを閲覧するために指定する値。当該イメージIDの値を設定したURLでアクセスすることにより、国税庁法人番号公表サイトの検索画面で確認できるイメージファイルと同様のイメージファイルを直接閲覧することができる。 |
| 15   | 法人種別             | kind                    | 999             | 3   | -     | -               | ○            | ○       | 法人種別を判別するためのデータ項目。例えば、処理対象として必要としない法人（組織区分）のデータを、法人種別のコード値を利用して除外設定を行うなどの活用が考えられる。 |
| 16   | 国内所在地（都道府県） | prefectureName        | 全角文字         | 10  | -     | -               | ○            | ○       | 法人番号保有者の本店又は主たる事務所の所在地の都道府県名。 |
| 17   | 国内所在地（市区町村） | cityName              | 全角文字         | 20  | -     | -               | ○            | ○       | 法人番号保有者の本店又は主たる事務所の所在地の市区町村名。 |
| 18   | 国内所在地（丁目番地等）| streetNumber          | 全角文字         | 300 | -     | -               | ○            | ○       | 法人番号保有者の本店又は主たる事務所の所在地の丁目番地等。 |
| 19   | 国内所在地イメージID | addressImageId          | 99999999         | 8   | -     | -               | ○            | ○       | 国内所在地イメージIDは、イメージファイルを閲覧するために指定する値。国内所在地にJIS第１・第２水準以外の文字を使用している場合及び国内所在地の文字数が300文字を超過した場合に値を設定する項目。 |
| 20   | 都道府県コード        | prefectureCode         | 99              | 2   | -     | JIS X 0401 に準ずる | ○        | ○       | データを取り込み、名寄せ作業や不要なデータを識別する際、当該コードを活用することにより効率的な作業を行うことができる。 |
| 21   | 市区町村コード        | cityCode               | 999             | 3   | -     | JIS X 0402 に準ずる | ○        | ○       | 国内所在地の市区町村コード。 |
| 22   | 郵便番号             | postCode               | 9999999          | 7   | -     | -               | ○            | ○       | 国内所在地の文字情報を基に設定した郵便番号。全国町・字ファイルを基に設定しているため、所在地に外字が含まれる場合や、誤字脱字がある場合には、正確な郵便番号が設定されていない場合がある。 |
| 23   | 国外所在地           | addressOutside         | 全角文字         | 300 | -     | -               | ○            | ○       | 本店又は主たる事務所の所在地が国外にある法人番号保有者の国外における本店又は主たる事務所の所在地を示すデータ項目。 |
| 24   | 国外所在地イメージID | addressOutsideImageId  | 99999999        | 8   | -     | -               | ○            | ○       | 国外所在地イメージIDは、イメージファイルを閲覧するために指定する値。 |
| 25   | 登記記録の閉鎖等年月日 | closeDate            | YYYY‐MM‐DD       | 10  | -     | -               | ○            | ○       | 登記記録の閉鎖等の事由が生じた年月日を表す。 |
| 26   | 登記記録の閉鎖等の事由 | closeCause           | 99              | 2   | -     | -               | ○            | ○       | 登記記録の閉鎖等が生じた事由を表すデータ項目。 |
| 27   | 承継先法人番号       | successorCorporateNumber | 9999999999999 | 13  | -     | -               | ○            | ○       | 合併等による事業承継があったことにより登記記録が閉鎖された場合の存続する法人の法人番号。 |
| 28   | 変更事由の詳細       | changeCause           | 全角半角混在     | 500 | -     | -               | ○            | ○       | 合併等による事業承継があった場合の事業承継内容を示すデータ項目。 |
| 29   | 法人番号指定年月日   | assignmentDate        | YYYY‐MM‐DD       | 10  | -     | -               | ○            | ○       | 法人番号指定年月日は、法人番号が指定された年月日を表す。 |
| 30   | 最新履歴             | latest                | 9               | 1   | -     | -               | ○            | ○       | 応答結果のデータが、最新の情報か過去の情報かを表す。 |
| 31   | 商号又は名称（英語表記）| enName              | 半角英数記号      | 300 | -     | -               | ○            | ○       | 法人番号保有者が登録した商号又は名称（英語表記）。 |
| 32   | 国内所在地（都道府県）（英語表記） | enPrefectureName | 半角英字         | 9   | -     | -               | ○            | ○       | 法人番号保有者が登録した本店又は主たる事務所の所在地の都道府県名（英語表記）。 |
| 33   | 国内所在地（市区町村丁目番地等）（英語表記） | enCityName | 半角英数記号 | 600 | -     | -               | ○            | ○       | 法人番号保有者が登録した本店又は主たる事務所の所在地の丁目番地等（英語表記）。 |
| 34   | 国外所在地（英語表記）| enAddressOutside     | 半角英数記号      | 600 | -     | -               | ○            | ○       | 本店又は主たる事務所の所在地が国外にある法人番号保有者が登録した国外における本店又は主たる事務所の所在地（英語表記）。 |
| 35   | フリガナ             | furigana              | 全角文字         | 500 | -     | -               | ○            | ○       | 法人番号保有者の商号又は名称に対するフリガナ情報を示すデータ項目。 |
| 36   | 検索対象除外         | hihyoji               | 9               | 1   | -     | -               | ○            | ○       | 設立登記法人のうち、登記上の所在地が既に廃止されていることが確認できた法人について、検索対象から除外していることを示すデータ項目。 |

## モデル

今回必要な項目をマッピングするためのモデルを下記のように定義してみます。


```csharp

 public class CsvJapanCorpNumberRecord
 {
     [Index(0)]
     public int RowNumber { get; set; }
     [Index(1)]
     public string? CorporateNumber { get; set; }
     [Index(2)]
     public int? ProcessNumber { get; set; }
     [Index(3)]
     public int CorrectNumber { get; set; }
     [Index(4)]
     public DateTime? UpdateDate { get; set; }
     [Index(5)]
     public DateTime? ChnageDate { get; set; }
     [Index(6)]
     public string? Name { get; set; }
     [Index(7)]
     public string? ImageId { get; set; }
     [Index(8)]
     public int? Kind { get; set; }
     [Index(9)]
     public string? PrefectureName { get; set; }
     [Index(10)]
     public string? CityName { get; set; }
     [Index(11)]
     public string? StreetName { get; set; }
     [Index(12)]
     public string? AddressImageId { get; set; }
     [Index(13)]
     public int? PrefectureCode { get; set; }
     [Index(14)]
     public int? CityCode { get; set; }
     [Index(15)]
     public string? PostalCode { get; set; }
     [Index(16)]
     public string? AddressOutside { get; set; }
     [Index(17)]
     public string? AddressOutsideImageId { get; set; }
     [Index(18)]
     public DateTime? CloseDate { get; set; }

     [Index(19)]
     public string? CloseCause { get; set; }
     [Index(20)]
     public string? SuccessorCorporateNumber { get; set; }
     [Index(21)]
     public string? ChangeCause { get; set; }
     [Index(22)]
     public DateTime? AssignmentDate { get; set; }
     [Index(23)]
     public int? Latest { get; set; }
     [Index(24)]
     public string? EnName { get; set; }
     [Index(25)]
     public string? EnPrefectureName { get; set; }
     [Index(26)]
     public string? EnCityName { get; set; }
     [Index(27)]
     public string? EnAddressOutside { get; set; }
     [Index(28)]
     public string? Furigana { get; set; }
     [Index(29)]
     public int? Hidden { get; set; }
 }
```

## アップロードされたデータを実際にマッピング

下記のコードでアップロードされたCSVファイルをマッピングしました。


実際にファイルをアップロードし、CSVとして読込み、Azure Tableに流し込む部分は下記です。 


```csharp

/// <summary>
/// Asynchronously streams data from a specified blob in Azure Blob Storage, interpreting it as a CSV file without headers,
/// and inserts the data into an Azure Table Storage table in batches. This method is specifically designed to work with CSV files
/// containing Japan corporate numbers, but can be adapted for other types of data.
///
/// The method first establishes connections to both the blob and table storage using provided names and connection strings.
/// It then reads the CSV file in chunks, converting each line into a `CsvJapanCorpNumberRecord` object. These objects are
/// accumulated until they reach a predefined batch size limit, at which point they are inserted into the table storage in a single
/// batch operation. This batch process is repeated until all records in the CSV file have been inserted. If there are records remaining
/// that do not fill the last batch, they are inserted as a final batch to ensure all data from the CSV file is transferred.
///
/// Parameters:
///  - `blobName`: The name of the blob within Azure Blob Storage that contains the CSV data to be streamed.
///  - `tableName`: The name of the Azure Table Storage table where the CSV data will be inserted.
///
/// Note: This method assumes that the CSV file does not have a header record and that each record perfectly matches the structure
/// expected by the `CsvJapanCorpNumberRecord` class. The caller is responsible for ensuring that the blob and table names provided
/// are correct and that the necessary permissions are in place for accessing the storage resources.
/// </summary>
public async Task StreamJpCorpNumberCsvToTableAsync(string blobName, string tableName)
{
    // Initialize a blob client instance using the connection string, container name, and blob name provided.
    var blobClient = new BlobClient(_blobConnectionString, _blobContainerName, blobName);

    // Initialize a table client instance using the connection string and table name provided.
    var tableClient = new TableClient(_tableConnectionString, tableName);

    // Create the table in Azure Table Storage if it does not already exist.
    await tableClient.CreateIfNotExistsAsync();

    // List to hold entities extracted from the CSV file, ready to be inserted into the table.
    var entitiesToInsert = new List<CsvJapanCorpNumberRecord>();

    // Initialize batch size counter to track the number of entities added to the current batch.
    int batchSize = 0;
    // Define the maximum batch size based on Azure Table Storage limitations.
    const int MaxBatchSize = 100;

    // Open the blob for reading.
    await using (var stream = await blobClient.OpenReadAsync())
    using (var reader = new StreamReader(stream, Encoding.UTF8))
    // Initialize a CSV reader with the stream, assuming the CSV does not have a header record.
    using (var csv = new CsvReader(reader, new CsvConfiguration(CultureInfo.InvariantCulture) { HasHeaderRecord = false }))
    {
        // Asynchronously iterate through all records in the CSV file.
        await foreach (var record in csv.GetRecordsAsync<CsvJapanCorpNumberRecord>())
        {
            // Add the current record to the list of entities to insert.
            entitiesToInsert.Add(record);
            batchSize++;

            // If the current batch size reaches the maximum allowed size...
            if (batchSize >= MaxBatchSize)
            {
                // Batch insert the entities into Azure Table Storage and reset for the next batch.
                await BatchInsertJpCorpNumberEntitiesAsync(tableClient, entitiesToInsert);
                entitiesToInsert.Clear(); // Clear the list after insertion.
                batchSize = 0; // Reset batch size counter.
            }
        }
    }

    // After iterating through all records, insert any remaining entities that did not fill the last batch.
    if (entitiesToInsert.Count > 0)
    {
        await BatchInsertJpCorpNumberEntitiesAsync(tableClient, entitiesToInsert);
    }
}
```

これで法人データがすべて自社のDBに挿入することができました。

この後、`Azure Table`をデータソースとして`Azure Search Index`に同期をしています。また、その部分はリクエストや時間があり次第記事にしてみます。


# 差分データを取り込んでメンテナンスする

一度取り込んでも毎日会社が新たに登記されたり、変更されたりしています。なので、これらの変更も記録していく必要があります。

## 命名ルール

差分ファイルに関しては、下記のフォーマットでデータzipファイルをダウロードすることができます。

`diff_作成年月日〔_一連番号〕.zip`

解凍後のファイル名は下記通りです

`diff_作成年月日〔_一連番号〕.csv`

今回はzipファイルが置かれているURLをたたき、ダウンロード、解凍して指定の`Azure Storage (Blob)`上に保存、そこで保存されたファイルを処理しています。

## 差分処理

新規の際はデータを入れるだけだったので、シンプルでしたが、今回は差分ファイルとのことで、指定した項目を見て判断する必要があります。また、変更の履歴は追いかけたいので、変更があったさいは必要箇所を記録します。

なので、「処理区分」を見て判断します。下記が判断と記録部分のコードです。

```csharp

 switch (csvRecord.ProcessNumber)
 {
     case "11":
         // Change of name

         var previousName = original.Value.Name;
         var newName = csvRecord.Name;

         await _operationStorageLogService.UpsertEntityAsync(operationLogTableName, new OperationLogModel
         {
             PartitionKey = orgModel.PartitionKey,
             RowKey = Guid.NewGuid().ToString(),
             Action = "Update",
             PublicMemo = "Name change (#11) from " + previousName + " to " + newName + " by SYNCDIFF",
             PrivateMemo = "システムに(SYNCDIFF #11)よって名前が「" + previousName + "」から「" + newName + "」へ変更されました。"
         });

         break;
     case "12":

         // Change of address (domestic)
         var previousAddreess = original.Value.PostalCode + " " + original.Value.AddressRegion + " " + original.Value.AddressLocality + " " + original.Value.StreetAddress;
         var newAddress = csvRecord.PostalCode + " " + csvRecord.PrefectureName + " " + csvRecord.CityName + " " + csvRecord.StreetName;

         await _operationStorageLogService.UpsertEntityAsync(operationLogTableName, new OperationLogModel
         {
             PartitionKey = orgModel.PartitionKey,
             RowKey = Guid.NewGuid().ToString(),
             Action = "Update",
             PublicMemo = "Address change (#12) from " + previousAddreess + " to " + newAddress + " by SYNCDIFF",
             PrivateMemo = "システムに(SYNCDIFF #12)よって住所が「" + previousAddreess + "」から「" + newAddress + "」へ変更されました。"
         });

         break;

     case "13":

         // Change of address (domestic)
         var previousAddreessOversea = original.Value.AddressOutside;
         var newAddressOversea = csvRecord.AddressOutside;

         await _operationStorageLogService.UpsertEntityAsync(operationLogTableName, new OperationLogModel
         {
             PartitionKey = orgModel.PartitionKey,
             RowKey = Guid.NewGuid().ToString(),
             Action = "Update",
             PublicMemo = "Address change (#13) from " + previousAddreessOversea + " to " + newAddressOversea + " by SYNCDIFF",
             PrivateMemo = "システムに(SYNCDIFF #13)よって住所(国外)が「" + previousAddreessOversea + "」から「" + newAddressOversea + "」へ変更されました。"
         });

         break;

     case "21":

         // Closure
         await _operationStorageLogService.UpsertEntityAsync(operationLogTableName, new OperationLogModel
         {
             PartitionKey = orgModel.PartitionKey,
             RowKey = Guid.NewGuid().ToString(),
             Action = "Update",
             PublicMemo = "Closure (#21) by SYNCDIFF",
             PrivateMemo = "システムに(SYNCDIFF #21)よってステータスが「閉鎖」に変更されました。"
         });

         break;

     case "22":

         // Closure voided
         await _operationStorageLogService.UpsertEntityAsync(operationLogTableName, new OperationLogModel
         {
             PartitionKey = orgModel.PartitionKey,
             RowKey = Guid.NewGuid().ToString(),
             Action = "Update",
             PublicMemo = "Closure Voided (#22) by SYNCDIFF",
             PrivateMemo = "システムに(SYNCDIFF #22)よってステータスが「閉鎖」が解除されました。"
         });

         break;

     case "71":

         // Merger
         await _operationStorageLogService.UpsertEntityAsync(operationLogTableName, new OperationLogModel
         {
             PartitionKey = orgModel.PartitionKey,
             RowKey = Guid.NewGuid().ToString(),
             Action = "Update",
             PublicMemo = "Merger (#71) by SYNCDIFF",
             PrivateMemo = "システムに(SYNCDIFF #71)よってステータスが「合併」に変更されました。"
         });

         break;
     case "72":

         // Merger voided
         await _operationStorageLogService.UpsertEntityAsync(operationLogTableName, new OperationLogModel
         {
             PartitionKey = orgModel.PartitionKey,
             RowKey = Guid.NewGuid().ToString(),
             Action = "Update",
             PublicMemo = "Merger Voided (#72) by SYNCDIFF",
             PrivateMemo = "システムに(SYNCDIFF #72)よってステータスが「合併」が解除されました。"
         });

         break;
     case "81":

         // Record voided
         await _operationStorageLogService.UpsertEntityAsync(operationLogTableName, new OperationLogModel
         {
             PartitionKey = orgModel.PartitionKey,
             RowKey = Guid.NewGuid().ToString(),
             Action = "Update",
             PublicMemo = "Record Voided (#81) by SYNCDIFF",
             PrivateMemo = "システムに(SYNCDIFF #81)よってステータスが「登記の抹消」に変更されました。"
         });

         break;
     case "99":

         // Record deleted
         await _operationStorageLogService.UpsertEntityAsync(operationLogTableName, new OperationLogModel
         {
             PartitionKey = orgModel.PartitionKey,
             RowKey = Guid.NewGuid().ToString(),
             Action = "Update",
             PublicMemo = "Record Deleted (#99) by SYNCDIFF",
             PrivateMemo = "システムに(SYNCDIFF #81)よって削除されました。"
         });

         break;
 }
```

### 01 新規

新規の場合は単純に追加します。なので、ここでは何も処理していません。

### 11 商号又は名称の変更

現在の「商号又は名称」を記録し、データを更新します。

### 12 国内所在地の変更

現在の「国内所在地」を記録し、データを更新します。

### 13 国外所在地の変更

現在の「国外所在地」を記録し、データを更新します。

### 21 登記記録の閉鎖等

「登記記録の閉鎖」を記録する

### 21 登記記録の復活等

「登記記録の復活等」を記録する

### 71 吸収合併

「吸収合併」を記録する

### 72 吸収合併無効

「吸収合併無効」を記録する

### 81 商号の登記の抹消

「商号の登記の抹消」を記録する

### 99 削除

削除フラグを設定し、「削除」を記録する

# 完成品

完成品はこちらで利用いただけます。

https://pde.receiptroller.com/ja/search/organizations


# 仲間募集中

エンジニアの方に限らずですが、仲間募集中です。下記コーポレートページです。

https://receiptroller.com/ja/aboutus