SharePoint Online の Excel を C#(.NET Framework 4.8)でURL指定して読み込む方法|Microsoft Graph APIでダウンロードしてDataTable化

SharePoint Online のドキュメントライブラリにある Excel(.xlsx)を、C#(.NET Framework 4.8)から「URL指定で読み込みたい」――しかし Web のURLをそのまま渡しても失敗することが多いです。認証を含めた取得方法と、DataTable化までの実装パターンを、現場でつまずきやすいポイント込みで整理します。

目次

SharePoint Online の「Web のURL」をそのまま Excel として読めない理由

SharePoint Online でファイルを開いたときに表示される “Web のURL” は、たいてい次のような性質を持っています。

  • ブラウザでの閲覧・編集(Office for the web)を前提にした URL である
  • サインイン(Cookie / セッション / 条件付きアクセス / MFA など)を前提としている
  • アクセスすると 302 リダイレクトや認証ページへ遷移することがある
  • Excel ファイル本体(.xlsx バイナリ)が素直に返ってくるとは限らない

そのため、C# 側で WebClient や HttpClient に URL を渡しても、返ってくるのが HTML(ログイン画面)だったり、権限エラー(401/403)だったりして、Excel ライブラリが「壊れたファイル」として扱ってしまいます。

URL の種類例プログラムでの扱いつまずきポイント
Web 表示用 URLhttps://tenant.sharepoint.com/sites/xxx/Shared%20Documents/MyDataFile.xlsxそのままでは Excel として読めないことが多い認証・リダイレクト・Office Web 画面に流れる
共有リンク(ダウンロード可能な場合)https://tenant.sharepoint.com/:x:/r/sites/...設定次第で読めるが組織では無効なことが多い匿名不可 / 有効期限 / ポリシー制約
Graph の content エンドポイント.../drive/root:/...:/contentExcel バイナリが取得しやすい(推奨)Entra ID アプリ登録と権限付与が必須

解決の基本方針:取得(認証付き)と解析(Excel読み取り)を分離する

実装を成功させるコツは、処理をきれいに 2 段階に分けることです。

(段階1)SharePoint から認証付きで .xlsx を取得(ダウンロード/ストリーム)
(段階2)取得した .xlsx を Excel 解析ライブラリで読み、DataTable / List に変換

この分離をすると、次のメリットが出ます。

  • 認証・権限エラー(401/403/404)と、Excel パースエラー(シート名違い・ヘッダー不整合)を切り分けやすい
  • ダウンロードしたファイルを一時保存して検証できる(Excel で手開きして確認可能)
  • 後で「ファイルは OneDrive だった」「保存先が別サイトになった」など構造変更があっても、取得部分だけ差し替えられる

3つの選択肢を比較:Graph / CSOM / 直URL

方式認証MFA/条件付きアクセス将来性実装難易度向いているケース
Microsoft Graph API(推奨)モダン(Entra ID / OAuth2)対応しやすい高い中企業標準・長期運用・サービス連携
SharePoint CSOM / REST方式により差が大きい環境によって厳しい中中既存資産が CSOM ベース、限定環境
共有リンク等での直取得不要(匿名)または簡易ポリシー次第低〜中低匿名ダウンロードが許可されている特殊ケース

多くの企業テナントでは「匿名共有」や「認証なしダウンロード」が抑止されているため、現実的には Microsoft Graph API を第一候補にするのが安定です。

推奨:Microsoft Graph API で SharePoint Online の Excel(.xlsx)を取得する

全体像:コンソール(.NET Framework 4.8)での典型構成

  • Entra ID(旧 Azure AD)でアプリ登録
  • MSAL(Microsoft.Identity.Client)でアクセストークン取得
  • Graph API でファイルをダウンロード(Stream)
  • ClosedXML / EPPlus / NPOI / Open XML SDK / ExcelDataReader などで読み取り

.NET Framework 4.8 のコンソールアプリで「ユーザーとしてサインインして読む(Delegated)」のが一番導入しやすいことが多いです。一方、夜間バッチなど “無人実行” が必要なら「アプリのみ(Application / app-only)」を検討します。

どの認証方式にするべきか(Delegated と app-only)

