1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PowerPointからテキストを抽出するWSH+JavaScriptをささっと作る

1
Last updated at Posted at 2026-04-19

はじめに

PowerPoint プレゼンテーションからテキストを抽出したいときがあります。一般的には Python を使う方法1などが知られています。フリーソフトをインストールするのに会社の情シス部門に申請書を提出する必要があるなど手続きが面倒な人も多いでしょう。なので、新たにアプリをインストールすることなく、Windows に最初からあるツール・ユーティリティだけで実現したいのです。世の中ではそういう人々を 初期装備ニキ と呼んでいるらしいですが・・・

ただし Microsoft Office は必須です。

なお、スクリプトの名前は pptStrings.js とします。UNIX だとバイナリから文字列を抽出する strings というコマンドがあり,そのコマンドにちなみました。

お品書き(仕様案)

  • 長い長い PowerPoint ファイル名を指定するのが面倒なので,ハイフン(-)とすれば現在開いているアクティブプレゼンテーションとします。
    • デフォルトでは全スライドのテキストを抽出します。オプション /S によってスライド番号を指定できます。スライド番号は 1-origin です。
    • 複数ページを指定する場合:/S:1,3,5
    • ページ範囲を指定する場合:/S:1-10
  • 図形のテキストを抽出します。もちろん表のテキストも抽出できるものとします。
  • テキストファイルの文字コードについて,デフォルトは shift_jis としますが, オプション /U を指定すると utf-8 とします。
  • フォント属性の異なる領域がある場合,デフォルトでは結合して出力しますが,オプション /D を指定すると分割して出力します。
  • デフォルトでは数式を出力しませんが,オプション /M を指定すると出力します。
  • 文字列両端(先頭と末尾)のスペースについて,デフォルトは出力しますが,オプション /T を指定すると削除します。
オプション一覧
オプション 意味 デフォルト
/S:[n] Slide number
出力するスライド番号を指定
スライド番号はコンマ区切りで複数指定可能
例)/S:1,3,5
ハイフン区切りで範囲指定も可能
例)/S:1-10
全ページを出力
/U UTF-8 format
UTF-8で出力
shift_jisで出力
/D Divide string
フォント属性の異なる領域を分割して出力
結合して出力
/M Mathzone output
数式を出力
数式は出力しない
/T Trim space
文字列両端のスペースを削除
スペースを出力

実装コード

気の短い人もいるでしょうから先に実装コードを示します。

