どうも。Kenny(tsujikenzo)です。このシリーズでは、 「GASでPDFを分割・結合しよう」 を全3回でお送りします。今日は最終回です。
前回のおさらい
前回は、GASでPDFを結合しようをお届けしました。
今回は、GASでPDFを分割しよう、をお届けします。
全体的な流れ(要件定義)
前回の要件定義と、ダメ出しを、そのまま生成AIにぶち込みます。
このようなプロンプトです。
結合はうまくいきました。それでは、これから分割をやります。
以下の結合についての要件定義を使って、分割についての要件定義を出力してください。
1. 結合したいPDF(何枚あってもOK)を「PDF結合機」というフォルダに投入する
2. 「PDF結合機」フォルダにPDFファイルがあるか確認する
3. あれば、結合用の空のPDFファイルを作る
4. 「PDF結合機」フォルダにあるすべてのPDFを配列に格納する
5. 各PDFファイルのすべてのページをコピーしながら、マージPDFにページを追加していく
6. for文で回す
7. 結合後のファイルに名前を付けて「結合済み」フォルダに保存する
8. 元のPDFファイルは「処理済み」フォルダに移動する
注意
- フォルダ名でフォルダを掴んでいたので、IDから掴むように
- forEach()をfor of文に(forEachはasync/awaitが正しく扱えない)
- 関数を全体的に囲むtryCatchは使わない
- 結合後のファイル名にgetTitle()から取得したファイル名を使用する
- ファイル名を取得するときは、new Uint8Array()から
- 後で読めるように簡潔にコメントを付ける
- 関数の頭にドキュメンテーションコメントも付ける
Geminiは、箇条書きの要件定義を出力しましたが、Chat GPT 4oは、より詳しい要件定義を出力しました。
これは、Chat GPT 4oの方を、採用しようとおもいます。
📄 PDF分割機の要件定義
✅ 基本要件
分割したいPDF(何枚でも可)を「PDF分割機」フォルダに投入する
ユーザーは任意のPDFファイルを指定のフォルダへアップロードします。
「PDF分割機」フォルダにPDFファイルがあるか確認する
フォルダ内にPDFが存在しない場合は処理をスキップし、ログに出力する。
各PDFファイルを1ページごとに分割する
分割対象のPDFファイルの全ページを取得し、各ページを新しいPDFとして保存する。
分割後のファイルを「分割済み」フォルダに保存する
ページごとにファイルを保存し、元のファイル名にページ番号を付与する。
元のPDFファイルは「処理済み」フォルダに移動する
分割が完了したファイルは、再処理防止のために「処理済み」フォルダへ移動する。
📂 フォルダ構成と管理
PDF分割機フォルダ: 分割対象のPDFファイルを格納するフォルダ
分割済みフォルダ: 分割後のPDFファイルを保存するフォルダ
処理済みフォルダ: 処理が完了した元のPDFファイルを保管するフォルダ
注意: フォルダは名前ではなくIDで管理する。
⚙️ 実装上の注意点
フォルダはIDで取得:
DriveApp.getFolderById() を使用してフォルダを取得する。
非同期処理は for...of で実装:
async/await を適切に機能させるため、forEach() は使用せず for...of ループを採用する。
エラーハンドリング:
try-catch を関数全体でラップせず、必要な箇所に限定して使用する。
ファイル名の付与:
分割後のファイル名には、元のファイル名とページ番号を組み合わせた形式を使用する。
例: Invoice_Page_1.pdf, Invoice_Page_2.pdf など。
ファイル名の取得:
PDFのタイトルが存在する場合は getTitle() を使用し、存在しない場合はファイル名を利用する。
データ取得時は new Uint8Array() を使用して正しい形式に変換する。
コメントの付与:
コード内には簡潔でわかりやすいコメントを記述し、関数にはドキュメンテーションコメントを追加する。
📦 ファイル名のルール
元のファイル名: Report.pdf
分割後のファイル名:
Report_Page_1.pdf
Report_Page_2.pdf
Report_Page_3.pdf ...
または、getTitle() で取得したタイトルがある場合は、タイトルを優先します。
🚀 処理フロー
フォルダIDの取得
PropertiesService から PDF_SPLITTER_FOLDER_ID, SPLIT_FOLDER_ID, PROCESSED_FOLDER_ID を取得する。
PDFファイルの取得
DriveApp.getFolderById() を使用し、PDFファイルのリストを作成。
PDFファイルのロード
各ファイルを Uint8Array に変換して PDFLib.PDFDocument.load() で読み込む。
ページごとの分割
copyPages() を使用して各ページを個別のPDFに複製し、新しいPDFとして保存。
ファイル名の作成
getTitle() から取得するか、元のファイル名+ページ番号で命名する。
分割ファイルの保存
DriveApp.createFile() で「分割済み」フォルダに保存。
元ファイルの移動
処理完了後、「処理済み」フォルダにファイルを移動。
ログの出力
各ステップで進捗ログを出力して、トラブルシューティングを容易にする。
📋 サンプルファイルの処理例
入力フォルダ: PDF分割機
ファイル: Contract.pdf (全3ページ)
処理後:
出力フォルダ: 分割済み
Contract_Page_1.pdf
Contract_Page_2.pdf
Contract_Page_3.pdf
元ファイル: Contract.pdf → 処理済み フォルダへ移動
これで、結合機の要件定義をベースにした分割機の要件定義が完成しました! 🚀
フォルダの準備
まずは、結合のときと同様に、処理を行うフォルダを作成します。
そして、プロパティストアにフォルダIDを格納するコードを準備しましょう。
//分割機
const PDF_SPLITTER_FOLDER_ID = "あなたのフォルダID"; // PDF分割機フォルダのID
properties.setProperty("PDF_SPLITTER_FOLDER_ID", PDF_SPLITTER_FOLDER_ID);
const SPLIT_FOLDER_ID = "あなたのフォルダID"; // 分割済みフォルダのID
properties.setProperty("SPLIT_FOLDER_ID", SPLIT_FOLDER_ID);
const PROCESSED_SPLIT_FOLDER_ID = "あなたのフォルダID"; // 分割後の処理済みフォルダのID
properties.setProperty("PROCESSED_SPLIT_FOLDER_ID", PROCESSED_SPLIT_FOLDER_ID);
できあがったコード
以下が完成したコードです。結合と同様に、まずはGoogle Driveクラスや、Fileクラスで、ファイル操作の処理を行っています。
そして、PDFを取得したあとは、PDF-LIBのメソッドを呼び出しながら処理を行っています。
/**
* 📄 PDF分割機
* 指定フォルダ内のPDFを1ページごとに分割し、分割済みフォルダに保存する。
* 元のPDFファイルは処理済みフォルダへ移動する。
*/
async function splitPdfs() {
const properties = PropertiesService.getScriptProperties();
//フォルダIDを取得する
const pdfSplitterFolderId = properties.getProperty("PDF_SPLITTER_FOLDER_ID");
const splitFolderId = properties.getProperty("SPLIT_FOLDER_ID");
const processedFolderId = properties.getProperty("PROCESSED_SPLIT_FOLDER_ID");
//フォルダIDが無いばあいのガード節
if (!pdfSplitterFolderId || !splitFolderId || !processedFolderId) {
console.error("❌ フォルダIDが見つかりません");
return;
}
//フォルダオブジェクトを取得する
const pdfSplitterFolder = DriveApp.getFolderById(pdfSplitterFolderId);
const splitFolder = DriveApp.getFolderById(splitFolderId);
const processedFolder = DriveApp.getFolderById(processedFolderId);
//フォルダ内にあるすべてのPDFファイルを取得する
const pdfFiles = pdfSplitterFolder.getFilesByType(MimeType.PDF);
//空の配列を用意しておく
const pdfFileList = [];
//配列にPDFファイルを格納する
while (pdfFiles.hasNext()) {
pdfFileList.push(pdfFiles.next());
}
//フォルダにPDFがなかったときのガード節
if (pdfFileList.length === 0) {
console.log("📁 PDF分割機フォルダにPDFがありません");
return;
}
//結合するPDFファイル数をカウントしてログ出力
console.log(`🔹 分割するPDF数: ${pdfFileList.length}`);
//PDF配列をfor of文で回しながら、PDFをloadしながらcopyPageしながらaddPageする
for (const file of pdfFileList) {
const blob = file.getBlob();
const bytes = blob.getBytes();
const uint8Array = new Uint8Array(bytes);
try {
const pdfDoc = await PDFLib.PDFDocument.load(uint8Array);
const totalPages = pdfDoc.getPageCount(); //PDFのページ数を取得しておく
//先頭のPDFファイル名を取得しておく
const fileTitle = pdfDoc.getTitle() || file.getName().replace(/\.pdf$/i, "");
console.log(`📄 ${fileTitle} のページ数: ${totalPages}`);
//分割後のファイル名に連番を振るためにイテレータが扱いやすいfor文を採用
for (let i = 0; i < totalPages; i++) {
const newPdf = await PDFLib.PDFDocument.create(); //空のPDFファイルを用意しておく
const [page] = await newPdf.copyPages(pdfDoc, [i]);
newPdf.addPage(page);
const pdfBytes = await newPdf.save(); //.save()メソッドはUint8Array形式を返す
const pageNumber = i + 1;
const splitFileName = `${fileTitle}_Page_${pageNumber}.pdf`;
const splitBlob = Utilities.newBlob(pdfBytes, MimeType.PDF, splitFileName);//Blobを生成する
splitFolder.createFile(splitBlob);//Blobからファイルを作成する
console.log(`✅ ${splitFileName} を保存しました`);
}
//元のファイルを処理済みフォルダへ移動
file.moveTo(processedFolder);
console.log(`🛠️ ${file.getName()} を処理済みフォルダへ移動しました`);
} catch (error) {
console.error(`❌ ${file.getName()} の分割に失敗しました:`, error);
}
}
console.log("🎉 すべてのPDFを分割しました");
}
実行してみる
まず、分割用の元ファイルを、準備しましょう。(前回結合したPDFでいいと思います。)
splitPdfs()を実行します。手で。
分割済みフォルダに、分割済みPDFが完成していれば成功です!ファイル名に連番も付与されていて、バッチリですね。
文字化けや、ページの欠けなどもなく、分割できているようです。
時限トリガー
時限トリガーも設置しましょう。前回の結合を参照ください。割愛します。
まとめ
以上で、「GASでPDFを分割しよう」をお送りしました。
いかがでしたでしょうか。PDF-LIBの分割メソッドは、何ページから何ページまでを分割する、などの高度な設定も可能です。
みなさんが、退屈なPDFの結合や分割から解放されて、価値のある働き方ができることを願っています。