方式概要メリット注意点
Delegated(ユーザー認証)実行ユーザーがサインインして、その権限で読む現場導入が早い/権限が利用者に沿う無人実行に不向き/ユーザー操作が必要になりがち
Application(app-only)アプリが単独でアクセス(証明書/シークレット等)バッチ向き/運用が安定管理者同意が必要/権限が強くなりやすいので最小権限設計が重要

アプリ登録(Entra ID)で準備するもの

最低限、次の情報が必要になります。

  • Tenant ID(ディレクトリ ID)
  • Client ID(アプリケーション ID)
  • (Delegated の場合)リダイレクト URI や Public client 設定
  • (app-only の場合)クライアントシークレット or 証明書
  • Microsoft Graph のアクセス許可(例:Sites.Read.All, Files.Read.All など)

権限は「動けばよい」で強いものを付けると後で監査・棚卸しで詰みがちです。可能なら対象サイトに限定する(例:Sites.Selected を使う)など、最小権限の方向で設計すると運用が楽になります。

.NET Framework 4.8 側の NuGet(代表例)

  • Microsoft.Identity.Client(MSAL)
  • ClosedXML(Excel を扱いやすい形で読み取る)
  • (代替)ExcelDataReader / ExcelDataReader.DataSet(DataSet/DataTable に寄せたい場合)

通信が失敗する環境では TLS の影響が出ることがあります。古い設定が残る現場では、起動時に TLS1.2 を明示しておくと切り分けに役立ちます(ただし OS/環境依存なので必要な場合のみ)。

// 必要なときだけ(環境により)
System.Net.ServicePointManager.SecurityProtocol |= System.Net.SecurityProtocolType.Tls12;

Graph API で「SharePoint のファイル本体(.xlsx)」をダウンロードする

Graph のパス指定は「ドライブ(Drive)とアイテム(DriveItem)」が基本

SharePoint のドキュメントライブラリ上のファイルは、Graph 的には “Drive(ドキュメントライブラリ)” の中の “DriveItem(ファイル)” です。よく使うのは次の 2 パターンです。

  • サイトの既定ドライブを使って、パスで指定して content を取る
  • drive-id / item-id を先に取得してから content を取る(確実だが手順が増える)

よく使う content エンドポイント例(パス指定)

次の形が読みやすく、検証もしやすいです(実際は URL エンコードが必要になります)。

GET https://graph.microsoft.com/v1.0/sites/{site-id}/drive/root:/{ライブラリ以降のパス}:/content

たとえばドキュメントライブラリが “Shared Documents” で、そこに MyDataFile.xlsx があるなら概念的にはこうなります。

/drive/root:/Shared Documents/MyDataFile.xlsx:/content

日本語サイトではライブラリ表示名が「ドキュメント」でも、内部的なパスが異なる場合があります。確実にするには、Graph Explorer 等で対象ファイルの DriveItem を確認してからパスを固めるのが安全です。

最小構成コード例:MSAL でトークン取得 → Graph でダウンロード → Sheet1 を DataTable 化(ClosedXML)

ここでは「コンソールアプリでユーザーとしてサインインし、ユーザー権限でファイルを読む(Delegated)」の例を示します。MFA がある環境でも通しやすいのが利点です。コンソールでは Device Code フローが扱いやすいことが多いです。

設定値(例)

  • tenantId:あなたのテナント ID
  • clientId:アプリ(クライアント)ID
  • siteId:対象サイトの site-id(後述の方法で取得)
  • filePath:ドライブの root からのパス(例:Shared Documents/MyDataFile.xlsx)

サンプル(Device Code でログインして取得)

using System;
using System.Data;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using ClosedXML.Excel;
using Microsoft.Identity.Client;

namespace SpoExcelReader
{
internal class Program
{
static void Main(string[] args)
{
MainAsync(args).GetAwaiter().GetResult();
}


