QNAPがサムネイルキャッシュを自動的に作成する件。

うちでは NAS に QNAP TS-669L を使っています。

古いモデルなので最近の大容量 HDD に換装するためには HDD トレイを買い直す必要があったりして余計な出費になりましたが、今の所 10TB x6 の構成で元気に動いています。一応 CPU は Intel Atom D2701 デュアルコア(論理4コア)だし、メモリは 2GB 追加してトータル 3GB はあるし、Gigabitイーサなので、LANを10Gbitにしない限りは余裕は不足感はありません。

で、SSH ログインしていろいろいじっていたら、フォルダに所有権が管理者になっている .@__thumb というディレクトリがあって、サムネイルキャッシュが格納されていました。結構地味に容量食ってます。

これはケシカランということで、サムネイルの自動生成を止める方法と、すでに作られてしまったキャッシュを.@__thumbディレクトリごと削除する方法をまとめます。


まず、QNAP の管理ウェブインタフェースを開きます。そして File Station を開きます。


File Station 右上のメニューから、「設定」を開きます。

すると「マルチメディア再生とサムネイル再生をサポートします」という項目があるので、チェックを外します。
もしも使っていないようなら、「コントロールパネル」→「マルチメディア管理」→「メディアライブラリー」で、メディアライブラリーを停止しておきます。
以上でサムネイルは自動生成されなくなるはず、です。たぶん。

