📝

TypeScriptでpdfからテキストを抽出する(PDF.js)

2023/03/13に公開

はじめに

本記事ではNode.js環境でTypeScriptを用いてPDFテキストを抽出する手順について、プロジェクトのセットアップから扱います。また、型定義を参照しつつサンプルコードに解説を加え、筆者の環境で遭遇したエラーも簡単に取り上げています。

想定読者

  • PDFをTypeScriptで扱いたい人。

使うもの/環境

2023年3月上旬、下記の環境化での実装です。

  • OS: Windows 11
  • VSCode
  • Node.js
    • v16.13.2
      • 16系だとエラーが生じます。本文ではそのエラーも扱います。
  • TypeScript
    • v4.5.5
    • インストール済であること、文法の知識は前提としています。
  • pdfjs-dist
    • PDF.jsのnpmライブラリ
    • PDFの操作に使用
  • サンプルPDF

Japanese Constitution
今回のサンプルpdf

手順

プロジェクトの作成

ディレクトリを作成し、npmプロジェクトを作成します。

npm init -y

package.jsonが作成されます。

続いて、下記コマンドでtsconfig.jsonを作成しておきます。

 tsc -init

pdfjs-distの導入

ライブラリをdevDependenciesとして導入。

npm install --save pdfjs-dist

実装

まず、読み込むpdfファイルを.pdf/sample.pdfに格納します。
今回実装するファイルをextractTextFromPDF.tsとして新規作成します。

ディレクトリ構成

📦sampleDir
┣ 📂node_modules
┣ 📂pdf
┃ ┗ 📜sample.pdf
┣ 📜extractTextFromPDF.ts
┣ 📜package-lock.json
┣ 📜package.json
┗ 📜tsconfig.json

extractTextFromPDF.tsの実装

先にサンプルコードを掲示します。

コード
extractTextFromPDF.ts
import * as fs from "fs";
import * as pdfjsLib from "pdfjs-dist";

async function extractTextFromPDF(): Promise<string> {
  const pdfPath = "./pdf/sample.pdf";
  const pdfData = new Uint8Array(fs.readFileSync(pdfPath));

  const loadingTask = pdfjsLib.getDocument({ data: pdfData });
  const pdf = await loadingTask.promise;
  const maxPages = pdf.numPages;
  let pdfText = "";

  for (let pageNumber = 1; pageNumber <= maxPages; pageNumber++) {
    const page = await pdf.getPage(pageNumber);
    const content = await page.getTextContent({ disableCombineTextItems: true, includeMarkedContent: false });
    const pageText = content.items.map((item) => ("str" in item ? item.str : "")).join("\n");
    pdfText += pageText + "\n";
  }
  fs.writeFileSync("./output.txt", pdfText);
  console.log(pdfText);
  return pdfText;
}

extractTextFromPDF().catch((error) => {
  console.error(error);
});
解説

以下、型定義を見つつ、細かめにstep by stepで補足します。

  • fsモジュールでpdfデータの読み込み
    • readFileSync (ドキュメント)
    • 下記のエラーが出ないよう、Uint8Arrayに変換が必要。[1]
  • pdfjs-distによるデータ操作
    • getDocumentPDFDocumentLoadingTaskクラスのインスタンスを取得。
    • promiseメソッドでPromise<PDFDocumentProxy>を返す。
    • PDFDocumentProxyクラスのメソッドでページ数を取得。
    • for文でページごとにイテレート
      • getPage(page)で各ページのオブジェクト(PDFPageProxyクラス)を得る。
      • getTextContentPromise<TextContent>を得る。
        • paramは2種類を取る。
          • disableCombineTextItems: boolean
          • includeMarkedContent?: boolean | undefined;
      • TextContentの型定義は下記。itemsTextItem | TextMarkedContentの配列であることに注意。
      • item.srcでpdfのテキストを取得。
        • srcを持つのはTextItemの場合のみ。"str" in itemで型ガードを入れる。(includeMarkedContentをfalseにしているが、型には反映されない。)
    • output.txtに書き出し。

実行

VSCodeのCode Runnerでコードを実行します。
ts-nodeをインストールして実行しても構いません。[2]

ここでNode.jsのバージョンが16系だと下記のエラーが発生します。

エラー要因はstructuredCloneがバージョン17からのサポートのため。[3]

Issueにも出ていました。
https://github.com/mozilla/pdf.js/issues/14729

ということで、Node.jsをアップデートします。

Node.jsのアップデート

Node.jsのバージョン管理ツールにはVoltaなど林立していますが、今回はnvmを使います。
Windows環境なので、nvm-windowsを使ってアップデートします。
https://github.com/coreybutler/nvm-windows

下記の記事などを参考に、nvm-setup.exeを実行、nvmをインストール。
https://maku77.github.io/nodejs/env/nvm

インストールが済んだらNode.jsを18系に切り替えます。

nvm install 18
nvm use 18
node -v
v18.15.0

これで再度実行。

