Word文書を解析して、写真または大学院の就業日に関するストーリーを作成する

写真の形でワード文書からデータを抽出する方法を紹介したいと思います。 おそらく、提示されたアイデアは原始的であり、誰かにとって明らかです。 しかし、私は通常の解決策に到達する前に眠れない夜を数回過ごす必要がありました。 だから、私は始めています。



2015年の始まりでした。 冬 天気が良かったので嬉しくて、やっと大学を卒業できると喜んで思いました(嘘をついて、今は大学院に行きます) 私は最近卒業証書を終えたので、さらに幸せでした。 しかし、すぐに、人間の性質によって、静けさの状態は徐々に退屈に置き換えられ始めました。 そして、ここでは、あたかも意図的に、沈黙が電話に置き換えられました。



「こんにちは、こんにちは、元気ですか?」友人の声が聞こえました。



直観は、会話が「tyzhprogramist」のトピックに関するものである可能性が最も高いとすぐに判断しました。 そうだった。 退屈な表情で、最初は知り合いの困難な時期や他のすべてについて聞いていましたが、彼女の最後のリクエストに興味がありました。



「卒業証書を手伝ってくれませんか? 一般的に、数学でウェブサイトシミュレータを作成する必要があります」と彼女は言いました。



面白かった。 複雑なフロントエンドの開発に興味を持っています。 すぐにリクエストを承認しました。



シミュレーターの実装には、合計で2日もかかりませんでした。 サイトシミュレーターを使用すると、高等数学のテストを受けて理論を見ることができます。 テストは2つのモードで実行できます。回答を強調表示するトレーニングモードと、最後に出力するテストモードです。 実装はReactJSとBootstrapで行われ、プロセス自体はとても楽しかったです。 しかし、それはほんの始まりに過ぎませんでした。 テストの質問のデータベースに記入する方法が必要でしたが、それは決して既製の順序付けされたデータの形ではありませんでした。



問題の声明



仕事の結果に満足して探している間に、友人が再び電話をかけました。 彼女は、* .docファイルに質問がある小さなアーカイバをメールに送ったことを私に通知しました。 「もしあれば、私はデータベースに質問と回答を投げることができます」と彼女は付け加えました。



テストの質問データベースに自分で記入する必要があるとは思わなかったので、これは私の気分を少し台無しにしました。



わかった 私はGMailを開いてここに行きました:







そして、各ファイルには、フォームに〜50個のテストタスクがあります。







まあ、これをすべて手動でデータベースに入力できるとは考えられません。 ちなみに、データベース内の各テストタスクは、1つの絵の質問、5つの絵の答え、難易度(A、B、C)および正解の数(1〜5)の形式で保存されました。 イライラして、私は長い間この質問を延期することにしました、良い時間は十分でした。 しかし、数日後、友人からの別のメッセージがメールに落ち、さらに別のメッセージが...結果として、高等数学に関する4つのセクションがあり、各セクションは14から23のセクションで構成され、各セクションには約30から100のテストタスクが含まれていました。 そして、最終的に、これらすべてをデータベースに手動で入力することは間違いなく失敗することを確認しました。



ところで、データベースについてです。 これは、セクション、サブセクション、テスト質問の3つのテーブルを持つMySQLです。 質問と5つの回答の写真は、データベースのBLOB列に直接保存されます。 これらの写真はたくさんあり、しかも少し重さがありますので、とても便利なようです。 そして、それらはすべて、他のデータとともに1か所に保存されます。



それで、何が必要でしたか? 最良の場合、テストタスクのすべてのワードファイルを含むフォルダーからデータベース内の対応するレコードを準備する必要がありました。これは実際に最終結果で取得されました。 主なものに興味があります:写真を直接抽出します。



入力 :テストタスクを含むワードファイル。

出力(例) :PNG画像があるフォルダー。タスクの名前は1.pngで、回答の名前は1.1.png、1.2.png、1.3.png、1.4.png、1.5.png、および回答ファイルです。 txt。i番目の行には、i番目のタスクの正解に対応する1〜5の数字が含まれます。



実装



Qt Creatorが大好きです。 そして、なぜ私は彼を愛すべきなのでしょうか? おそらく大学で私たちはそのために正確に訓練されているため、私自身は知りません。 そして、それを使用している間、私はある種の静かな喜びを感じます。 まあ、一般的に、あなたは私がパーサープログラムを書き始めたものを理解しています。



