2
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?

(サンプル付き)Next.jsで、PDFからのテキスト抽出をする方法 ×2

Posted at

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の方がおすすめです。

おわり

2
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
2
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?