output.txt
昭和二十一年憲法
日本国憲法日本国民は、正当に選挙された国会における代表者を通じて行動し、われらとわれらの子孫のために、諸国民との協和による成果と、わが国全土にわたつて自由のもたらす恵沢を確保し、政府の行為によつて再び戦争の惨禍が起ることのないやうにすることを決意し、ここに主権が国民に存することを宣言し、この憲法を確定する。そもそも国政は、国民の厳粛な信託によるものであつて、その権威は国民に由来し、その権力は国民の代表者がこれを行使し、その福利は国民がこれを享受する。これは人類普遍の原理であり、この憲法は、かかる原理に基くものである。われらは、これに反する一切の憲法、法令及び詔勅を排除する。日本国民は、恒久の平和を念願し、人間相互の関係を支配する崇高な理想を深く自覚するのであつて、平和を愛する諸国民の公正と信義に信頼して、われらの安全と生存を保持しようと決意した。われらは、平和を維持し、専制と隷従、圧迫と偏狭を地上から永遠に除去しようと努めてゐる国際社会において、名誉ある地位を占めたいと思ふ。われらは、全世界の国民が、ひとしく恐怖と欠乏から免かれ、平和のうちに生存する権利を有することを確認する。われらは、いづれの国家も、自国のことのみに専念して他国を無視してはならないのであつて、政治道徳の法則は、普遍的なものであり、この法則に従ふことは、自国の主権を維持し、他国と対等関係に立たうとする各国の責務であると信ずる。日本国民は、国家の名誉にかけ、全力をあげてこの崇高な理想と目的を達成することを誓ふ。
第一章
 天皇
第一条
 天皇は、日本国の象徴であり日本国民統合の象徴であつて、この地位は、主権の存する日本国民の総意に基く。
第二条
 皇位は、世襲のものであつて、国会の議決した皇室典範の定めるところにより、これを継承する。
第三条
 天皇の国事に関するすべての行為には、内閣の助言と承認を必要とし、内閣が、その責任を負ふ。
第四条
 天皇は、この憲法の定める国事に関する行為のみを行ひ、国政に関する権能を有しない。天皇は、法律の定めるところにより、その国事に関する行為を委任することができる。
第五条
 皇室典範の定めるところにより摂政を置くときは、摂政は、天皇の名でその国事に関する行為を行ふ。この場合には、前条第一項の規定を準用する。
[...]

しっかり取れていますね。

getTextContentdisableCombineTextItemsをfalseにすると、元のpdfにしたがって改行が入ります。

output.txt(disableCombineTextItems
昭和二十一年憲法

日本国憲法
日本国民は、正当に選挙された国会における代表者を通じて行動し、われらとわれらの子孫のために、諸国民との協和による成果と、
わが国全土にわたつて自由のもたらす恵沢を確保し、政府の行為によつて再び戦争の惨禍が起ることのないやうにすることを決意し、こ
こに主権が国民に存することを宣言し、この憲法を確定する。そもそも国政は、国民の厳粛な信託によるものであつて、その権威は国民
に由来し、その権力は国民の代表者がこれを行使し、その福利は国民がこれを享受する。これは人類普遍の原理であり、この憲法は、か
かる原理に基くものである。われらは、これに反する一切の憲法、法令及び詔勅を排除する。
日本国民は、恒久の平和を念願し、人間相互の関係を支配する崇高な理想を深く自覚するのであつて、平和を愛する諸国民の公正と信
義に信頼して、われらの安全と生存を保持しようと決意した。われらは、平和を維持し、専制と隷従、圧迫と偏狭を地上から永遠に除去
しようと努めてゐる国際社会において、名誉ある地位を占めたいと思ふ。われらは、全世界の国民が、ひとしく恐怖と欠乏から免かれ、
平和のうちに生存する権利を有することを確認する。
われらは、いづれの国家も、自国のことのみに専念して他国を無視してはならないのであつて、政治道徳の法則は、普遍的なものであ
り、この法則に従ふことは、自国の主権を維持し、他国と対等関係に立たうとする各国の責務であると信ずる。
日本国民は、国家の名誉にかけ、全力をあげてこの崇高な理想と目的を達成することを誓ふ。

第一章
 
天皇

第一条
 
天皇は、日本国の象徴であり日本国民統合の象徴であつて、この地位は、主権の存する日本国民の総意に基く。

第二条
 
皇位は、世襲のものであつて、国会の議決した皇室典範の定めるところにより、これを継承する。

第三条
 
天皇の国事に関するすべての行為には、内閣の助言と承認を必要とし、内閣が、その責任を負ふ。

第四条
 
天皇は、この憲法の定める国事に関する行為のみを行ひ、国政に関する権能を有しない。
天皇は、法律の定めるところにより、その国事に関する行為を委任することができる。

第五条
 
皇室典範の定めるところにより摂政を置くときは、摂政は、天皇の名でその国事に関する行為を行ふ。この場合には、前条第一項
の規定を準用する。

おわりに

以上、TypeScriptでpdfファイルからテクストを抽出する手順を詳しめに記載しました。

テキストをTypeScriptで取得できると、jestやvitestなどでのテストができ、プログラマティックな校正など可能性が広がります。

思ったよりTypeScriptでの実装例が少なかったので、[4]よろしければ参考にしてください。

脚注
  1. https://techblog.yahoo.co.jp/advent-calendar-2016/node_new_buffer/ ↩︎

  2. https://qiita.com/mangano-ito/items/75e65071c9c482ddc335 ↩︎

  3. https://developer.mozilla.org/ja/docs/Web/API/structuredClone#ブラウザーの互換性
    structuredCloneについては、下記の記事が勉強になりました。
    https://zenn.dev/uhyo/articles/what-is-structuredclone ↩︎

  4. 管見の限り、下記の記事くらい。
    https://blog.goo.ne.jp/dak-ikd/e/6a184fa3ff05f51c42d830b5e61575ce ↩︎

Discussion