5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【ServiceNow】Knowledge26のコンテンツをCSVで一覧化する方法

5
Last updated at Posted at 2026-03-01

Knowledge26のコンテンツ一覧CSVをAIで自動作成した話

こんにちは、にわと(𝕏:@niwaniwa2wato)です。

このたび、Knowledge26 CreatorCon レポーター派遣プログラムに任命されました!
2026年5月5日〜7日にラスベガスで開催されるServiceNow最大の年次カンファレンスKnowledge26に、レポーターとして参加します。

コンテンツの紹介記事はServiceNow communityに掲載しています。こちらも是非、ご確認ください。

合格を知ってまず考えたのは、「3日間で約400のコンテンツの中からどう選ぶか」ということでした。せっかく現地に行くのだから、事前にコンテンツ情報を整理して、私が最大限に学びを持ち帰れる計画を立てたい。

さて、公式サイト眺めますが、数が多く1つ1つクリックして内容を読んでいくのは、大変です。

せめて一覧化して見てみたいと思いました。

そこで、この記事では、コンテンツ情報をCSVで生成した話を紹介します。

公式サイトだと網羅的に見づらい問題

Knowledge26の公式サイトにはコンテンツが掲載されています。ただ、約400を超えるコンテンツをWebのUIで一つずつ確認していくのはなかなか骨が折れますし、AIにポンポン情報を投げるの面倒です。

  • フィルタリングはできるものの、一覧性が低い
  • 複数のコンテンツを横断的に比較しづらい
  • キーワードで一括検索して絞り込みたいけど、引っかからないこともある

「これは自分でデータを持った方が早いな」と思い、コンテンツ一覧をCSVにまとめることにしました。

コンテンツ一覧CSVを作るための情報を手に入れる

公式サイトからコンテンツ情報を取得しなければ始まらない。
手っ取り早く、情報を入手するためにはコピペが一番楽でした。

image-2.png

画像のようにコンテンツは上から下に表示されていくので、コピペが楽そうです。

  • 画面をスクロールして、コンテンツを最後まで表示するために「Show more」をクリック
  • 各コンテンツの「show more」をクリックしt...
    image-7.png

ん!? 400近いコンテンツを全部クリックするのは面倒じゃね?

というわけで、開発者ツールからいじりましょう。以下のスクリプトを開発者ツールのコンソールから実行する

// すべての「Show more」を自動クリックして展開する
document.querySelectorAll('button, a, span, div').forEach(el => {
  if (el.textContent.trim().toLowerCase().includes('show more') || 
      el.textContent.trim() === '続きを読む') {
    el.click();
  }
});
console.log('すべてのShow moreを展開しました');

完了テキストが表示されば、すべての「Show more」がクリックされて表示されます。

image-3.png

そうしたら、コピペですべてのコンテンツ情報を取得できます。

  1. コンテンツ情報を上から下までコピペする
  2. Google Docsに張り付けて保存
  3. マークダウン形式でダウンロード

CSVを作成する - AI編

Antigravityに任せまることで、良い感じに作成することもできます。

2026年3月1日時点の件数であることに注意してください。
コンテンツは随時追加されるため、現在読んでいるタイミングと、記事の数値が異なります。
Promptの数値も現在のコンテンツの数値に合わせてください。

添付したmdファイルから、コンテンツのカテゴリ、タイトル、説明文、URLで整理して、すべてまとめたCSVファイルを作成してください。コンテンツは389件あるので、作成したCSVに不足がないか確認もしてください。

全部で389コンテンツ。CSVにしてしまえば、Excelやスプレッドシートで自由にフィルタリング・検索、AIへのインプットにも使えますね。

CSVを作成する - スクリプト編

CSVを作成するパーサースクリプトを公開しておきます。
Node.js(v14以上)があれば外部ライブラリ不要でそのまま動きます。

スクリプト冒頭の INPUT_FILEOUTPUT_FILE のパスを自分の環境に書き換えて 下記スクリプトを実行します。

