Next.jsでPDFからテキスト情報を抽出する方法
背景
社内で利用するアプリケーションにおいて、PDFをアップロードし、その中身のテキスト情報を取り出す機能が必要でした。
願望・要件
- 可能であれば、Next.js内で完結したい
- Lambda関数を実装する手もあるが、面倒なので、できれば、Next.js内で済ませたい
- 取り出したテキストをS3に保存するなどは必要なく、取り出したテキストを扱ってAPIを実行できればOK
手段
その1 ライブラリを使う
下記のライブラリを使用する。
https://www.npmjs.com/package/pdf2json
node.js環境で、PDFファイルをテキストやJSONに変換してくれるライブラリ。
npm i pdf2json
でインストールし、
Next.jsのAPIルートで、下記のように使用します。
import { type NextRequest, NextResponse } from 'next/server';
import PDFParser from 'pdf2json';
const extractTextFromPDF = (pdfBuffer: Buffer): Promise<string> => {
return new Promise((resolve, reject) => {
const pdfParser = new PDFParser(null, true);
pdfParser.on('pdfParser_dataError', (errData) => {
reject(new Error(`PDF parsing error: ${errData.parserError}`));
});
pdfParser.on('pdfParser_dataReady', (pdfData) => {
try {
let text = '';
if (pdfData?.Pages) {
for (const page of pdfData.Pages) {
if (page.Texts) {
for (const textElement of page.Texts) {
if (textElement.R) {
for (const run of textElement.R) {
if (run.T) {
text += `${decodeURIComponent(run.T)} `;
}
}
}
}
}
text += '\n';
}
}
resolve(text.trim());
} catch (error) {
reject(error);
}
});
pdfParser.parseBuffer(pdfBuffer);
});
};
export async function POST(request: NextRequest) {
try {
// 省略 セッションチェックなど //
const formData = await request.formData();
const data = {
file: formData.get('file')
};
const { file } = data.file;//省略していますが、実際はzodを使って、parseしています。
const buffer = Buffer.from(await file.arrayBuffer());
let content = '';
try {
content = await extractTextFromPDF(buffer);
} catch (error) {
console.error('PDF parsing error:', error);
return NextResponse.json({ error: 'PDFの解析に失敗しました' }, { status: 400 });
}
//省略 後処理 //
return NextResponse.json({ success: true });
} finally {
//省略
}
} catch (error) {
console.error('File create error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
上記のAPIに対して、フロントエンド側で、下記のようにファイルを送ればOK。
(formというstateでinput要素から受け取ったFileを管理している前提)
const saveFile = async () => {
try {
setIsLoading(true);
// ファイルがアップロードされている場合のみ処理
if (form.fileData) {
const formData = new FormData();
formData.append('file', form.fileData);
await axios.post('/api/files/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
onSuccess();
} else {
alert('ファイルを選択してください');
}
} catch (error) {
console.error('Error saving file:', error);
alert('ファイルの処理に失敗しました');
} finally {
setIsLoading(false);
}
};
その2 child_processでLinuxコマンドを実行
Linuxのコマンドには、pdftotextというPDFからテキストを取り出すコマンドがあります。
pdftotext (読込むPDFファイル) (出力するテキストファイル)
のように使います。
これをNext.js内で使用します。
そのために、Node.jsのchild_processを使います。
https://nodejs.org/api/child_process.html#child-process
child_processは子プロセスを作成し、コマンドを実行するためのモジュールです。
execFileは、
これを使うと、APIルートは下記のように書けます。
import { type NextRequest, NextResponse } from 'next/server';
import { execFile } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { promises as fs } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const extractTextWithPdftotext = async (pdfBuffer: Buffer): Promise<string> => {
const tmpDir = tmpdir();
const pdfPath = join(tmpDir, `upload-${randomUUID()}.pdf`);
await fs.writeFile(pdfPath, pdfBuffer);
return await new Promise<string>((resolve, reject) => {
// 出力を標準出力へ (-) にする
const args = [pdfPath, '-'];
execFile('pdftotext', args, async (error, stdout, stderr) => {
try {
await fs.unlink(pdfPath);
} catch (e) {
console.error('Failed to delete temp PDF file:', e);
}
if (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return reject(new Error('pdftotext コマンドが見つかりません'));
}
return reject(new Error(`pdftotext 実行エラー: ${stderr || error.message}`));
}
resolve(stdout.trim());
});
});
};
export async function POST(request: NextRequest) {
// 省略 方法1と同じように、extractTextWithPdftotextを呼び出せばOK
}
フロントエンドからのpostは方法1と同じように行います。
また、そもそも、pdftotextが使えるように、インストールしておく必要があります。
Dockerfileで、poppler-utilsをインストールしています。
poppler-utilsの中に、pdftotextが含まれます。
RUN apt update \
&& apt -y install locales poppler-utils \
&& \localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
(一緒にlocalesなどもインストールしています)
Dockerを使用しない場合、macであれば、
brew install poppler
で、インストールできます。
比較
両方試した結果としては、
個人的には、
- 追加のライブラリが不要
- コードがシンプル
- 処理時間が早い(体感)
という点から、方法2の方がおすすめです。
おわり