ローカル環境で使用できる自分用のメモアプリの作成方法と、全文検索機能の実装方法について紹介します。
コードはGitHubに載せているので、ダウンロード可能です。
アプリを作成した経緯
私は普段のメモはNotionに全て書いています。
Notionは本当に便利で、できれば全てのメモをNotionに書きたいのですが、仕事関連の内容はセキュリティ上、Notionに書くことはできません。
よって仕事関連のメモはエクセルに書いてローカルフォルダに保存しているのですが、ファイルが多くなったりフォルダ階層が深くなったりして、目的の内容を探すのがとても大変になりました。
そこで、ローカル環境で使用できる自分用のメモアプリを作成することにしました。
特徴としては、対象のフォルダ内のマークダウンファイルを全文検索できます。
メモアプリのデモ
下記の用に、ブラウザ上で記事を見ることができます。
例えば、下記の目次ページで「吾輩は猫である」をクリックすれば、「吾輩は猫 である」の内容が書いたページへ遷移します。
検索機能のデモは以下です。
メモアプリの内容
メモアプリは3つの種類のファイルで構成しています。
記事ページ、目次ページ、検索ページです。
メモアプリのファイル構成
メモアプリは以下のようなファイル構成となっています。
メモアプリ格納フォルダ
├── article
│ ├── sample1.md
│ ├── sample2.md
│ ├── sample3.md
│ └── sample4.md
├── templates
│ ├── base.html
│ └── index.html
│── index.md
└── md_search.py
記事ページ
記事ページはマークダウンで作成し、全てarticleフォルダに格納します。
上記の例でいうと、sample1.md
などです。
アプリと言っていますが、記事の作成はVS Codeなどのエディタを使用してマークダウンで書きます。
ただし、作成したファイルをarticle
フォルダに格納しておけば、全文検索で目的の資料を簡単に検索することができます。
目次ページ
目次ページも記事ページと同じようにマークダウンで作成します。
上記の例でいうと、index.md
です。
このindex.md
をトップページとしてブラウザでブックマークしておけば、アプリやwebページのトップページのように使うことができます。
Chrome拡張機能が必要
マークダウンファイルをブラウザで見るために以下の拡張機能を入れる必要があります。
オプションでデザインを変えれたり、cssを適用することもできるので非常に便利です。
また、ローカルファイルリンクをクリックしてページ遷移するためには下記の拡張機能を入れる必要があります。
検索ページ
検索機能はPythonで実装しています。
検索機能にはbeautiful soup4
を使っています。
webアプリとしての実装はflask
を使っています。
検索ページの実装
コードはGitHubに置いているので、コード全体はGitHubで見てください。
ここからは、コードのポイントをピックアップして説明します。
ライブラリのインストール
まず必要なライブラリをインストールします。
- bs4(スクレイピング用)
- markdown2(bs4でスクレイピングするためにmd形式をhtml形式に変換)
- flask(webアプリ作成)
pip install beautifulsoup4
pip install markdown2
pip install Flask
検索対象
検索対象は以下の構成中のarticleのフォルダ直下のmdファイルです。
逆に言えば、全文検索したいドキュメントはマークダウン形式でarticleフォルダに格納する必要があります(変更いたり、複数フォルダ指定することは可能です。)。
メモアプリ格納フォルダ
├── article
│ ├── sample1.md
│ ├── sample2.md
│ ├── sample3.md
│ └── sample4.md
├── templates
│ ├── base.html
│ └── index.html
│── index.md
└── md_search.py
検索用のpythonファイル
まず、検索対象となるフォルダのパスを指定します。
フォルダは一つではなく、複数指定することもできます。
ただしパスの指定は絶対パスで指定してください。
相対パスでも検索は可能ですが、相対パスではブラウザで検索結果のリンク先を見るときに正常にページが表示できません。
path = "#" # mdファイルの格納場所を絶対パスで指定
後で説明しますが、検索用のキーワードがform
を介してPOST
で渡ってくるので、そのキーワードを使って全文検索します。
キーワードを正規表現で検索できるように処理しておきます。
search_key = request.form.get('search_key')
search = re.compile(search_key)
まず、検索対象フォルダからmdファイルのみ抽出します。
for file in os.listdir(path): # 指定したフォルダからmdファイルを検索 base, ext = os.path.splitext(file) if ext == '.md': print(file) f = open(path + file, encoding="utf-8_sig") md = f.read()
md形式をhtml形式に変換します。
ライブラリのmarkdown2を使用します。
htmlconv = markdown2.Markdown().convert(md) # markdown形式をhtml形式に変換
beautiful soupで全文検索するために、bodyタグを無理やり付けます。
html = "<body>" + htmlconv + "</body>" # bodyタグを付ける。検索対象をbodyタグ内と指定するため。
beautiful soup でスクレイピングを行い、検索キーワードが含まれている場合はファイル名とパスと本文をリストに追加します。
検索結果に表示する本文は、検索キーワードが最初に出現する位置から50文字前を表示開始位置としています。
ただし、50文字前が本文の最初より前となった場合は、本文の最初を表示開始位置とします。
表示終了位置は、検索キーワードが最初に出現する位置から300文字後としています。
elems = soup.find_all(text = search)
judge = bool(elems)
if judge: # 検索対象のキーワードがある場合は、検索結果を表示するための配列を作成 file_name = file url = path + file contents = soup.find('body').text # 検索文字の位置を特定して、検索文字の50文字前から300文字後を表示。 m = re.search(search, contents) s = m.start()-50 if (m.start()-50) >= 0 else 0 # 検索文字が元から50文字以内の場合は0文字から表示する。 contents = soup.find('body').text[s:s+300].replace( '\n' , '' ) + "..." list = {"file_name":file_name, "url":url, "contents":contents} result.append(list)
webページ用のテンプレートhtmlファイル
まず、表示のベースとなるテンプレート用のhtmlファイルを準備します。
<!DOCTYPE html>
<html lang="ja"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous" /> <title>検索画面</title> {% block head %}<style>span.mark { background-color: yellow; } body {background-color: #F8F8F8;}/* ハイライト用 */</style>{% endblock %} </head> <body id="targetspace"> {% block body %}{% endblock %} </body>
</html>
検索結果では、検索キーワードをハイライトする仕様(黄色)を実装しています。
そのために上記のbase.html
に下記のcssを入れています。
span.mark { background-color: yellow; }
検索結果表示用のhtmlファイル
検索結果を表示するためのhtmlファイルを用意します。
表示するためだけではなく、検索キーワードを送る用のフォームも用意します。
検索用のキーワードはPOST
でmd_search.py
へ渡します。
{% extends 'base.html' %} {% block body %}
<h1 class="m-3">メモアプリ 全文検索</h1>
<div class="input-group ml-3"> <form action="/" method="POST"> <input type="text" name="search_key" required="required" autocomplete="off" /> <input type="submit" value="Search" /> </form>
</div>
<p class="m-3"> 検索キーワード:<span id="search_word">{{ search_key }}</span>
</p>
<p class="m-3">検索結果:{{ result | length }}件 ({{ elapsed_time }} 秒)</p>
<ul> {% for i in result %} <h1> <a href="{{ i.url }}" style="text-decoration: none">{{ i.file_name }}</a> </h1> <p>{{ i.contents }}</p> {% endfor %}
</ul>
キーワードハイライト用のJavaScriptをindex.html
の下部に設置しています。
ハイライトのコードは下記のページを参考にさせていただきました。
<!-- ハイライト処理に関するスクリプト -->
<script> var backupOriginal = ""; //元のHTMLソースを保持しておく変数 var search_key = document.getElementById("search_word").innerHTML; //検索ワードを取得。 //文字列を検索してハイライト用要素を加える処理 function replacer(str, word, att) { var SearchString = "(" + word + ")"; var RegularExp = new RegExp(SearchString, "g"); var ReplaceString = '<span class="' + att + '">$1</span>'; var ResString = str.replace(RegularExp, ReplaceString); return ResString; } //ハイライトを加える処理 function addhighlight() { backupOriginal = document.getElementById("targetspace").innerHTML; var forShow = backupOriginal; forShow = replacer(forShow, search_key, "mark"); document.getElementById("targetspace").innerHTML = forShow; } //読み込みが完了後にハイライト処理を行う window.onload = function () { if (search_key) { addhighlight(); } };
</script>
検索ページの使い方
md_search.py
を実行すればローカルでflaskサーバーが立ち上がるので、アクセスすれば使用できます。
私の場合はデスクトップに md_search.py
のショートカットを用意しておいて、すぐに実行できるようにしています。