実装コードはコチラ
pptstrings.js
//------------------------------------------------------------------------------
// MsoShapeType 列挙
// https://learn.microsoft.com/ja-jp/office/vba/api/office.msoshapetype
//------------------------------------------------------------------------------
var	msoGroup = 6;	// グループ
//------------------------------------------------------------------------------
// メイン関数の呼び出し
//------------------------------------------------------------------------------
var	args = WScript.Arguments.Unnamed;
var	opts = WScript.Arguments.Named;
var	ret = main(args, opts);
try {
	WScript.Quit(ret);
} catch(e) {
	/* 何もしない */
}
//------------------------------------------------------------------------------
// メイン関数
//------------------------------------------------------------------------------
function main(args, opts) {
	//--------------------------------------------------------------------------
	// オプション
	//--------------------------------------------------------------------------
	var	    utf8_format = false;			// utf-8 形式で出力
	var	  divide_string = false;			// フォント属性の異なる文字列を分割
	var	mathzone_output = false;			// 数式ゾーンを出力
	var	     trim_space = false;			// 文字列両端のスペースを削除
	//--------------------------------------------------------------------------
	// ヘルプメッセージの表示
	//--------------------------------------------------------------------------
	if(args.Count < 2) {
		var	msg = [
			"PowerPoint ファイルの文字列を抽出します。",
			"",
			"PPTSTRINGS(.JS) (オプション) [PowerPointファイル名] [テキストファイル名]",
			"",
			"<オプション>",
			"/S:[n] スライド番号を指定します。デフォルトは全ページを対象とします。",
			"        番号は 1,2,3 とコンマ(,)区切りで複数ページを指定可能です。",
			"        番号は 10-20 とハイフン(-)区切りでページ範囲を指定できます。",
			"/U      utf-8 形式で出力します。デフォルトは shift_jis です。",
			"/D      フォント属性の異なる文字列を分割して出力します。",
			"/M      数式ゾーンを出力します。",
			"/T      文字列両端のスペースを削除します。",
			"",
			"<注意事項>",
			"・ファイル名をハイフン(-)とするとアクティブプレゼンテーションを対象とします。"
		];
		WScript.StdErr.WriteLine(msg.join("\n"));
		return -1;
	}
	//--------------------------------------------------------------------------
	// コマンドライン解析
	//--------------------------------------------------------------------------
	var	  input_filename = args(0);
	var	 output_filename = args(1);
	var	pages = [];
	if(opts.Exists("S")) {
		var	a = opts("S").split(",");
		for(var i = 0; i < a.length; i++) {
			var	m;
			if((m = a[i].match(/^(\d+)-(\d+)$/)) != null) {
				var	start = parseInt(m[1]);
				var	  end = parseInt(m[2]);
				if(start > end) {
					WScript.StdErr.WriteLine("ページ番号の指定 " + a[i] + " が不正です!!");
					return -1;
				}
				for(var n = start; n <= end; n++)
					pages.push(n);
			} else if((m = a[i].match(/^(\d+)$/)) != null) {
				var	n = parseInt(m[1]);
				pages.push(n);
			} else {
				WScript.StdErr.WriteLine("ページ番号の指定 " + a[i] + " が不正です!!");
				return -1;
			}
		}
	}
	if(opts.Exists("U"))     utf8_format = true;
	if(opts.Exists("D"))   divide_string = true;
	if(opts.Exists("M")) mathzone_output = true;
	if(opts.Exists("T"))      trim_space = true;
	//--------------------------------------------------------------------------
	// PowerPoint の起動
	//--------------------------------------------------------------------------
	var	app;
	try {
		app = GetObject("", "PowerPoint.Application");
	} catch(e) {
		app = WScript.CreateObject("PowerPoint.Application");
	}
	if(app == null) {
		WScript.StdErr.WriteLine("PowerPoint の起動に失敗しました!!");
		return null;
	}
	app.Visible = true;
	//--------------------------------------------------------------------------
	// PowwerPoint ファイルの取得
	//--------------------------------------------------------------------------
	var	ppt = null;
	if(input_filename != "-") {
		var	fso = WScript.CreateObject("Scripting.FileSystemObject");
		if(!fso.FileExists(input_filename)) {
			WScript.StdErr.WriteLine("ファイル " + input_filename + " が存在しません!!");
			return -1;
		}
		var	fullpath1 = fso.GetAbsolutePathName(input_filename).toLowerCase();
		for(var i = 1; i <= app.Presentations.Count; i++) {
			var	fullpath2 = app.Presentations(i).FullName.toLowerCase();
			if(fullpath1 == fullpath2) {
				ppt = app.Presentations(i);
				break;
			}
		}
		if(ppt == null) {
			ppt = app.Presentations.Open(fullpath1);
			if(ppt == null) {
				WScript.StdErr.WriteLine("ファイル " + fullpath1 + " のオープンに失敗しました!!");
				return -1;
			}
		}
	} else if(app.Presentations.Count == 0) {
		WScript.StdErr.WriteLine("現在オープンしているプレゼンテーションがありません!!");
		return -1;
	} else if(app.ActivePresentation == null) {
		WScript.StdErr.WriteLine("アクティブプレゼンテーションが存在しません!!");
		return -1;
	} else {
		ppt = app.ActivePresentation;
	}
	//--------------------------------------------------------------------------
	// ADODBSTREAM クラスのオープン
	//--------------------------------------------------------------------------
	var	ado = new ADODBSTREAM();
	ado.CreateTextFile(output_filename, utf8_format ? "utf-8" : "shift_jis", true);
	//--------------------------------------------------------------------------
	// テキスト範囲を対象とする
	//--------------------------------------------------------------------------
	var	parse_range = function(range) {
		if(!mathzone_output && range.MathZones.Count != 0)
			return;
		var	s = range.Text;
		s = s.replace(/\r/g, "");
		if(trim_space)
			s = s.replace(/^[\s ]+|[\s ]+$/, "");
		ado.WriteLine(s);
	};
	//--------------------------------------------------------------------------
	// テキストフレームを対象とする
	//--------------------------------------------------------------------------
	var parse_frame = function(shape) {
		if(!shape.HasTextFrame) return;
		if(!shape.TextFrame2.HasText) return;
		var	range = shape.TextFrame2.TextRange;
		if(divide_string) {
			for(var i = 1; i <= range.Runs.Count; i++)
				parse_range(range.Runs(i));
		} else {
			parse_range(range);
		}
	};
	//--------------------------------------------------------------------------
	// 図形を対象とする
	//--------------------------------------------------------------------------
	var	parse_shape = function(shape) {
		if(shape.Type == msoGroup)
			for(var i = 1; i <= shape.GroupItems.Count; i++)
				parse_shape(shape.GroupItems(i));
		if(shape.HasTable)
			for(var i = 1; i <= shape.Table.Rows.Count; i++)
				for(var j = 1; j <= shape.Table.Columns.Count; j++)
					parse_frame(shape.Table.Cell(i, j).Shape);
		parse_frame(shape);
	}
	//--------------------------------------------------------------------------
	// スライドを対象とする
	//--------------------------------------------------------------------------
	var	parse_slide = function(slide) {
		slide.Select();
		for(var i = 1; i <= slide.Shapes.Count; i++)
			parse_shape(slide.Shapes(i));
	};
	//--------------------------------------------------------------------------
	// 指定されたページ番号,または全ページを対象とする
	//--------------------------------------------------------------------------
	if(pages.length == 0) {
		for(var i = 1; i <= ppt.Slides.Count; i++) {
			parse_slide(ppt.Slides(i));
		}
	} else {
		for(var i = 0; i < pages.length; i++) {
			var	n = pages[i];
			if(n < 1 || n > ppt.Slides.Count) continue;
			parse_slide(ppt.Slides(n));
		}
	}
	//--------------------------------------------------------------------------
	// ADODBSTREAM クラスのクローズ
	//--------------------------------------------------------------------------
	ado.Close();
	return 0;
}
//------------------------------------------------------------------------------
// ADODB.Stream ラッパークラス
//------------------------------------------------------------------------------
function ADODBSTREAM() {
	this.adTypeBinary = 1;
	this.adTypeText   = 2;
	this.adReadAll    = -1;
	this.adReadLine   = -2;
	this.adWriteChar  = 0;
	this.adWriteLine  = 1;
	this.adSaveCreateNotExist  = 1;
	this.adSaveCreateOverWrite = 2;
	this.stream = WScript.CreateObject("ADODB.Stream");
	this.filename  = "";
	this.overwrite = false;
	this.CreateTextFile = function(filename, charset, overwrite) {
		this.stream.Type = this.adTypeText;
		this.stream.CharSet = charset;
		this.stream.Open();
		this.filename  = filename;
		this.overwrite = overwrite;
	};
	this.WriteLine = function(buf) {
		this.stream.WriteText(buf, this.adWriteLine);
	};
	this.Close = function() {
		this.stream.SaveToFile(this.filename,
			this.overwrite ? this.adSaveCreateOverWrite : this.adSaveCreateNotExist);
		this.stream.Close();
	};
}