【使い方】

  1. 「ユーザー設定」セクションのパスを自分の環境に合わせて書き換えて保存する
    例:parse_k26_sessions.mjs
  2. ターミナルで保存したmjsファイルがあるディレクトリに移動する
    3.実行コマンドを叩く
    例:node parse_k26_sessions.mjs
  3. OUTPUT_FILE に指定したパスに CSV が生成される

【動作環境】

  • Node.js v14 以上(外部ライブラリのインストールは不要)

【出力形式】

  • 列構成: Category, Title, URL, Description
  • エンコーディング: UTF-8 BOM 付き(Excel で開いても文字化けしない)
import { readFileSync, writeFileSync } from 'fs';

// ============================================================
// ユーザー設定(★ここを自分の環境に合わせて変更してください)
// ============================================================

/**
 * INPUT_FILE: 変換元の Markdown ファイルのパス
 *   例(Windows): String.raw`C:\Users\yourname\Downloads\K26 Session.md`
 *   例(Mac/Linux): '/Users/yourname/Downloads/K26 Session.md'
 */
const INPUT_FILE = String.raw`C:\Users\yourname\Downloads\K26_Session.md`;

/**
 * OUTPUT_FILE: 出力先の CSV ファイルのパス
 *   存在しない場合は自動的に新規作成される。既存ファイルは上書きされるので注意。
 *   例(Windows): String.raw`C:\Users\yourname\Downloads\K26_Sessions.csv`
 *   例(Mac/Linux): '/Users/yourname/Downloads/K26_Sessions.csv'
 */
const OUTPUT_FILE = String.raw`C:\Users\yourname\Downloads\K26_Sessions.csv`;

// ============================================================
// 定数定義(通常は変更不要)
// ============================================================

/**
 * CATEGORIES: カテゴリ行として認識するキーワードの一覧
 *   Markdown 上では「* Session」「* Hands-on Lab」のように
 *   アスタリスク+スペース+カテゴリ名 の形式で記載されている。
 *   ※ 部分一致を防ぐため、長い文字列を先頭に置くこと。
 *   Knowledge26 以外のイベントに流用する場合はこのリストを書き換える。
 */
const CATEGORIES = [
    'Preconference Training', // 事前トレーニング(他カテゴリと組み合わせて使われる)
    'Hands-on Lab',           // ハンズオンラボ
    'Session',                // 通常セッション
    'Roadmap',                // ロードマップセッション
    'Keynote',                // キーノート
    'Panel',                  // パネルディスカッション
    'Roundtable',             // ラウンドテーブル
    'Spotlight',              // スポットライト
];

/**
 * TITLE_RE: セッションタイトル行を検出する正規表現
 *   Markdown 上のタイトル行: * [**セッションタイトル**](https://...)
 *   $1 = タイトルテキスト、$2 = セッション詳細ページの URL
 */
const TITLE_RE = /\[\*\*(.+?)\*\*\]\((https?:\/\/.+?)\)/;

// 登壇者紹介行(説明文として取り込まないようスキップする)
const FEATURING_RE = /\*\*Featuring:\*\*/;

/**
 * PRECONF_RE: Preconference Training の受講料案内行を検出する正規表現
 *   この行に含まれる "[**Register Now**](URL)" がタイトル行正規表現にマッチするため、
 *   タイトル判定より先にスキップする必要がある。
 */
const PRECONF_RE = /\*\*ServiceNow University preconference:\*\*/;

/**
 * SKIP_TAGS: 説明文に混入するWebページのタグ行をスキップするための文字列セット
 *   公式サイトのタグラベルが「* ServiceNow University」のような形式で
 *   Markdown に出力されることがあるため、除外リストで対処する。
 *   同様の問題が発生した場合はこのセットにタグ名を追加する。
 */
const SKIP_TAGS = new Set([
    'ServiceNow University',
    'Platform Owner',
    'Session Tile',
    'Builder / Citizen Developer',
    'AI Agents',
    'Solution & Industry Keynotes',
]);