    static async Task MainAsync(string[] args)
    {
        // ====== 設定 ======
        var tenantId = "YOUR_TENANT_ID";
        var clientId = "YOUR_CLIENT_ID";
        var siteId   = "YOUR_SITE_ID"; // 例: tenant.sharepoint.com,xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx,yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy

        // SharePoint のドキュメントライブラリ配下のパス(URLエンコードは HttpClient 側で任せず、自分で安全に)
        var filePath = "Shared Documents/MyDataFile.xlsx";

        // Delegated 権限(例)
        // 実運用では最小権限に寄せる:読み取りなら Files.Read / Sites.Read.All など要件に合わせる
        string[] scopes = new[] { "Files.Read.All", "Sites.Read.All" };

        // ====== MSAL(Device Code)でトークン取得 ======
        var app = PublicClientApplicationBuilder
            .Create(clientId)
            .WithAuthority(AzureCloudInstance.AzurePublic, tenantId)
            .WithDefaultRedirectUri()
            .Build();

        var authResult = await app
            .AcquireTokenWithDeviceCode(scopes, code =>
            {
                Console.WriteLine(code.Message);
                return Task.CompletedTask;
            })
            .ExecuteAsync();

        // ====== Graph でファイルをダウンロード(Stream) ======
        using var http = new HttpClient();
        http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);

        // パスはエンコードして安全に
        var encodedPath = string.Join("/", filePath.Split('/')
            .Select(seg => Uri.EscapeDataString(seg)));

        var url = $"https://graph.microsoft.com/v1.0/sites/{siteId}/drive/root:/{encodedPath}:/content";

        using var resp = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
        resp.EnsureSuccessStatusCode();

        using var xlsxStream = await resp.Content.ReadAsStreamAsync();

        // ====== ClosedXML で Sheet1 を DataTable 化 ======
        var dt = ReadWorksheetToDataTable(xlsxStream, "Sheet1", firstRowAsHeader: true);

        Console.WriteLine($"Rows: {dt.Rows.Count}, Cols: {dt.Columns.Count}");
        Console.WriteLine("Done.");
    }

    static DataTable ReadWorksheetToDataTable(Stream xlsxStream, string sheetName, bool firstRowAsHeader)
    {
        // 重要:ClosedXML はシーク不可の Stream を嫌う場合があるため、必要なら MemoryStream にコピーする
        using var ms = new MemoryStream();
        xlsxStream.CopyTo(ms);
        ms.Position = 0;

        using var wb = new XLWorkbook(ms);
        var ws = wb.Worksheets.FirstOrDefault(x => string.Equals(x.Name, sheetName, StringComparison.OrdinalIgnoreCase));
        if (ws == null)
            throw new InvalidOperationException($"Worksheet not found: {sheetName}");

        var used = ws.RangeUsed();
        var dt = new DataTable(sheetName);

        if (used == null)
            return dt; // 空シート

        var firstRow = used.FirstRowUsed();
        var firstDataRowNumber = firstRow.RowNumber();

        // 列定義
        var headerRow = firstRow;
        var colCount = used.ColumnCount();

        for (int c = 1; c <= colCount; c++)
        {
            string colName;

            if (firstRowAsHeader)
            {
                colName = headerRow.Cell(c).GetValue<string>();
                if (string.IsNullOrWhiteSpace(colName))
                    colName = $"Column{c}";
            }
            else
            {
                colName = $"Column{c}";
            }

            // 同名列対策
            var original = colName;
            int suffix = 1;
            while (dt.Columns.Contains(colName))
            {
                suffix++;
                colName = $"{original}_{suffix}";
            }

            dt.Columns.Add(colName, typeof(string)); // まずは string で取り込むのが安全
        }

        // データ行
        int startRow = firstRowAsHeader ? firstDataRowNumber + 1 : firstDataRowNumber;

        for (int r = startRow; r <= used.LastRowUsed().RowNumber(); r++)
        {
            var row = ws.Row(r);

            // 行が全空ならスキップ(必要に応じて調整)
            bool allEmpty = true;
            var values = new object[colCount];

            for (int c = 1; c <= colCount; c++)
            {
                var cell = row.Cell(used.FirstColumn().ColumnNumber() + (c - 1));
                string v = cell.GetFormattedString(); // 表示形式で取得(数値/日付の見え方を揃える)
                values[c - 1] = v;

                if (!string.IsNullOrWhiteSpace(v))
                    allEmpty = false;
            }

            if (allEmpty) continue;

            dt.Rows.Add(values);
        }

        return dt;
    }
}


}

ポイントは次の通りです。

  • まずは string で DataTable に入れる:Excel の型推定は地雷になりがちなので、後段で必要な列だけ変換する方が安定します。
  • GetFormattedString() を使う:日付や小数点など、Excel の表示に近い形で取れます(要件により GetValue<T> に切り替え)。
  • 空行スキップ:Excel にありがちな “見た目は空だが範囲に含まれる” 行で行数が膨れないようにします。