技術解説

1. オプション解析

オプション /S を解析して,テキストを抽出するスライド番号を配列 pages[] に格納します。配列が空の場合は全スライドを対象とします。ページ範囲を指定した場合,開始番号と終了番号の大小関係に注意してチェック機能をつけました。

オプション解析
var	opts = WScript.Arguments.Named;

var	pages = [];
if(opts.Exists("S")) {
	var	a = opts("S").split(",");
	for(var i = 0; i < a.length; i++) {
		var	m;
		if((m = a[i].match(/^(\d+)-(\d+)$/)) != null) {
			var	start = parseInt(m[1]);
			var	  end = parseInt(m[2]);
			if(start > end) {
				WScript.StdErr.WriteLine("ページ番号の指定 " + a[i] + " が不正です!!");
				return -1;
			}
			for(var n = start; n <= end; n++)
				pages.push(n);
		} else if((m = a[i].match(/^(\d+)$/)) != null) {
			var	n = parseInt(m[1]);
			pages.push(n);
		} else {
			WScript.StdErr.WriteLine("ページ番号の指定 " + a[i] + " が不正です!!");
			return -1;
		}
	}
}

2.PowerPoint の起動

PowerPoint を起動します。既に起動している場合は流用します。Visible 属性を true にすることを忘れないようにしましょう。