次に.@__thumbをまるごと削除する方法ですが、検索すると、findxargsを使って find ./* -name ".@__thumb" -type d | xargs rm -rfという方法が引っかかってきます。

ところがこれだとうまく削除できません。たぶん、Busyboxだからじゃないかと思いますが、ともあれこれじゃだめです。

QTS 4.3.4 にはデフォルトで Python 2.7.5 がありますので、これを使います。
import os
import shutil

for root, dirs, files in os.walk(".", topdown=False):
    print("Root: %s" % root)
    for dir in dirs:
        if dir == ".@__thumb":
            delpath = os.path.join(root, dir)
            print "Removing %s" % delpath
            shutil.rmtree(delpath)
os.walk()してカレントディレクトリ以下のすべてのディレクトリを検索して、名前が ".@__thumb" だったらツリーごと削除、というだけのスクリプトです。

あとはadminでSSHログインして然るべきディレクトリで実行するだけです。

C/MigemoでAND検索するには。

検索結果を絞り込むために、「AかつBを含む」という条件を設定したいことがあります。

ところが、Pythonのreモジュールには、論理和(AまたはB)はあっても論理積(AかつB)はありません。というか、正規表現というもの自体に論理積がないのかもしれません。if文の条件にはできるのに。

PDFのお料理レシピを検索する。で、recipe tamago cabbageができませんと書いたのですが、これを実現するには篩がけを2回すればいいことになります。
つまり、"tamago"で検索した結果に対してさらに"cabbage"で検索を行って絞り込むわけです。

前回のコードでは、一致した結果をrecipe_files.append()で追加していきましたが、2回め以降の絞り込みでは、一致しない要素をrecipe_files.remove()で取り除いてやればいけそうです。

よくよく考えてみると、これはC/Migemoに限らずre.search()でAND検索をやるときにも使えそうです。汎用性を考えるなら、モジュール化してやれば便利そうなので、ちょっと考えてみます。(次回に続く)

PDFのお料理レシピを検索する。

日本語検索を「かな漢字変換」を通さずにやる。その2で、ファイル名で検索かけて一致するものをピックアップする、というのをやったので、これは使えるかも、とPDFにウェブクリップしていたお料理レシピを検索するようにしてみました。

  • 複数キーワード対応(AND検索)とマッチ数を表示するようにしました。2021/8/1 ソース改定
  • 新規追加されたファイルのみキャッシュに登録するオプションを追加しました。2021/8/2 ソース改定

ク****ドや味*素などいろんなサイトでレシピが公開されていますが、「今日は豚こまがあるから…」とグーグル先生に「豚こま レシピ」とか毎回検索してもいいんですが、気に入ったレシピはChromeからPDF形式でファイルに落として(印刷して)おいて、ちょくちょく参照しています。当然それが溜まってくるとタイトル(ファイル名)から見つけにくくなってくるので、これをなんとかしようと。

ライブラリのように数千もあるわけではないので、ディレクトリ指定でos.walk()してファイル名をパターンサーチしていけばいいか、ということで、引っかかったファイル名をSumatraPDFに食わせてそのままPDF表示、という形にしました。

ついでに、「せっかくC/Migemoを使えるんだからローマ字でも検索できるようにしちゃえ」ということで組み合わせ、さらに「コンテンツ(レシピ内容)からも検索できるようにしたほうがいいよね」ということでpdfminerも組み合わせることにしました。

ところがPDFファイルが少なければ問題ないのですが、多くなってくると毎回pdfminerでテキスト抽出してパターンサーチというのはものすごく時間がかかります。そこで、一回pdfminerでextract_text()したらその結果をキャッシュしておけばいい、ということで、SQLite3を使ってキャッシュを行うようにしました。

さらに、コンテンツを検索したいときには"-c"スイッチを指定して、デフォルトではコンテンツを検索しないようにしました。未キャッシュのファイルが多いときには"-c"を指定するとものすごく時間がかかりますが、一度キャッシュすれば楽ちんです。ほんとはタイムスタンプまで含めて登録しておいて、ファイル日付が違ってたらキャッシュし直しとかしてもいいんですが…。

手前味噌ながらこのユーティリティのいいところは、recipe tamagoとすると、「たまご」「タマゴ」「玉子」「卵」「Egg」などの表記の揺れをカバーしてくれることです(辞書による)。ただし、C/Migemoの仕様の関係でrecipe tamago cabbageなどのように複数のキーワードを指定しても無駄なところがあります。複数キーワードも受け付けるようにしました。複数指定された場合には、AND検索(AとBを同時に含むもの)に絞り込みできます。たとえば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)

Windows11のTPM。

今年後半に発表されるというWindows11ですが、実行要件としてTPM(Trusted Platform Module)2.0が必須となるようです。

これはCPUに搭載された機能のようですが、別途ハードウェアモジュールとしても提供されるようです。うちではAsrockのマザーボードを使用しており、メインは Core i9-9900K を搭載した Asrock Z390 Extreme4 ですが、これはBIOSで対応しているようです。一方、サブとして使っているのは Core i7-4990K の Z97 Extreme4 で、こちらはTPM2.0に対応していません。

Windows11がサポートしているIntelのCPUはこちら、AMDのCPUはこちらにリストがあります。

これだと4990Kのサブ機は非対応ということになるのですが、AsrockからはハードウェアTPMモジュールが提供されているようです。チップ製造元はNuvotonやInfineonとなっています。こちらのものはマザーボード上のTPMヘッダーに装着して使用するようですが、Z97 Extreme4にもTPMヘッダーは実装されています。また、TPM-SPIというモジュールもあるようで、こちらは非常に小型です。SPIインタフェースというのが気になりますが、対応マザーボードリストも掲示されていません。さらには暗号化の輸出規制の関係か、販売される国が限定されているようです。

TPM-sモジュールについては、過去にAmazonで1500円程度で販売されていたこともあるようですから、市場に流れてくれば買いやすい値段になるかもしれません。現状では2~3万円しているようですから、様子見ですね。

さて、TPM2.0は9900Kではサポートしているということで、BIOSの設定を見ると、右の方に項目がありました。"Security"タブの一番下の項目が "Intel(R) Platform Trust Technology"で、デフォルトでは"Disabled"されています。

この状態では TPM.msc の画面は以下のようになっています。


これを"Enabled"にして再起動してみると、以下のように変わります。
使い方はわからないし、とりあえずユーザがいじるものではない、みたいな記述もあるのでとりあえずここまでにしておきます。

pyls_msがなくなった。

deopleteのソースをJediからLSPに変更する。で、Microsoft Python Language ServerをNeovimから使えるようにしてみたんだけれど、:call dein#update()してみたら、エラーが出てしまいました。:messageしてみると以下のような出力が。

[dein] Error occurred while executing hook: nvim-lspconfig
[dein] Vim(lua):E5108: Error executing lua [string ":lua"]:3: attempt to index field 'pyls_ms' (a nil value)
Lua が pyls_ms を読み込もうとしたんだけれど値が nil でした、ってことです。

え、もしや、と思って検索すると、https://github.com/neovim/nvim-lspconfig/issues/1073で、Python3で使っていると Error 134を出してクラッシュする、というようなことが書いてあります。

それに対して、pyls_msはpylance/pyrightに引き継がれていてMicrosoftはメンテナンスを放棄しているので、pyrightかpylspに切り替えることを推奨します、とのこと。
pyls_ms is borderline abandoned by microsoft (in favor of pylance/pyright), I would recommend switching to pyright or pylsp (`pyls is also abandoned) I'll probably remove pyls_ms shortly. It's not a bug in the built-in client (which this repo is not, this repo is just the basic settings we send to the language servers), it's likely a bug in pyls_ms that will never be fixed
ということで、pyls_ms.luaはすでに削除されてしまっていたのでした。実際、たしかに時々LSPによる補完が効かないことがあって、なんでかなぁと思ったり、NeovimはおろかChromeとか起動しているすべてのアプリを巻き込んでクラッシュしたりということもあったので、潮時なのかもしれません。ということで候補なのですが…。

pylspことPython LSP ServerはJediをエンジンとして利用しているので、せっかくJediを離れてpyls_msにしたのにまた戻ることになります。補完速度的にも若干遅い気がしますし。

pyrightはpyls_msと同じMicrosoftから提供されていますが、使用するためにはNode.jsを別途インストールする必要があるようです。

仕方ないので、Node.jsの現行推奨版である14.17.3-x64 LTSをインストールします。ただ、Chocolateyを含むビルドツールも必要に応じてインストールする、というオプションはチェックをしないでおきます。PythonもVisual Studio 2019もインストールしてあるし。

インストールが終了したら、新規にコマンドプロンプトを開いて動作を確認します。
C:\Users\kats$ node --version
v14.17.3

C:\Users\kats$ npm --version
6.14.13
動くようなのでpyrightをインストールします。
C:\Users\kats$ npm install -g pyright
C:\Users\kats\AppData\Roaming\npm\pyright -> C:\Users\kats\AppData\Roaming\npm\node_modules\pyright\index.js
C:\Users\kats\AppData\Roaming\npm\pyright-langserver -> C:\Users\kats\AppData\Roaming\npm\node_modules\pyright\langserver.index.js
+ pyright@1.1.158
added 1 package from 1 contributor in 1.015s
pyrightを起動してみると、
C:\Users\kats$ pyright
No configuration file found.
No pyproject.toml file found.
stubPath C:\Users\kats\typings is not a valid directory.
Assuming Python platform Windows
Searching for source files
Auto-excluding C:\Users\kats\.cache\dein\.cache\init.vim\.dein\test\test-files\python\with_virtualenv\env
Auto-excluding C:\Users\kats\.cache\dein\repos\github.com\dense-analysis\ale\test\test-files\python\with_virtualenv\env
Skipping recursive symlink "C:\Users\kats\AppData\Local\Application Data" -> "C:\Users\kats\AppData\Local"
Skipping broken link "C:\Users\kats\AppData\Local\ElevatedDiagnostics"
Auto-excluding C:\Users\kats\Projects\JupyterLab
Auto-excluding C:\Users\kats\work
Found 11346 source files
Emptying type cache to avoid heap overflow. Heap size used: 1537MB
Emptying type cache to avoid heap overflow. Heap size used: 1823MB
Emptying type cache to avoid heap overflow. Heap size used: 2108MB
Emptying type cache to avoid heap overflow. Heap size used: 2381MB
Emptying type cache to avoid heap overflow. Heap size used: 1538MB
Emptying type cache to avoid heap overflow. Heap size used: 1734MB
なにやらカレントディレクトリ以下のすべての*.pyファイルを検索してキャッシュしているようです。

それはさておき。

Neovim側では、これまで使用していたpyls_msの設定を削除して、pyrightの設定をdein.tomlに追加します。
nvim_lsp.pyright.setup{
}
ここに記入する設定は、CONFIG.md#pyrightに説明されています。が、基本的にはパスさえ通っていれば特に設定の必要はなさそうです。

dein.tomlを保存してからNeovimを起動し直すと、こころなしかpyls_msのときよりも起動が早いようです。遅延発火(lazy)はしていないので、これはNode.jsをインストールしてまで導入したかいがあったかもしれません。

日本語検索を「かな漢字変換」を通さずにやる。その3

日本語検索を「かな漢字変換」を通さずにやる。その2を受けて…。

検索文字列に "class" を指定したのに "分類" などが引っかかってくる(かつ "クラス" などが含まれない)と、「一体何でキミはここにいるのかね?」な状況になって困惑してしまいます。そこで、"ripgrep" や "The Platinum Searcher" の出力のように、マッチした文字列をエスケープシーケンスで強調するようにしてみました。

こんな感じです。
BRIGHT_RED = "\033[91m"
BRIGHT_GREEN = "\033[92m"
BRIGHT_YELLOW = "\033[93m"
BRIGHT_BLUE = "\033[94m"
BRIGHT_MAGENTA = "\033[95m"
BRIGHT_CYAN = "\033[96m"
BRIGHT_WHITE = "\033[97m"
DEFAULT = "\033[39m"

def search_in_lsr(pat, dirs):
    """search pat in ls-R files"""
    for target in dirs:
        logger.debug("finding in %s", target)
        try:
            with open(target + "/ls-R", "r", encoding="utf-8") as f:
                lines = f.readlines()

                for line in lines:
                    if res := pat.search(line):
                        line = pat.sub(BRIGHT_CYAN + res.group() + DEFAULT, line)
                        print(target + line, end="")
        except FileNotFoundError as e:
            print(e)
            print(f'run "mp4lsr.py {target}" to generate ls-R files.')
            sys.exit(1)
ファイル名のリストは、mktexlsrの作成する "ls-R" ファイルのように、「このディレクトリ以下のファイルリスト」という形で、いくつかのディレクトリに散らばっています。

例にしてあげるとこんな感じ。
M: -+- 自然科学 -+- 物理学
    |            +- 化学
    |            +- ・・・
    |            +- ls-R
    |
    +- 文化芸術 -+- 音楽 -+- クラシック
                 |        +- 雅楽
                 |        +- 民謡
                 |        +- ・・・
                 |        +- ls-R
                 +- 美術
                 +- ・・・
言ってみれば、ジャンルごとにそのディレクトリツリーの "ls-R" ファイルを作成するわけです。

引数のpatはCMigemoが出力してきた正規表現をre.compileした正規表現オブジェクト、dirsは "ls-R" が置いてあるディレクトリのリストです。
pat.search(line)Nullでなければ、マッチした文字列をすべてエスケープシーケンスで囲むように置換する、という処理をしています。

ところがこれをコマンドプロンプトでやると、指定したエスケープシーケンスが文字のママ出力されてしまい、一層困惑することになります。

そこでPython Windows10 において Console 出力に色を付けるを参考にコマンドプロンプトをVT互換のエスケープシーケンスを処理するように設定することで、一致した文字列を強調できるようになりました。Microsoftによる日本語の解説はコンソールの仮想ターミナル シーケンスにあります。

自画自賛なんですがめちゃくちゃ便利。

日本語検索を「かな漢字変換」を通さずにやる。その2

それではPythonからC/Migemoを使ってみます。

CMigemo.exeの出力をsubprocessで取り込んで使おうかと思ったのですが、出力形式がVimスタイルやEmacsスタイルはあっても、今ひとつPythonのreに丁度いいような形ではなくてうまく引っかかってこなかったので、なにかないかとぐぐってみるとcmigemo 0.1.6というパッケージがありました。ctypesを使ってmigemo.dllをロードするようです。

Githubのプロジェクトページはこちら。moozさんという方が作られたようです。

ということで早速これを
pip install python-cmigemoして、組み込んでみました。
import cmigemo

migemo_dict = "c:/Apps/bin/dict/base-dict"

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)
なのですが、どうにもmigemo.dllをロードしてくれません。 ソースを追いかけてみると、
    def _load_libmigemo(self, lib_name="migemo"):
        import platform

        if platform.system() == u"Windows":
            libmigemo = windll.migemo
のところでエラーになっています。ということで、この部分を
    def _load_libmigemo(self, lib_name="migemo"):
        import platform

        if platform.system() == u"Windows":
            libmigemo = windll.LoadLibrary(find_library("migemo"))
として、find_library()に探させてみたら、migemo.dllをPATHの通ったところに置いておけば見つけてくれるようです。

これで、C/Migemoが出力してきた正規表現を検索パターンにしてre.compile()してやればいろいろとできそうです。

でもって結果ですが…。

もちろん辞書にもよると思いますが、思った以上に強力です。
"class" で検索をかけると、"Class" や "クラス" はもちろんのこと、"クラシック" とか "分類" などまでも引っかかってきます。検索時にre.IGNORECASEを指定してやると、"CLASS" も "class" も "ClaSS" も引っかかってくるし、日本語もちゃんと受け取って、その場合にはちゃんと日本語での検索結果が上がってきます。 これは思った以上に快適です。

SWDなら3線でいいとはいうものの。

 安価で優秀なデバッグプローブはないかしら、と探したら、 Raspberry Pi Debug Probe というのがあったんですが、これは3線がにゅるんと出てるだけです。 もちろんSWDなら3線を繋げばいいのではありますが、汎用的に考えるなら5x2のリボンケーブルコネクタが欲し...