site-id をどうやって手に入れるか(よくあるつまずき)

Graph の URL を組むには site-id が必要になることが多いです。取得方法はいくつかありますが、実務で使いやすい考え方をまとめます。

方法:hostname と site のパスから site を引く

サイトURLが次のような場合:

https://tenant.sharepoint.com/sites/YourSite

概念的には Graph で次の問い合わせを行い、返ってきた id を使います。

GET https://graph.microsoft.com/v1.0/sites/tenant.sharepoint.com:/sites/YourSite

この応答の id が、サンプルコードで使っている siteId です。開発時は Graph Explorer で試すと早いです。

方法:Drive から辿って item-id を確定させる(パスの揺れが怖い場合)

ライブラリ名が日本語だったり、スペース・記号が混ざるパスでトラブルが多い場合は、先に一覧 API で対象フォルダを辿り、最終的に driveItem-id を取ってから /items/{id}/content で落とすと確実です(手順は増えますが安定します)。

Excel 解析ライブラリの選び方(ClosedXML / ExcelDataReader など)

「DataTable にしたい」のか「ドメインモデル(List<T>)にしたい」のかで、向き不向きがあります。

ライブラリ得意注意点向くケース
ClosedXMLExcel を直感的に扱える/シート指定が簡単大容量だとメモリを使いやすい中規模までの帳票、扱いやすさ優先
ExcelDataReader読み取りが軽い/DataSet 化が簡単Encoding.RegisterProvider が必要なことがある読み取り特化、DataTable に寄せたい
EPPlus多機能(生成も強い)ライセンスや利用条件の確認が必要な場合がある社内規約・ライセンス確認が取れる環境
Open XML SDK低レベルで確実/依存が少ない実装が重くなりやすいライブラリ制約が厳しい、最小依存

ExcelDataReader で “そのまま DataTable” に寄せる例

「とにかく DataTable を早く作りたい」場合はこの系が便利です。

using System.Data;
using System.IO;
using ExcelDataReader;

// ExcelDataReader は .NET Framework だとエンコーディング登録が必要なことがある
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);

using var reader = ExcelReaderFactory.CreateReader(xlsxStream);
var ds = reader.AsDataSet(new ExcelDataReader.ExcelDataSetConfiguration
{
    ConfigureDataTable = _ =&gt; new ExcelDataReader.ExcelDataTableConfiguration
    {
        UseHeaderRow = true
    }
});

DataTable sheet1 = ds.Tables["Sheet1"]; // シート名で取得(見つからない場合は index)

SharePoint CSOM / REST で取得する場合(環境により有効)

Graph を採用できない事情(既存の CSOM 資産がある、ネットワーク制約がある等)がある場合、SharePoint CSOM(Client-Side Object Model)でファイルを取得し、その後に Excel 解析へ回す手もあります。

ただし、SharePoint Online では認証周り(MFA 必須、条件付きアクセス、基本認証の廃止)で詰まりやすいのが現実です。特に「ユーザー名+パスワードをコードに埋め込んで接続」のようなやり方は、セキュリティ上も運用上も避けるべきです。

CSOM を使うなら、以下の観点で “環境の認証方式と噛み合うか” を先に確認するのが安全です。

  • MFA が必須か(必須なら従来型の資格情報方式はほぼ詰む)
  • アプリ専用の認証(証明書ベース等)を許容できるか
  • 社内標準が Graph なのか、SharePoint REST/CSOM が許可されているのか

共有リンクで「匿名ダウンロード可能」な場合だけ成立する直取得

もし組織の設定として、共有リンク(“誰でもリンク” など)が許可され、かつ “ダウンロードが認証なしで可能” であれば、URL を直にダウンロードして Excel 解析に渡せるケースがあります。

ただし企業テナントでは次の理由で成立しないことが多いです。

  • 匿名共有が無効
  • リンクに有効期限がある/失効しやすい
  • 監査上、匿名リンク運用が禁止

「一時的に動いたから採用する」と、後からリンク失効で障害になりがちです。長期運用の前提なら Graph(または統制された認証方式)をおすすめします。

DataTable / List 化で失敗しやすいポイントと対策

ヘッダー行の取り扱い

  • 1行目が必ずヘッダーとは限らない(タイトル行や注意書き行が混ざる帳票がある)
  • 空の列名があると DataTable が扱いにくい(Column1 補完などが必要)
  • 同名列があると例外になりやすい(サンプルのように _2 などで回避)