最初は、そこからデータを何らかの形で引き出すために、このWordとどのように対話するのか疑問に思いました。 あらゆる種類の恐ろしい考えが思い浮かび、その後の処理でワードファイルをHTMLに変換するようになりました。 しかし、グーグルはすぐに適切な道を私に送り、VBA言語に関する情報を提供しました。 私はすぐに豊富な既製の機能に感銘を受けました。その間、どのような段落、位置などを学びましたが、その間、単語文書ツリーの構造の複雑さのためにすごいです。



しかし、テキストを画像に変換する方法がまだ理解できていなかったのでがっかりしました。 最初に、必要なテキストを引き出した後、text2pngのようなものを使用したかったのです。 しかし、数式と写真はどうですか? VBAには組み込み関数はありませんでした。 ある時点で、ドキュメント内の単語ドキュメントに、以前は写真の形でExcelセルを挿入していたように思われるという思いが誤ってフラッシュしました。 でした! これは「特別な挿入」と呼ばれ、ドキュメントの任意の部分を画像として挿入できます。 クリップボードにドキュメントの一部を入力したとします。ドキュメントは画像として保存する必要があります。 しかし、この画像をディスクに保存する方法は? グーグルは解決策を見つけるのにも役立ちました。 以下のコードセクションは、クリップボードの内容をユニバーサルEMFベクトルファイルとしてディスクに保存します。



#include <windows.h> void clipboardDataToEmfFile(QString fileName){ OpenClipboard(0); GetEnhMetaFileBits((HENHMETAFILE)GetClipboardData(14),0,0); HENHMETAFILE returnValue = CopyEnhMetaFileA((HENHMETAFILE)GetClipboardData(14), QDir::toNativeSeparators(fileName).toStdString().c_str()); EmptyClipboard(); CloseClipboard(); DeleteEnhMetaFile(returnValue); }
      
      





素晴らしい。 しかし、このEMFはどのような動物ですか? PNGに変換する必要がありました。 私は画像コンバーターを探し始めました。 束を経て、適切なものが見つかりませんでした。 そしてここでも(直観を信じる人はいますか?)Golden Softwareのディスクから学年に楽しみのために入れた賢い画像ビューアが私の頭に浮かび始めました。 しかし、それはコンバーターではなかったように。 ただし、確認する必要がありました。 ある種の「Ifran」または「Irfan」が私の頭の中で回転していました。一般に、プログラムが見つかりました。 無料、バッチ画像処理機能付き、コマンドラインをサポート! そして最も重要なことは、EMFをサポートしていることです。 それが私たちに必要なものでした。 必要なDLLとini-parametersファイルを含むIrfanView実行可能ファイルは、コンパイルされたプログラムと同じフォルダーにあり(これがライセンスに違反しないことを望みます)、このような関数で使用されます。



 void convertEmfsToPng(QString inFolder, QString outFolder){ QProcess proc; QString exeStr = "\"" + QDir::toNativeSeparators(QDir::currentPath()+"/i_view32.exe") + "\""; QString inFilesStr = "\"" + QDir::toNativeSeparators(inFolder + "*.emf") + "\""; QString outFilesStr = "\"" + QDir::toNativeSeparators(outFolder + "*.png") + "\""; QString iniFolderStr = "\"" + QDir::toNativeSeparators(QDir::currentPath()) + "\""; proc.start(exeStr + " " + inFilesStr + " /advancedbatch /ini=" + iniFolderStr + " /convert=" + outFilesStr); proc.waitForFinished(30*60*1000); }
      
      





これで、必要な部分をワード文書からバッファーにコピーすることができます。 これを行うには、タスク、回答、正解の数、および複雑さのレベルで、ソーステキストを個別のブロックに分割するためのアルゴリズムを考え出す必要があります。



実装の最初の試みは次のとおりでした。 ソースドキュメントを取得し、フォームのテキスト([1-5])\)を\ n $ 1 \)に置き換えます。 各回答の開始前に、改行を追加します。 VBAでは、置換文字列の記述方法が異なりますが、覚えていません。 ドキュメント設定で、ページ幅を最大に設定し、ドキュメント全体のフォントを減らします。 その結果、ドキュメントでは、各タスクが正確に8行を占めることがわかりました。



繰り返しタスクがどのように見えるか




