PDFをOCRしてScrapboxに取り込むDeno script
機能
pdf fileを指定すると、1ページごとにscrapboxのページになるようにしたjson fileを作る
PDFのページは画像に変換してGyazoにあげる
解像度は指定可能
OCRも実行し、各ページに引用形式で書き込む
やること
予め画像化されたPDFのページをGyazoに上げて、Scrapboxのpageにしたjson fileを作る
OCRされたテキストデータが有れば、それも取り込む
やらないこと
pdfを画像に変換する
OCRをする
タイトルから中身が乖離してきたな……
実装
pdfの読み込みとGyazoへのupload
一つづつawaitで待ってuploadするようにした
uploadの順番と、ページ番号の順番とを一致させたい
2021-01-28 00:02:26 ……これ一致させる必要あるかな?
全部一斉に送っちゃえ!
json fileにしてdownloadする
問題はレイアウトか。
1 pageごとにScrapbox pageに分ける
タイトル
PDFの名前 + ページ番号
\
${title} ${pageNum}\
でいいか。
メタ情報をどうやって入れる?
PDFのテキストを勝手にScrapboxに挿入できるようにするととても楽
2021-01-24 19:28:40 方法が見つからないので断念
とりあえずタイトルにノート名を入れるとして
ノート名+ページ番号
前後のページに対するリンクも貼っておく?
ノート名をhashtagでつけるだけでもいいのでは?
でも前後のリンクもあると読みやすいか