// ============================================================
// ユーティリティ関数
// ============================================================

/**
 * matchCategory(line): カテゴリ行かどうかを判定し、カテゴリ名 or null を返す
 */
function matchCategory(line) {
    for (const cat of CATEGORIES) {
        const re = new RegExp(`^\\*\\s+${cat.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`);
        if (re.test(line)) return cat;
    }
    return null;
}

/**
 * csvEscape(val): RFC 4180 準拠の CSV エスケープ
 *   カンマ・改行・ダブルクォートを含む値はダブルクォートで囲み、
 *   値内のダブルクォートは "" にエスケープする。
 */
function csvEscape(val) {
    if (val.includes('"') || val.includes(',') || val.includes('\n') || val.includes('\r')) {
        return '"' + val.replace(/"/g, '""') + '"';
    }
    return `"${val}"`;
}

// ============================================================
// メインのパース処理
// ============================================================

/**
 * parseSessions(text): Markdown テキストを解析してセッション情報の配列を返す
 *   各セッションは { Category, Title, URL, Description } のオブジェクト。
 *
 *   【処理の流れ】
 *   1 行ずつ走査しながら以下を識別する:
 *     A) カテゴリ行  → 直後のセッション群に適用するカテゴリを更新する
 *     B) タイトル行  → タイトルと URL を取得し、続く行から説明文を収集する
 *     C) その他の行  → 説明文収集中はバッファに追加、それ以外は無視する
 */
function parseSessions(text) {
    const lines = text.split('\n');
    const sessions = [];
    let currentCategory = null;
    let preconfPrefix = false; // 直前に "Preconference Training" カテゴリ行があったかどうか
    let i = 0;

    while (i < lines.length) {
        const line = lines[i].trim();

        if (!line) { i++; continue; }

        // A) カテゴリ行の処理
        const cat = matchCategory(line);
        if (cat) {
            if (cat === 'Preconference Training') {
                preconfPrefix = true;
            } else {
                // "Preconference Training" 直後なら "Preconference Training / Session" のように結合する
                currentCategory = preconfPrefix ? `Preconference Training / ${cat}` : cat;
                preconfPrefix = false;
            }
            i++;
            continue;
        }

        // B) タイトル行の処理: "* [**タイトル**](URL)" の形式
        const titleMatch = line.match(TITLE_RE);
        if (titleMatch && line.startsWith('*')) {
            const title = titleMatch[1];
            const url   = titleMatch[2];
            const descParts = [];
            i++;

            // タイトル行の次の行から説明文を収集するループ
            while (i < lines.length) {
                const next = lines[i].trim();

                if (!next) { i++; continue; }

                // 次のカテゴリ行が来たら説明文収集を終了
                if (matchCategory(next)) break;

                // "**Featuring:**" 行はスキップ
                if (FEATURING_RE.test(next)) { i++; continue; }

                // Preconference 案内行はスキップ
                // ※ タイトル行正規表現より先にチェックする必要がある
                if (PRECONF_RE.test(next)) { i++; continue; }

                // Webページのタグ行はスキップ
                const tagCandidate = next.replace(/^\*\s+/, '').replace(/\s+$/, '');
                if (next.startsWith('*') && SKIP_TAGS.has(tagCandidate)) { i++; continue; }

                // 次のセッションのタイトル行が来たら終了
                if (next.startsWith('*') && TITLE_RE.test(next)) break;

                // 説明文として取り込む(行頭の "* " をトリム)
                let desc = next;
                if (desc.startsWith('* ')) desc = desc.slice(2);
                else if (desc.startsWith('*')) desc = desc.slice(1);

                // "[**Register Now**](URL)" リンクがあれば除去する
                desc = desc.replace(/\[\*\*Register Now\*\*\]\(.+?\)/g, '').trim();

                if (desc) descParts.push(desc);
                i++;
            }

            const description = descParts.join(' ').replace(/\s+/g, ' ').trim();
            sessions.push({
                Category:    currentCategory || '',
                Title:       title,
                URL:         url,
                Description: description,
            });
            continue;
        }

        i++;
    }

    return sessions;
}

// ============================================================
// エントリーポイント
// ============================================================

function main() {
    const text = readFileSync(INPUT_FILE, 'utf-8');
    const sessions = parseSessions(text);
    console.log(`パースされたセッション数: ${sessions.length}`);

    // カテゴリが空のレコードを警告(通常は 0 件のはず)
    const emptyCat = sessions.filter(s => !s.Category);
    if (emptyCat.length) {
        console.log(`警告: カテゴリが空のレコード ${emptyCat.length} 件`);
        emptyCat.slice(0, 5).forEach(s => console.log(`  - ${s.Title.slice(0, 60)}`));
    }

    // 説明文が空のレコードを注意表示(通常は 0 件のはず)
    const emptyDesc = sessions.filter(s => !s.Description);
    if (emptyDesc.length) {
        console.log(`注意: 説明文が空のレコード ${emptyDesc.length} 件`);
        emptyDesc.slice(0, 5).forEach(s => console.log(`  - ${s.Title.slice(0, 60)}`));
    }

    // カテゴリ別集計
    const catCounts = {};
    sessions.forEach(s => { catCounts[s.Category] = (catCounts[s.Category] || 0) + 1; });
    console.log('\nカテゴリ別集計:');
    Object.entries(catCounts)
        .sort((a, b) => b[1] - a[1])
        .forEach(([cat, count]) => console.log(`  ${cat}: ${count} 件`));

    // CSV 出力(UTF-8 BOM 付き/Excel で開いても文字化けしない)
    const BOM    = '\uFEFF';
    const header = 'Category,Title,URL,Description';
    const rows   = sessions.map(s =>
        `${csvEscape(s.Category)},${csvEscape(s.Title)},${csvEscape(s.URL)},${csvEscape(s.Description)}`
    );
    const csv = BOM + header + '\n' + rows.join('\n') + '\n';
    writeFileSync(OUTPUT_FILE, csv, 'utf-8');
    console.log(`\nCSVファイルを出力しました: ${OUTPUT_FILE}`);

    // 先頭・末尾5件を表示して目視確認できるようにする
    console.log('\n=== 先頭5件 ===');
    sessions.slice(0, 5).forEach(s => console.log(`[${s.Category}] ${s.Title.slice(0, 70)}`));
    console.log('\n=== 末尾5件 ===');
    sessions.slice(-5).forEach(s => console.log(`[${s.Category}] ${s.Title.slice(0, 70)}`));
}

main();

CSVで見えてきたコンテンツ全体感

400近いコンテンツをCSVにして眺めてみると、いくつかのトレンドが見えてきます。

AI Agent / Agenticが圧倒的な存在感
コンテンツタイトルや概要に「Agent」「Agentic」が含まれるものが非常に多い。昨年のKnowledge25でAi Agentの強化についての話題中心だった流れから、さらに一歩進んで「Ai Agentを実際に使った結果や過程」が今年のメインテーマになっています。

Autonomous XX という命名規則
Autonomous IT、Autonomous HR、Autonomous CRM、Autonomous Risk & Security。各領域に「Autonomous」を冠したスポットライトコンテンツが並んでいます。ServiceNowが全製品横断で「自律化」を推し進めていることが分かります。

日本企業の登壇が増えている
ANA、NTTドコモ、中部電力、NRI、三井物産、ソフトバンク、富士通。日本企業のコンテンツが複数あります。グローバルカンファレンスで日本の事例が発表されるようになってきたのは、日本市場の成長を反映しているのだと思います。

おわりに

Knowledge26のコンテンツ一覧をCSVにしたことで、約400のコンテンツの全体感を把握しやすくなりました。またAIに扱いやすいデータとして変換できたことで、自分が注力したいコンテンツを探させたり、内容を壁打ちすることも可能です。

𝕏:にわと@niwaniwa2wato

Knowledge 26で会える方、ラスベガスでお会いしましょう!

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?