TypeScriptでpdfからテキストを抽出する(PDF.js)
はじめに
本記事ではNode.js環境でTypeScriptを用いてPDFテキストを抽出する手順について、プロジェクトのセットアップから扱います。また、型定義を参照しつつサンプルコードに解説を加え、筆者の環境で遭遇したエラーも簡単に取り上げています。
想定読者
- PDFをTypeScriptで扱いたい人。
使うもの/環境
2023年3月上旬、下記の環境化での実装です。
- OS: Windows 11
- VSCode
- コードの実行はCode Runnerを使用。
- Node.js
- v16.13.2
- 16系だとエラーが生じます。本文ではそのエラーも扱います。
- v16.13.2
- TypeScript
- v4.5.5
- インストール済であること、文法の知識は前提としています。
-
pdfjs-dist
-
PDF.js
のnpmライブラリ - PDFの操作に使用
-
- サンプルPDF
- 日本語、テキストのみ。
- Marked Contentは対象外。
- 本記事ではe-Govでダウンロードできる『日本国憲法』のpdfをサンプルに用いる。
今回のサンプル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の実装
先にサンプルコードを掲示します。
コード
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で補足します。
- pdfjs-distによるデータ操作
-
getDocument
でPDFDocumentLoadingTask
クラスのインスタンスを取得。 -
promise
メソッドでPromise<PDFDocumentProxy>
を返す。 -
PDFDocumentProxy
クラスのメソッドでページ数を取得。 - for文でページごとにイテレート
-
getPage(page)
で各ページのオブジェクト(PDFPageProxy
クラス)を得る。 -
getTextContent
でPromise<TextContent>
を得る。- paramは2種類を取る。
- disableCombineTextItems: boolean
- includeMarkedContent?: boolean | undefined;
- paramは2種類を取る。
-
TextContent
の型定義は下記。items
はTextItem | 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にも出ていました。
ということで、Node.jsをアップデートします。
Node.jsのアップデート
Node.jsのバージョン管理ツールにはVolta
など林立していますが、今回はnvm
を使います。
Windows環境なので、nvm-windows
を使ってアップデートします。
下記の記事などを参考に、nvm-setup.exe
を実行、nvmをインストール。
インストールが済んだらNode.jsを18系に切り替えます。
nvm install 18
nvm use 18
node -v
v18.15.0
これで再度実行。
昭和二十一年憲法
日本国憲法日本国民は、正当に選挙された国会における代表者を通じて行動し、われらとわれらの子孫のために、諸国民との協和による成果と、わが国全土にわたつて自由のもたらす恵沢を確保し、政府の行為によつて再び戦争の惨禍が起ることのないやうにすることを決意し、ここに主権が国民に存することを宣言し、この憲法を確定する。そもそも国政は、国民の厳粛な信託によるものであつて、その権威は国民に由来し、その権力は国民の代表者がこれを行使し、その福利は国民がこれを享受する。これは人類普遍の原理であり、この憲法は、かかる原理に基くものである。われらは、これに反する一切の憲法、法令及び詔勅を排除する。日本国民は、恒久の平和を念願し、人間相互の関係を支配する崇高な理想を深く自覚するのであつて、平和を愛する諸国民の公正と信義に信頼して、われらの安全と生存を保持しようと決意した。われらは、平和を維持し、専制と隷従、圧迫と偏狭を地上から永遠に除去しようと努めてゐる国際社会において、名誉ある地位を占めたいと思ふ。われらは、全世界の国民が、ひとしく恐怖と欠乏から免かれ、平和のうちに生存する権利を有することを確認する。われらは、いづれの国家も、自国のことのみに専念して他国を無視してはならないのであつて、政治道徳の法則は、普遍的なものであり、この法則に従ふことは、自国の主権を維持し、他国と対等関係に立たうとする各国の責務であると信ずる。日本国民は、国家の名誉にかけ、全力をあげてこの崇高な理想と目的を達成することを誓ふ。
第一章
天皇
第一条
天皇は、日本国の象徴であり日本国民統合の象徴であつて、この地位は、主権の存する日本国民の総意に基く。
第二条
皇位は、世襲のものであつて、国会の議決した皇室典範の定めるところにより、これを継承する。
第三条
天皇の国事に関するすべての行為には、内閣の助言と承認を必要とし、内閣が、その責任を負ふ。
第四条
天皇は、この憲法の定める国事に関する行為のみを行ひ、国政に関する権能を有しない。天皇は、法律の定めるところにより、その国事に関する行為を委任することができる。
第五条
皇室典範の定めるところにより摂政を置くときは、摂政は、天皇の名でその国事に関する行為を行ふ。この場合には、前条第一項の規定を準用する。
[...]
しっかり取れていますね。
getTextContent
のdisableCombineTextItems
をfalseにすると、元のpdfにしたがって改行が入ります。
昭和二十一年憲法
日本国憲法
日本国民は、正当に選挙された国会における代表者を通じて行動し、われらとわれらの子孫のために、諸国民との協和による成果と、
わが国全土にわたつて自由のもたらす恵沢を確保し、政府の行為によつて再び戦争の惨禍が起ることのないやうにすることを決意し、こ
こに主権が国民に存することを宣言し、この憲法を確定する。そもそも国政は、国民の厳粛な信託によるものであつて、その権威は国民
に由来し、その権力は国民の代表者がこれを行使し、その福利は国民がこれを享受する。これは人類普遍の原理であり、この憲法は、か
かる原理に基くものである。われらは、これに反する一切の憲法、法令及び詔勅を排除する。
日本国民は、恒久の平和を念願し、人間相互の関係を支配する崇高な理想を深く自覚するのであつて、平和を愛する諸国民の公正と信
義に信頼して、われらの安全と生存を保持しようと決意した。われらは、平和を維持し、専制と隷従、圧迫と偏狭を地上から永遠に除去
しようと努めてゐる国際社会において、名誉ある地位を占めたいと思ふ。われらは、全世界の国民が、ひとしく恐怖と欠乏から免かれ、
平和のうちに生存する権利を有することを確認する。
われらは、いづれの国家も、自国のことのみに専念して他国を無視してはならないのであつて、政治道徳の法則は、普遍的なものであ
り、この法則に従ふことは、自国の主権を維持し、他国と対等関係に立たうとする各国の責務であると信ずる。
日本国民は、国家の名誉にかけ、全力をあげてこの崇高な理想と目的を達成することを誓ふ。
第一章
天皇
第一条
天皇は、日本国の象徴であり日本国民統合の象徴であつて、この地位は、主権の存する日本国民の総意に基く。
第二条
皇位は、世襲のものであつて、国会の議決した皇室典範の定めるところにより、これを継承する。
第三条
天皇の国事に関するすべての行為には、内閣の助言と承認を必要とし、内閣が、その責任を負ふ。
第四条
天皇は、この憲法の定める国事に関する行為のみを行ひ、国政に関する権能を有しない。
天皇は、法律の定めるところにより、その国事に関する行為を委任することができる。
第五条
皇室典範の定めるところにより摂政を置くときは、摂政は、天皇の名でその国事に関する行為を行ふ。この場合には、前条第一項
の規定を準用する。
おわりに
以上、TypeScriptでpdfファイルからテクストを抽出する手順を詳しめに記載しました。
テキストをTypeScriptで取得できると、jestやvitestなどでのテストができ、プログラマティックな校正など可能性が広がります。
思ったよりTypeScriptでの実装例が少なかったので、[4]よろしければ参考にしてください。
-
https://techblog.yahoo.co.jp/advent-calendar-2016/node_new_buffer/ ↩︎
-
https://developer.mozilla.org/ja/docs/Web/API/structuredClone#ブラウザーの互換性
structuredCloneについては、下記の記事が勉強になりました。
https://zenn.dev/uhyo/articles/what-is-structuredclone ↩︎ -
管見の限り、下記の記事くらい。
https://blog.goo.ne.jp/dak-ikd/e/6a184fa3ff05f51c42d830b5e61575ce ↩︎
Discussion