じゃあつけるか。
→cf.xxxのリンクが貼ってあったら、そのページへのリンクも手動で書く
2021-02-15
23:12:54 PDFの中にテキストが含まれていたら、それを引用形式で書き出すようにした
2021-01-29
19:14:27 解像度を指定できるようにした
2021-01-25
23:42:21
更新・作成日時を秒単位に直して代入するようにした
一部の関数を別ページに切り出した
known issue
Gyazo accountと結びつかない?
認証情報を送っていないのだから、結びつかなくて当然
interface
画像ファイルとtextファイルがあるdirectoryで実行する
ファイル名は 1.png
/ 1.txt
にする
同階層に import.json
を作成する
実行コマンド
shdeno run --allow-read --allow-write --allow-net --location https://upload.gyazo.com --unstable --reload https://scrapbox.io/api/code/takker/PDFをOCRしてScrapboxに取り込むDeno_script/mod.ts convert basename access_token
dependencies
mod.tsimport {cac} from 'https://unpkg.com/cac/mod.ts';
import {execute} from './script.ts';
command line argumentsの設定
mod.tsconst cli = cac('convert-scrapbox-json');
cli.command('convert [pagename] [accessToken]', 'page name of each scrapbox page')
.action((pagename, accessToken) => {
if (typeof accessToken !== 'string') throw Error('Specify an Gyazo access token');
execute(pagename, accessToken);
});
cli.help();
cli.version('0.0.0');
cli.parse();
画像ファイルがあるdirectoryでupload処理を行う
画像ファイルとテキストファイルのpathを取得する
script.tsimport {expandGlobSync} from 'https://deno.land/std@0.87.0/fs/mod.ts';
// @deno-types='../Scrapbox書籍のformat/script.d.ts'
import {convert} from '../Scrapbox書籍のformat/script.js';
// @deno-types='../OCRしたテキストを整形/script.d.ts'
import {tidy} from '../OCRしたテキストを整形/script.js';
export async function execute(filename: string, accessToken: string) {
try {
const imageURLPenings = getGyazoURLs([...expandGlobSync(`*.png`)]
.flatMap(({path}) => {
const name = path.match(/\/([^\/]+)$/)?.[1];
return name ? [name] : [];
})
.sort((a,b) => parseInt(a) - parseInt(b)),
filename, {accessToken});
console.log(`Open OCR text files...`);
const ocrTexts = [...expandGlobSync(`*.txt`)]
.flatMap(({path}) => {
const name = path.match(/\/([^\/]+)$/)?.[1];
return name ? [name] : [];
})
.sort((a,b) => parseInt(a) - parseInt(b))
.map((path, i) => {
const text = Deno.readTextFileSync(path);
// OCRしたテキストは、事前に整形しておく
return tidy(text, {removeBracket: true})
.split(/\n/).flatMap(line => line.trim() === '' ? [] : [line]).join('\n');
});
console.log(`Opened ${ocrTexts.length} OCR text files.`);
const imageURLs = await imageURLPenings;
JSONデータを作る
script.ts const json = {
pages: imageURLs.map(({url, index}) => convert(filename, url, index, {
start: index === 1,
end: index === imageURLs.length,
text: ocrTexts[index - 1],
})),
};
JSONデータを保存する
script.ts const output = 'output.json';
console.log(`Writing to ${output}...`);
await Deno.writeTextFile(output, JSON.stringify(json));
console.log('Successfully finished.');
} catch(e) {
console.error(e);
}
}
画像ファイルを開いてGyazoにuploadし、URLを取得する
serverの負荷を減らすために、10枚ずつuploadする
script.ts// @deno-types='../uploadToGyazo/script.d.ts'
import {uploadToGyazo} from '../uploadToGyazo/script.js';
async function getGyazoURLs(pathes: string[], fileName: string, {accessToken}: {accessToken: string}) {
const URLs: {url: string, index: number}[] = [];
const chunk = 10;
let retryPathes: {path: string, index: number}[] = [];
const max = Math.floor(pathes.length / chunk) + 1;
for (let i = 0; i < max; i++) {
const pendings = pathes.slice(i * chunk, (i + 1) * chunk)
.map(async (path, j) => {
const index = i * chunk + j + 1;
try {
console.log(`[page ${index}] Open "${path}"...`);
const buffer = await Deno.readFile(path);
const image = new Blob([buffer], {type: 'image/png'});
console.log(`[page ${index}] Opened.`);
console.log(`[page ${index}] Uploading "${path}" to Gyazo...`);
const {permalink_url} = await uploadToGyazo(image, accessToken, {title: `${fileName} ${index}`});
console.log(`[page ${index}] Success.`, permalink_url);
URLs.push({url: permalink_url, index});
} catch(e) {
console.error(`[page ${index}] Faild to upload:`, e);
console.error(`[page ${index}] Retry later.`);
retryPathes.push({path, index});
}
});
await Promise.all(pendings);
}
// 送信に失敗した画像を送信し直す
while (retryPathes.length > 0) {
const {path, index} = retryPathes.pop()!;
try {
console.log(`[page ${index}] Open "${path}"...`);
const buffer = await Deno.readFile(path);
const image = new Blob([buffer], {type: 'image/png'});
console.log(`[page ${index}] Opened.`);
console.log(`[page ${index}] Uploading "${path}" to Gyazo...`);
const {permalink_url} = await uploadToGyazo(image, accessToken, {title: `${fileName} ${index}`});
console.log(`[page ${index}] Success.`, permalink_url);
URLs.push({url: permalink_url, index});
} catch(e) {
console.error(`[page ${index}] Faild to upload:`, e);
console.error(`[page ${index}] Retry later.`);
retryPathes.push({path, index});
}
}
return URLs;
}
test1.tsconst filename = Deno.args[0];
const buffer = await Deno.readFile(filename);
console.log(buffer);
2021-03-02 16:56:26 いや、読めてるな。
非同期にアクセスしたのが問題か?
2個づつアクセスしたらどうだろう?
test2.tsconst filenames = Deno.args;
const buffers = await Promise.all(filenames.map(filename => Deno.readFile(filename)));
console.log(buffers);
17:00:03 2個読めた
17:00:50 6個読めた
17:01:25 10個読めた!
とすると、同時に読み込んだのが原因ではない……
WSLのフルパス /mnt/c/...
を認識できなかったのかな?
では相対パスで読み込んでみよう。
17:12:32 大量にすべての画像ファイルを一気に読み込んでいたのが問題だった
あっという間に16GBのメモリを食い尽くしたから、おかしいと思ったんだ
17:22:16 どうやら数百個のファイル読み込みを一度に指示すると、途中までしか成功しないようだ時間がかかるようだ
text fileの方は、同期で読み込むようにしよう