2025/04/13 シラバスをJSON化するPythonスクリプトの実装
t6o_o6t.icon
✅ PDFをJSON化するスクリプトを実装中
PyMuPDFを使って、PDF内のすべての表の行を横断的にイテレートできるようにした
だいたいのケースで上手くいくのだが、PyMuPDFが表以外の部分を表だと思い込むケースがある 電子光工学科の選択科目のPDFで、基礎レーザー工学の下にある「2024年度 シラバス」をテーブルだと判断してしまっているようだ
列数が想定と違うテーブルは処理の対象から外すと良いかも?
これはうまくいかなかった。「2024年度 シラバス」の部分が、本来のテーブルに加わってしまっていたため
矩形を指定してみる
まずはページ全体の矩形を取得する
Page.rect
上から6%くらいの部分を除外するとうまくいった
Rect(0, page.rect.height * 0.06, page.rect.br)
例外的な科目
統計解析
「単位数」などの文字列が横に広がっている。
したがって、項目名がつねに同一の文字列であるという仮定をしてはならない。
インターンシップ
授業回数が-となっている。
したがって、授業回数の内容が数字であることを先に確認しなければならない。
学科などの取り扱い
必修・選択の区別は表に記載されている
しかし、学科は記載がない
ひとまず学科をEnum化すると良いか...
一般教育科目
体育科目
外国語科目
応用化学生物学科
電子光工学科
情報システム工学科
教職科目
どのように実装する?
学科の科目と、教職科目に関しては、ファイル名に学科名が入っていることを条件に判定すればOK
一般教育科目は、共通教育科目がついていて、かつ外国語および選択という言葉が入っていないことが条件
しかし、この方法では、体育科目と外国語科目の区別がつかない...
ファイル名を
同名の科目はマージしたほうがよいのでは?
科目に対して学科は1対多の関係にあるのでは?
💡先に開講科目一覧をパースしておけば良い
体育科目と外国語科目は、その表に類似文字列が含まれているかどうかで判定する
ただ、学科の科目は、同名の科目が複数学科や学年にまたがって存在することがあるので、通常通りファイル名で判断する
classify.py
FilenameHint
Enum(Common, PEOrFL, ChemBio, Photon, InfoSys, Teacher, Unknown)
static from_path(string): Self
PEPossibilityDetector
static from_cells(cells): Self
is_possible(title): bool
SubjectCategory
Enum(Common, PE, FL, ChemBio, Photon, InfoSys, Teacher, Unknown)
実際にJSONに含めるデータはこれである
SubjectCategoryClassifier
constructor(pe_possibility_detector)
classify(title, filename_hint): SubjectCategory
parse_chunkの引数は?
chunkだけではSubjectを作れないことが分かったので、構造を見直したほうがよいのでは?
Filenameごとにインスタンスを生成する
code:py
class Parser():
def __init__(self, *, pe_possibility_detector, filename_hint_classifier):
...
def parse(filename, chunk):
is_pe_possibly = self.pe_possibility_detector.is_pe_possibly(...)
filename_hint = self.filename_hint_classifier.classify(...)
subject_category = filename_hint.decide_by_pe_possibility(is_pe_possibly)
pe_possibility_detectorの渡し方が問題。
各Filenameごとにインスタンスを生成する必要はない。
単一のParserインスタンスを使いまわすのが最適である。
公開情報ページ用のモジュールを作る
extract_syllabus_paths関数を作る
シラバス処理とは分ける
行の抽出処理は、ページごとに並列化するのが適切なのか?
そもそも、並列化して速度が向上していたのか?
並列化したことによって、コードが複雑になっていると感じる
chunkやfilenameはページを横断して、ファイルごとに考える必要がある
しかし、抽出後の行情報からは、ファイルという概念が抜け落ちている
シンプルさと速度のトレードオフが取れているか怪しくなってきた
ページごとに並列化
code:log
2025-05-04 08:32:49.316 | INFO | __main__:<module>:50 - Counting syllabus pages
2025-05-04 08:32:49.317 | INFO | __main__:<module>:53 - Extracting rows from PDF
2025-05-04 08:33:21.543 | INFO | __main__:<module>:81 - Complete!
32秒
ファイルごとに並列化する?
code:py
pool.execute(extract_rows, files)
code:log
2025-05-04 09:49:41.201 | INFO | __main__:<module>:52 - Counting syllabus pages
2025-05-04 09:49:41.219 | INFO | __main__:<module>:56 - Extracting rows from PDF
2025-05-04 09:50:10.581 | INFO | __main__:<module>:71 - Parsing rows as subject
29秒
各ファイルの取り扱いは直列で、行の抽出処理はページごとに並列化する?
code:py
for content, page_count in zip(contents, page_counts):
worker_args = ((content, i) for i in range(page_count))
logger.debug(list(executor.map(pdf.extract_rows, *zip(*worker_args))))
code:log
2025-05-04 08:37:04.938 | INFO | __main__:<module>:50 - Counting syllabus pages
2025-05-04 08:37:04.938 | INFO | __main__:<module>:53 - Extracting rows from PDF
2025-05-04 08:37:38.389 | INFO | __main__:<module>:84 - Complete!
34秒
軽めのタスクが大量にある状況なので、あまり並列化しても効果は出ない?
一切並列化しない
code:log
2025-05-04 09:32:23.299 | INFO | __main__:<module>:52 - Counting syllabus pages
2025-05-04 09:32:23.324 | INFO | __main__:<module>:56 - Extracting rows from PDF
2025-05-04 09:33:26.064 | INFO | __main__:<module>:89 - Complete!
行の抽出処理は63秒かかっているので、並列化には意味がある
syllabus contentsのfetchおよびpage countを並列化する効果について
並列化しない場合
code:log
2025-05-04 09:40:01.326 | INFO | parser.navigation:fetch_navigation:21 - Fetching syllabus URLs
2025-05-04 09:40:02.933 | INFO | __main__:<module>:48 - Fetching syllabus contents
2025-05-04 09:40:10.695 | INFO | __main__:<module>:52 - Counting syllabus pages
2025-05-04 09:40:10.720 | INFO | __main__:<module>:56 - Extracting rows from PDF
並列化した場合
code:log
2025-05-04 09:38:17.292 | INFO | parser.navigation:fetch_navigation:21 - Fetching syllabus URLs
2025-05-04 09:38:18.829 | INFO | __main__:<module>:48 - Fetching syllabus contents
2025-05-04 09:38:21.132 | INFO | __main__:<module>:52 - Counting syllabus pages
2025-05-04 09:38:21.154 | INFO | __main__:<module>:56 - Extracting rows from PDF
syllabus contentsのfetchは、並列化していない場合は8秒程度かかる。並列化すると3秒程度に短縮できる。
page countは、並列化してもしなくても、0.1秒以下の時間しかかからないので無視してよい。
プロファイリング
https://scrapbox.io/files/68172daf7e36cfcc38b343c4.png
行の抽出処理についてもプロファイルを実施
https://scrapbox.io/files/681732459b32b4056def8a19.png
pymupdf.Document()やload_pageはボトルネックにはなっていないことが確認できた!
マルチプロセスのオーバーヘッドが気になる
ページごとに並列化する場合、ページ数と同じ回数だけプロセスにファイル内容を転送することになる
概算して、500回程度は転送が行われていると考えて良さそう
一方、ファイルごとに並列化する場合、ファイル内容の転送は計10回となる
この転送量の差が、影響を与えているのかを確認したい
そのために、並列化の単位をファイルごとにする。
確認した
実行時間の差が大きくならなかったので、転送によるオーバーヘッドが根本的な原因とはいえない
ひとまず動作は確認できた
ファイル構成は適切か?
pdf.pyなど、分け方が不適切になっている箇所がある
出力フォーマット、Modelの定義は適切か?
基本的にEnumのAutoを使っていくようにしたい
✅GitHub上へのJSONファイルのホスト / GitHub Actionsによる更新
distディレクトリを作って、そこにJSONファイルを配置するようにする。
GitHub Actionsでは、現在のスクリプトをベースに、checkoutアクションとCreate Pull Requestを使って更新用のPRを作成する。
メンテナは、Actionsが発行したPRをMergeするだけでよい。
OK!t6o_o6t.icon