型変換は “後段で必要な列だけ” が安全

Excel のセルは見た目と内部値が一致しないことがあります(たとえば日付に見えて実は文字列、数値に見えて実は文字列など)。取り込み時に強制変換すると、1セルの異常で全体が落ちることがあります。

要件おすすめ戦略理由
まず一覧を取得して後処理で整形取り込みは string 統一 → 後段で列ごとに parse例外耐性が高い/障害調査がしやすい
数値・日付が厳密に必要列ごとに TryParse、失敗はログ+既定値“1件の不正値で全停止” を避ける
Excel の表示値が重要表示形式(Formatted)で取得小数点桁や日付表現が揃う

シート名が固定でない場合の現実的な対処

帳票のテンプレが変わると、Sheet1 が sheet1 になったり、入力 など別名になったりします。安定運用したい場合は次のどれかを採用します。

  • シート名を設定ファイル化して変更に追従できるようにする
  • 先頭シート(index 0)を読む(テンプレが固定なら有効)
  • ヘッダー行の特徴(列名セット)でシートを判定する

Graph でのエラー原因の切り分け(401/403/404 が出たら)

401 Unauthorized

  • トークンが取れていない / 期限切れ
  • スコープ(権限)が不足している
  • テナントや Authority の指定が間違っている

403 Forbidden

  • ユーザー(Delegated)の権限で対象ファイルにアクセスできない
  • 管理者同意が必要な権限を付与していない
  • app-only の場合、サイトへの許可付与設計が不足(最小権限の設定ミス含む)

404 Not Found

  • site-id / パスが違う(ライブラリ名や日本語パス、スペースのエンコード漏れ)
  • ファイル移動・リネームが起きている
  • 別サイト(別コレクション)を見ている

この段階では「URL が間違っているのか」「権限がないのか」の切り分けが重要です。Graph Explorer などで同じユーザーで叩いて再現する、または一度 item-id を取得してから content を取ると、原因を特定しやすくなります。

運用で差がつく実装の工夫(オリジナルの実務ポイント)

ETag を使って “変更があった時だけ” ダウンロードする

SharePoint 上の Excel を定期的に取り込む場合、毎回フルダウンロードすると無駄が増えます。Graph は DriveItem のメタデータに ETag が付くので、前回の ETag を保持し、変化がある時だけダウンロードする設計にすると負荷と時間を減らせます。

ファイル取得と Excel 解析を別ログにする

「取得は成功したが解析で失敗」「解析は成功したがデータが空」など、障害の種類が分かれます。ログを次のように分けると、現場での復旧が速くなります。

  • 取得ログ:サイト/パス、HTTP ステータス、レスポンスサイズ、ETag、所要時間
  • 解析ログ:シート名、行数/列数、ヘッダー内容、必須列の有無、型変換エラー件数

Excel 側の “運用ルール” を最初に決めると安定する

技術的に読めるようにしても、Excel 側の運用が揺れると取り込みは壊れます。取り込み対象の Excel は、次のような最低限のルールを合意しておくと強いです。

  • データ範囲のヘッダーは固定(列名を勝手に変えない)
  • 空行・空列の混入ルール(許容するならスキップ条件を定義)
  • 日付・数値の入力規則(文字列混入時の扱い)
  • シート名の固定(または設定で追従できるようにする)

まとめ:SharePoint Online の Excel を C# で URL 指定で読むなら「Graph で取得 → Excel ライブラリで解析」

  • SharePoint の “Web のURL” は、そのまま Excel バイナリの URL ではないことが多い
  • まずは認証付きでファイル本体(.xlsx)を取得する仕組みを確立する
  • Microsoft Graph API はモダン認証で将来性が高く、SharePoint Online では第一候補になりやすい
  • 取得した Stream を ClosedXML / ExcelDataReader などで読み、DataTable / List に変換する
  • 運用では権限設計(最小権限)、パス揺れ対策、ログとエラー切り分けが重要

「ユーザーとして実行するのか(Delegated)」「完全無人で動かすのか(app-only)」「MFA や条件付きアクセスがあるか」で最適解は変わりますが、基本のアーキテクチャを押さえれば、SharePoint Online の Excel 取り込みは安定して作れます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次