PowerPoint の起動
var	app;
try {
	app = GetObject("", "PowerPoint.Application");
} catch(e) {
	app = WScript.CreateObject("PowerPoint.Application");
}
if(app == null) {
	WScript.StdErr.WriteLine("PowerPoint の起動に失敗しました!!");
	return null;
}
app.Visible = true;

3. プレゼンテーションのオープン

ファイル名をハイフン(-)とすればアクティブプレゼンテーションとします。アクティブプレゼンテーションが存在しない場合はエラーとしますが,現在オープンしているプレゼンテーションの数 Presentations.Count == 0 で判別することにしました。当初,ActivePresentation == null で検知しようとしたのですが,うまく検知できませんでした。※条件判断は残してあります。

ハイフン(-)ではない場合は絶対パス名を取得します。PowerPoint で現在オープンしているプレゼンテーションファイル名と比較し,絶対パスが一致していればそのプレゼンテーションオブジェクトを取得します。見つからなければ新規オープンします。

プレゼンテーションのオープン
var	ppt = null;
if(input_filename != "-") {
	var	fso = WScript.CreateObject("Scripting.FileSystemObject");
	if(!fso.FileExists(input_filename)) {
		WScript.StdErr.WriteLine("ファイル " + input_filename + " が存在しません!!");
		return -1;
	}
	var	fullpath1 = fso.GetAbsolutePathName(input_filename).toLowerCase();
	for(var i = 1; i <= app.Presentations.Count; i++) {
		var	fullpath2 = app.Presentations(i).FullName.toLowerCase();
		if(fullpath1 == fullpath2) {
			ppt = app.Presentations(i);
			break;
		}
	}
	if(ppt == null) {
		ppt = app.Presentations.Open(fullpath1);
			if(ppt == null) {
			WScript.StdErr.WriteLine("ファイル " + fullpath1 + " のオープンに失敗しました!!");
			return -1;
		}
	}
} else if(app.Presentations.Count == 0) {
	WScript.StdErr.WriteLine("現在オープンしているプレゼンテーションがありません!!");
	return -1;
} else if(app.ActivePresentation == null) {
	WScript.StdErr.WriteLine("アクティブプレゼンテーションが存在しません!!");
	return -1;
} else {
	ppt = app.ActivePresentation;
}

4. プレゼンテーションの処理

配列 pages[] が空の場合は全スライドを対象とします。スライド番号を指定した場合は,配列に格納されたスライド番号に従って処理を進めます。

指定したスライド番号がゼロまたは負の値であったり,全スライド番号(最終ページ番号)よりも大きい場合はエラーとしないでスキップするようにしました。

プレゼンテーションの処理
if(pages.length == 0) {
	for(var i = 1; i <= ppt.Slides.Count; i++) {
		parse_slide(ppt.Slides(i));
	}
} else {
	for(var i = 0; i < pages.length; i++) {
		var	n = pages[i];
		if(n < 1 || n > ppt.Slides.Count) continue;
		parse_slide(ppt.Slides(n));
	}
}

5. スライドの処理

スライド上に存在する全ての図形を対象とします。処理過程が分かるように処理中のスライドをアクティブにします。

スライドの処理
var	parse_slide = function(slide) {
	slide.Select();
	for(var i = 1; i <= slide.Shapes.Count; i++)
		parse_shape(slide.Shapes(i));
};

6. 図形の処理

グループ化した図形は再帰で処理します。表(テーブル)はセルごとに処理します。

図形の処理
var	msoGroup = 6;	// グループ

var	parse_shape = function(shape) {
	if(shape.Type == msoGroup)
		for(var i = 1; i <= shape.GroupItems.Count; i++)
			parse_shape(shape.GroupItems(i));
	if(shape.HasTable)
		for(var i = 1; i <= shape.Table.Rows.Count; i++)
			for(var j = 1; j <= shape.Table.Columns.Count; j++)
				parse_frame(shape.Table.Cell(i, j).Shape);
	parse_frame(shape);
}