さて、この処理の後、ドキュメント行のコレクションの配列を通過し、カウンターiを開始し、i%8に応じて、タスク/回答の画像を保存するか、複雑さのレベルで正解の数を抽出するだけです。



しかし、それは適合しませんでした。 長いタスクは非難することです。それは、1行で書かれているので、ひどく浅く見えて、常に収まるとは限りません。 さらに、テキスト「1)」を置き換えると、応答番号以外の場所に影響する場合があります。 結果に悲しんで、私は再びこの場合に何ができるかについて考え始めました。 そして、有限状態マシンについて思い出しました。 状態を覚え、文字入力を思い出しました。 パーサーを呼び出しました。 おそらくこれは他の人にとって明らかな解決策だったかもしれませんが、複雑なアルゴリズムとはほど遠い人として、私は自分のアイデアに非常に満足していました。



それでは、ステートマシンに基づいてパーサーコードを記述して試してみましょう。 7つの州があります。



次の状態の開始条件を使用して実装します。 パーサーの最初のバージョンをテストした後、すべてがうまくいきました。 写真は、単語文書自体のように、美しく、大きく取得されました。 しかし、ここで...時々、横棒が現れました。たとえば、ある写真では、次のブロックまで余分な作品が撮影されました。 そのため、パーサーは誤って認識しました。 問題は何ですか? すべてがシンプルであることが判明しました-ワード文書のタスクは手動で入力されたため、たとえば次のような人的要因がありました。



それは悪夢でした。 幸いなことに、主な間違いはタスクの数を書くことであり、パーサーによって何らかの形で考慮されました。 画像を抽出した後の残りのエラーは、最大サイズと最小サイズの画像をすばやく確認し、続いて修正と再抽出を行うことで検出されました。



パーサーの最後の部分は以下のコードです。 彼はひどいです、厳しく判断しないでください。 VBAオブジェクトを格納するには、QAxObjectが使用されます。



変数名、マシンの状態、使用される追加機能の説明
  • status-マシンの状態:

    • -3-タスク間
    • -2-ジョブ番号内
    • -1-タスク内
    • 0-回答の後に
    • 1-内部回答1
    • 2-内部回答2
    • 3-内部回答3
    • 4-内部回答4
    • 5-内部回答5


  • startind-現在のブロックの開始位置(タスク、回答、正解の番号と難易度の行)
  • n-タスクのシリアル番号
  • nstr-先行ゼロが3桁までのジョブシーケンス番号文字列
  • str-現在のブロックの現在位置までの文字列
  • lineStart、lineEnd-現在の段落の始まりと終わりの位置番号
  • lines-文書の段落コレクションのオブジェクト
  • tline-現在の段落のオブジェクト
  • line-現在の段落の範囲オブジェクト
  • ipar-現在の段落番号
  • tmpObj-現在のキャラクターの範囲オブジェクト
  • currChar-現在のキャラクター
  • outdir-出力画像フォルダーのパス行
  • getAnswerLine(QString)関数-2つの数字の文字列を返します:難易度レベル(1-3)と正解の番号(1-5)、たとえば24-これは難易度レベルBおよび番号4の下の正解のタスクです
  • rangeToEmfFile関数(QString fname、int start、int end、QAxObject * activeDoc)-activeDocドキュメントの開始位置と終了位置の間のドキュメントをfnameという名前のEMFファイルとして保存します


