- 複数キーワード対応(AND検索)とマッチ数を表示するようにしました。2021/8/1 ソース改定
- 新規追加されたファイルのみキャッシュに登録するオプションを追加しました。2021/8/2 ソース改定
ク****ドや味*素などいろんなサイトでレシピが公開されていますが、「今日は豚こまがあるから…」とグーグル先生に「豚こま レシピ」とか毎回検索してもいいんですが、気に入ったレシピはChromeからPDF形式でファイルに落として(印刷して)おいて、ちょくちょく参照しています。当然それが溜まってくるとタイトル(ファイル名)から見つけにくくなってくるので、これをなんとかしようと。
ライブラリのように数千もあるわけではないので、ディレクトリ指定で
os.walk()
してファイル名をパターンサーチしていけばいいか、ということで、引っかかったファイル名をSumatraPDFに食わせてそのままPDF表示、という形にしました。ついでに、「せっかくC/Migemoを使えるんだからローマ字でも検索できるようにしちゃえ」ということで組み合わせ、さらに「コンテンツ(レシピ内容)からも検索できるようにしたほうがいいよね」ということでpdfminerも組み合わせることにしました。
ところがPDFファイルが少なければ問題ないのですが、多くなってくると毎回pdfminerでテキスト抽出してパターンサーチというのはものすごく時間がかかります。そこで、一回pdfminerで
extract_text()
したらその結果をキャッシュしておけばいい、ということで、SQLite3を使ってキャッシュを行うようにしました。さらに、コンテンツを検索したいときには"-c"スイッチを指定して、デフォルトではコンテンツを検索しないようにしました。未キャッシュのファイルが多いときには"-c"を指定するとものすごく時間がかかりますが、一度キャッシュすれば楽ちんです。ほんとはタイムスタンプまで含めて登録しておいて、ファイル日付が違ってたらキャッシュし直しとかしてもいいんですが…。
手前味噌ながらこのユーティリティのいいところは、ただし、C/Migemoの仕様の関係で。複数キーワードも受け付けるようにしました。複数指定された場合には、AND検索(AとBを同時に含むもの)に絞り込みできます。たとえば
recipe tamago
とすると、「たまご」「タマゴ」「玉子」「卵」「Egg」などの表記の揺れをカバーしてくれることです(辞書による)。recipe tamago cabbage
などのように複数のキーワードを指定しても無駄なところがありますrecipe バジル トマト
とかrecipe 鶏肉 pi-man
とか。ちょっと長いですが、recipi.pyです。
- 実行にはC/Migemoとpdfminer、SumatraPDFがインストールされていることが必要です。
- 配置ディレクトリなどは適宜修正してください。
- recipe -h でオプション一覧が出ます。
#!python #!python # -*- coding: utf-8 -*- # vim:fenc=utf-8 ff=unix ft=python ts=4 sw=4 sts=4 si et fdm fdl=99: # vim:cinw=if,elif,else,for,while,try,except,finally,def,class: # # copyright (c) K.Fujii 2021 # created : Jul 26, 2021 # last modified: Aug 2, 2021 # Changelog: # Jul 26, 2021: migemoを利用してローマ字でも検索するように変更 # : pdfminerでPDFをテキスト化し、コンテンツについても検索できるよう変更 # Jul 27, 2021: SQLite3でテキスト化したコンテンツをキャッシュできるように変更 # : 複数キーワード対応 # Jul 28, 2021: キャッシュをドロップして再生成するオプションを追加 # Aug 01, 2021: 一致した件数を表示 # Aug 02, 2021: 新規ファイルのみキャッシュ追加を行うオプションを追加 """ recipi.py: レシピのウェブクリップPDFから入力されたキーワードに一致するものを検索し、SumatraPDFで表示する キーワード検索にはC/Migemoを利用し、かな漢字変換をしなくても検索できる 対象ディレクトリは recipe_dir に指定する TODO: SumatraPDFで同じファイルを2重に表示しないようにする? TODO: 候補数が多いときには選べるようにする? """ import argparse import io import logging import os import subprocess import re import sqlite3 import sys import cmigemo from pdfminer.high_level import extract_text from pdfminer.pdfparser import PDFSyntaxError migemo_dict = "c:/Apps/bin/dict/base-dict" recipe_dirs = "p:/料理レシピ" cache_db = "p:/料理レシピ/recipe.db" PDF_viewer = [ "SumatraPDF", "-reuse-instance", ] def compile_pattern(S): """return compiled pattern""" logger.debug(S) migemo = cmigemo.Migemo(migemo_dict) ret = migemo.query(S) logger.debug("regex=%s", ret) return re.compile(ret, re.IGNORECASE) def list_files(directory): """list all the files under specified directory""" ret = [] logger.debug("recipe_dirs: %s", directory) for root, dirs, files in os.walk(directory): t = [os.path.join(root, f) for f in files] ret.extend(t) logger.debug(ret) return ret def extract_content(f): try: ret = repr(extract_text(f)) except PDFSyntaxError: ret = None return ret def search_in_dirs(patterns, files, contents): """search pattern in files""" recipe_files = [] if contents: conn = sqlite3.connect(cache_db) # テーブルがなければ作成する conn.execute("CREATE TABLE IF NOT EXISTS recipes (fname TEXT, contents TEXT)") conn.row_factory = sqlite3.Row cur = conn.cursor() pat = compile_pattern(patterns[0]) for f in files: if os.path.splitext(f)[1] == ".pdf": if pat.search(f): # ファイル名のみの検索 logger.debug("matched %s", f) recipe_files.append(f) elif contents: # PDFコンテンツも検索 cur.execute( "SELECT * FROM recipes WHERE fname LIKE ?", ("%" + f + "%",) ) data = cur.fetchone() if data: if pat.search(data["contents"]): recipe_files.append(f) else: # キャッシュされていない場合にはキャッシュに登録 c = repr(extract_content(f)) if c: logger.debug("INSERT: %s", f) logger.debug("CONTENTS: %s", c) cur.execute( "INSERT into recipes VALUES (?, ?)", (f, c), ) conn.commit() if pat.search(c): recipe_files.append(f) # patternsの要素が1のときにエラーになる? for pattern in patterns[1:]: pat = compile_pattern(pattern) for f in recipe_files[:]: logger.debug("file: %s", f) if not pat.search(f): if contents: # PDFコンテンツも検索 cur.execute( "SELECT * FROM recipes WHERE fname LIKE ?", ("%" + f + "%",) ) data = cur.fetchone() if data: if not pat.search(data["contents"]): logger.debug("unmatched %s", f) recipe_files.remove(f) else: recipe_files.remove(f) if contents: conn.close() if len(recipe_files): view_cmd = PDF_viewer + recipe_files print("matched {0} files with {1}".format(len(recipe_files), patterns)) subprocess.Popen(view_cmd) else: print("No file matched :", patterns) def generate_cache(): """新規追加されたファイルをキャッシュに追加する""" files = list_files(recipe_dirs) conn = sqlite3.connect(cache_db) # テーブルがなければ作成する conn.execute("CREATE TABLE IF NOT EXISTS recipes (fname TEXT, contents TEXT)") conn.row_factory = sqlite3.Row cur = conn.cursor() for f in files: # キャッシュされていない場合にはキャッシュに登録 cur.execute("SELECT * FROM recipes WHERE fname LIKE ?", ("%" + f + "%",)) data = cur.fetchone() if not data: c = repr(extract_content(f)) logger.debug("INSERT: %s", f) logger.debug("CONTENTS: %s", c) cur.execute( "INSERT into recipes VALUES (?, ?)", (f, c), ) conn.commit() conn.close() def regenerate_cache(): """テーブルをドロップしてキャッシュを生成し直す""" files = list_files(recipe_dirs) conn = sqlite3.connect(cache_db) conn.execute("DROP TABLE recipes") conn.execute("CREATE TABLE IF NOT EXISTS recipes (fname TEXT, contents TEXT)") cur = conn.cursor() for f in files: if os.path.splitext(f)[1] == ".pdf": c = repr(extract_content(f)) cur.execute("INSERT into recipes VALUES (?, ?)", (f, c)) conn.commit() conn.close() if __name__ == "__main__": sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") logger = logging.getLogger(__name__) handler = logging.StreamHandler() formatter = logging.Formatter("%(asctime)s %(name)-12s %(levelname)-8s %(message)s") handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(logging.INFO) logger.propagate = False parser = argparse.ArgumentParser( description="ウェブクリップから料理レシピを検索し、PDFビューワで表示する", # formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument( "pattern", metavar="pattern(s)", type=str, nargs="*", help="pattern(s) for search. accept REGEX, C/Migemo", ) parser.add_argument( "-c", "--contents", metavar="contents", action="store_const", const=1, default=0, help="Search keyword(s) in PDF contents as well as file names", ) parser.add_argument( "-g", "--generate", metavar="generate", action="store_const", const=1, default=0, help="Generate cache with PDF contents for new file(s)", ) parser.add_argument( "-r", "--recache", metavar="recache", action="store_const", const=1, default=0, help="Clear and Re-generate cache with PDF contents", ) parser.add_argument( "-d", "--debug", metavar="debug", action="store_const", const=1, default=0, help="Print Debug information", ) args = parser.parse_args() if args.debug == 1: logger.setLevel(logging.DEBUG) if args.generate: generate_cache() sys.exit() elif args.recache: regenerate_cache() sys.exit() elif not args.pattern: parser.print_help() sys.exit() patterns = args.pattern search_in_dirs(patterns, list_files(recipe_dirs), args.contents)
0 件のコメント:
コメントを投稿