7. テキストフレームの処理

Microsoft Office のテキストオブジェクトには TextFrame オブジェクトと TextFrame2オブジェクトの二つあります。どちらも TextRange というプロパティを持ちます。TextFrame オブジェクトの TextRange プロパティは文字通り TextRange オブジェクトですが,TextFrame2 オブジェクトの TextRange プロパティは TextRange2 オブジェクトなので要注意です。また TextRange オブジェクトと TextRange2 オブジェクトはどちらも Runs メンバーを持ちます。TextRange.Runs はメソッドですが,TextRange2.Runs はプロパティという違いがあり,両者はよく似ていますが仕様が微妙に異なるので要注意です。

var parse_frame = function(shape) {
	if(!shape.HasTextFrame) return;
	if(!shape.TextFrame2.HasText) return;
	var	range = shape.TextFrame2.TextRange;
	if(divide_string) {
		for(var i = 1; i <= range.Runs.Count; i++)
			parse_range(range.Runs(i));
	} else {
		parse_range(range);
	}
};

8. テキスト処理

ようやくテキスト処理に辿り着けました。改行コード(\n)はそのまま通過させますが,キャリッジリターン(\r)は削除します。ADODB.Stream にバイナリ出力するのであれば削除する必要はありませんが,今回はテキストモードで出力するので削除する必要があります。

テキスト処理
var	parse_range = function(range) {
	if(!mathzone_output && range.MathZones.Count != 0)
    	return;
	var	s = range.Text;
	s = s.replace(/\r/g, "");
	if(trim_space)
		s = s.replace(/^[\s ]+|[\s ]+$/, "");
	ado.WriteLine(s);
};

9. Shift_JIS/UTF-8 出力

Windows Script Host でシステム既定の文字コード(日本語版 Windows だと Shift_JIS)もしくは UnicodeUTF-16lE)以外で出力しようとすると,ADODB.Stream を使う必要があります。今回,ADODB.Stream をそのまま使わず,FileSystemObject のインタフェースに似せた薄いラッパーを作りました。とくに今回はファイル出力だけで良いので簡単です。メソッドは下記の3つしかありません。

  • CreateTextFile(filename, charset, overwrite)
  • WriteLine(string)
  • Close()

FileSystemObject のメソッドは引数を省略可能ですが,このラッパーは省略できません。

ADODB.Stream ラッパークラス
function ADODBSTREAM() {
	this.adTypeBinary = 1;
	this.adTypeText   = 2;
	this.adReadAll    = -1;
	this.adReadLine   = -2;
	this.adWriteChar  = 0;
	this.adWriteLine  = 1;
	this.adSaveCreateNotExist  = 1;
	this.adSaveCreateOverWrite = 2;
	this.stream = WScript.CreateObject("ADODB.Stream");
	this.filename  = "";
	this.overwrite = false;
	this.CreateTextFile = function(filename, charset, overwrite) {
		this.stream.Type = this.adTypeText;
		this.stream.CharSet = charset;
		this.stream.Open();
		this.filename  = filename;
		this.overwrite = overwrite;
	};
	this.WriteLine = function(buf) {
		this.stream.WriteText(buf, this.adWriteLine);
	};
	this.Close = function() {
		this.stream.SaveToFile(this.filename,
			this.overwrite ? this.adSaveCreateOverWrite : this.adSaveCreateNotExist);
		this.stream.Close();
	};
}

使い方

1. 引数なしで実行

引数なしで実行するとヘルプメッセージを表示します。

引数なしで実行したとき
c:\Qiita>pptstrings
PowerPoint ファイルの文字列を抽出します。

PPTSTRINGS(.JS) (オプション) [PowerPointファイル名] [テキストファイル名]

<オプション>
/S:[n] スライド番号を指定します。デフォルトは全ページを対象とします。
        番号は 1,2,3 とコンマ(,)区切りで複数ページを指定可能です。
        番号は 10-20 とハイフン(-)区切りでページ範囲を指定できます。