ひどい長いコード。
 QAxObject *activeDoc = wordApp->querySubObject("ActiveDocument"); int status = -3; int startind = 0; int n=0; QString nstr; QString str = ""; int lineStart, lineEnd; QAxObject *lines = activeDoc->querySubObject("Paragraphs"); if (onlyAsnwers) for (int ipar = 1; ipar <= lines->property("Count").toInt(); ipar++){ QAxObject *tline = lines->querySubObject("Item(QVariant)", ipar); QAxObject *line = tline->querySubObject("Range"); QString str = line->property("Text").toString(); line->clear(); delete line; tline->clear(); delete tline; int ind = str.indexOf(":"); if (ind != -1){ str = str.mid(ind+6); answersTxt << getAnswerLine(str); } } else for (int ipar = 1; ipar <= lines->property("Count").toInt(); ipar++){ QAxObject *tline = lines->querySubObject("Item(QVariant)", ipar); QAxObject *line = tline->querySubObject("Range"); lineStart = line->property("Start").toInt(); lineEnd = line->property("End").toInt(); line->clear(); delete line; tline->clear(); delete tline; str = ""; for (int j=lineStart; j<lineEnd; j++){ QAxObject *tmpObj = activeDoc->querySubObject("Range(QVariant,QVariant)", j, j+1); QString currChar = tmpObj->property("Text").toString(); tmpObj->clear(); delete tmpObj; str += currChar; switch (status){ case -3: if (j>=4 && str.right(5) == ""){ status = -2; startind = j+1; } break; case -2: if (str.right(6) == ""){ n++; nstr = QString::number(n); while (nstr.length() < 3) nstr = "0" + nstr; status = -1; QAxObject *tmpObj = activeDoc->querySubObject("Range(QVariant,QVariant)", startind, j-6); QString tmp = tmpObj->property("Text").toString(); tmpObj->clear(); delete tmpObj; answersTxt << getAnswerLine(tmp); startind = j+2; } else if (str.right(7) == ""){ n++; nstr = QString::number(n); while (nstr.length() < 3) nstr = "0" + nstr; status = -1; QAxObject *tmpObj = activeDoc->querySubObject("Range(QVariant,QVariant)", startind, j-7); QString tmp = tmpObj->property("Text").toString(); tmpObj->clear(); delete tmpObj; answersTxt << getAnswerLine(tmp); startind = j+2; } break; case -1: if (str.right(7) == ":"){ status = 0; rangeToEmfFile(outdir+nstr+".emf", startind, j-7, activeDoc); startind = j+1; } else if (str.right(6) == ":"){ status = 0; rangeToEmfFile(outdir+nstr+".emf", startind, j-6, activeDoc); startind = j+1; } break; case 0: if (str.right(2) == "1)" || str.right(3) == "1 )"){ status = 1; startind = j+2; } break; case 1: if (str.right(2) == "2)"){ rangeToEmfFile(outdir+nstr+".1.emf", startind, j-2, activeDoc); status = 2; startind = j+2; } else if (str.right(3) == "2 )"){ rangeToEmfFile(outdir+nstr+".1.emf", startind, j-3, activeDoc); status = 2; startind = j+2; } break; case 2: if (str.right(2) == "3)"){ rangeToEmfFile(outdir+nstr+".2.emf", startind, j-2, activeDoc); status = 3; startind = j+2; } else if (str.right(3) == "3 )"){ rangeToEmfFile(outdir+nstr+".2.emf", startind, j-3, activeDoc); status = 3; startind = j+2; } break; case 3: if (str.right(2) == "4)"){ rangeToEmfFile(outdir+nstr+".3.emf", startind, j-2, activeDoc); status = 4; startind = j+2; } else if (str.right(3) == "4 )"){ rangeToEmfFile(outdir+nstr+".3.emf", startind, j-3, activeDoc); status = 4; startind = j+2; } break; case 4: if (str.right(2) == "5)"){ rangeToEmfFile(outdir+nstr+".4.emf", startind, j-2, activeDoc); status = 5; startind = j+2; } else if (str.right(3) == "5 )"){ rangeToEmfFile(outdir+nstr+".4.emf", startind, j-3, activeDoc); status = 5; startind = j+2; } break; case 5: if (j>=4 && str.right(5) == ""){ rangeToEmfFile(outdir+nstr+".5.emf", startind, j-5, activeDoc); status = -2; str = ""; } else if (lineEnd-lineStart < 2){ rangeToEmfFile(outdir+nstr+".5.emf", startind, j, activeDoc); status = -3; } break; } } if (status == 5) rangeToEmfFile(outdir+nstr+".5.emf", startind, lineEnd, activeDoc); } lines->clear(); delete lines; activeDoc->clear(); delete activeDoc;
      
      







上記のコードのロジックは、上記のものとわずかに異なります。 彼女は段落区切りも使用します。 しかし、これは主要なアイデアを大きく変えるものではありません。



これが、この言葉を「打ち負かす」ことがわかった方法です!



おわりに



その結果、約4,000個のすべてのタスクが抽出され、必要なパーサーシェルが作成されました。 リモートデータベースにタスクをダウンロードして管理するためのプログラムも作成されました。 料金は受領され、彼女の卒業証書は完全に保護され、私の卒業証書も完全に保護されます。



ご清聴ありがとうございました。この投稿が同様の問題を抱えている人に役立つことを願っています。 それとも誰かがより良い実装を知っているのでしょうか?



更新:

結果のいくつかの写真











All Articles