/U      utf-8 形式で出力します。デフォルトは shift_jis です。
/D      フォント属性の異なる文字列を分割して出力します。
/M      数式ゾーンを出力します。
/T      文字列両端のスペースを削除します。

<注意事項>
・ファイル名をハイフン(-)とするとアクティブプレゼンテーションを対象とします。

2. サンプルプレゼンテーションの内容

サンプル sample.pptx のスライド内容です。

それぞれ内訳は下記のようになっています。

  • タイトル文字列:スライドマスターでスタイル定義したタイトルです。実態は当該スライド上にあります。
  • あいうえお:通常のテキストボックスです。
  • $E=mc^2$:通常のテキストボックスに数式を挿入しました。
  • 【1】まとめ:通常のテキストボックスです。
  • A,B,C,D:表(テーブル)です。
  • グループ①,グループ②:それぞれ通常のテキストボックスですが,グループ化しています。

3. デフォルト実行

オプションなしで実行した場合です。

デフォルト実行
c:\Qiita>pptstrings sample.pptx sample1.txt

数式はデフォルトでは出力されません。

sample1.txt
タイトル文字列
あいうえお
【1】まとめ
A
B
C
D
グループ①
グループ②

4. フォント属性の異なる文字列を分割

フォント属性の異なる文字列を分割して出力した場合です。

フォント属性の異なる文字列を分割して出力
c:\Qiita>pptstrings sample.pptx sample2.txt /D

まさか【】で区切られるとは予想外でした。

sample2.txt
タイトル文字列
あいうえお
【
1
】
まとめ
A
B
C
D
グループ①
グループ②

5. 数式を出力

数式を出力するときは文字コードを UTF-8 にしないと文字化けします。また文字列両端のスペースを削除するようにしました。

数式を出力かつ文字コードをUTF-8にして出力
c:\Qiita>pptstrings sample.pptx sample3.txt /M /U /T

数式の領域が斜体になっています。これらの文字は数学用英数字記号と呼ばれ,文字コード Unicode1D4001D7FF に割り当てられており,Shift_JIS の領域外です2

sample3.txt
タイトル文字列
あいうえお
𝐸=𝑚 𝑐 2
【1】まとめ
A
B
C
D
グループ①
グループ②

今後の課題

  1. 本当は抽出したテキストを標準出力に出力したかったのですが,UTF-8 で標準出力に出力する方法が分からなかったので諦めました。不可能かもしれません。

  2. 指定したスライド番号はソートしたほうが良いかもしれません。

  3. テキストを抽出する図形は登録順ですが,位置に応じてソートしたほうが良いかもしれません。

  4. ADODB.Stream のラッパーについては引数を省略可能にできるなど,もう少し手を入れたいところです。

  5. AI に自作コードを添削させたら SmartArt 対応の追加を提案してきました。

    SmartArt対応コード
    if (shape.HasSmartArt) {
        var nodes = shape.SmartArt.AllNodes;
        for (var i = 1; i <= nodes.Count; i++) {
            var node = nodes(i);
            if (node.TextFrame2.HasText)
                parse_range(node.TextFrame2.TextRange);
        }
    }
    

    また AIPowerPoint では msoGroup 以外にも msoCanvasmsoDiagram が子図形を持つ場合があるとのことで,厳密には下記のように書いたほうが良いと提案してきました。

    子図形を持つ図形の処理
    if (shape.Type == msoGroup || shape.Type == msoCanvas || shape.Type == msoDiagram) {
        for (var i = 1; i <= shape.GroupItems.Count; i++)
            parse_shape(shape.GroupItems(i));
    }
    

    これらの図形タイプを自分は扱ったことが無く,今のところ無くても困らないので採用を保留していますが,必要と感じた方は上記のコードを parse_shape に追加してください。テスト素材に困るのが最大の理由でしょうか?

最後に

実はこのスクリプトは元々 VBScript で書かれていたものを JavaScript 向けに全面書き換えたものです。JavaScript の言語仕様としては相当古いものですが,それでも正規表現や可変配列が使えるのは便利です。

  1. PythonでPowerPointからテキストを抽出する!(テーブルにも対応)- Qiita

  2. Unicode 数学用英数字記号 - 図書館員のコンピュータ基礎講座

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?