Path.glob()でExplorer風にソートしてみる。

 Path.globでファイル名に[]を含む場合のエスケープ。で、ワイルドカードに[]が入っていると動作がおかしくなるのでそれをエスケープするというのをやりましたが、同じく glob ネタ。

Explorerではひらがな・カタカナを区別せずにソートしているっぽいですが、Path.glob()とかバッチファイルだとワイルドカードが文字コード順にソートされるので、処理順序がExplorerでの表示順と異なります。ちょっと気持ち悪いので、それっぽくできないかと考えてみました。ちなみに、Explorerでの並び順と正確に同じかどうかは検証してません。それっぽければ問題ないので。

sortedglob.py

#!python3
# -*- 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:
"""
sortedglob.py:
与えられたパターンに一致するファイル名を取得して、
Explorer風のソートを行い、リストを返す

created : Jun 26, 2022
"""
from pathlib import Path
from jaconv import z2h, kata2hira


def sortedglob(pattern: str):
    """Explorer風にソートするglob.

    patternで渡されたワイルドカードパターンを展開して、
    Explorer風にソートした順番で返す。

    Args:
        pattern (str): globに渡すパターン

    Yields:
        Path: ソートされたPathオブジェクト
    """
    dir = Path(pattern).absolute().parent
    p = Path(pattern).name
    pathlist = {}
    for f in dir.glob(p):
        key = kata2hira(z2h(str(f.absolute()), kana=False, ascii=True, digit=True))
        pathlist[key] = f
    result = sorted(pathlist.items(), key=lambda x: x[0])
    for f in result:
        yield f[1]


if __name__ == "__main__":
    for s in sortedglob("M:/Files/Documents/*"):
        print(s)
jaconvで全角英数を半角にして、カタカナは全部ひらがなに揃えて、それをキーにしてソートします。
さらに漢数字対応とかしてもいいかも。

マルチスレッドとgenerator。

 通常の線形の処理なら問題ないのですが、処理を効率化しようとマルチスレッドにすると、処理をする対象をどうやってスレッドに渡そうかと思うことがあります。

たとえば、たこ焼きを焼いています。

たこ焼き器の凹みをなんて呼ぶのかは知らないのですが、タコツボとでも呼びましょう。

タコツボにはかならず1つのタコを入れます。2つ入れてはいけないし、1つも入ってないのもだめです。

たこ焼き職人が1人なら問題ありません。順序よくタコツボにタコを落としていきます。

職人が2人になると、問題が起こります。タコツボに入れたタコは沈んでしまうので、もうひとりの職人が間違ってもう1個のタコを入れてしまうかもしれません。

それを防ぐためにはタコツボを管理する人が必要です。タコツボを差し出しておいて、どちらかの職人がタコを入れたらすっと引っ込めて次のタコツボを差し出します。そうすればどちらの職人がタコを入れようが、タコツボには1つのタコだけが入ります。


なんて、よくわからない喩え話ですが、要は処理したいシーケンスがあり、そのシーケンスのアイテムは一度だけ処理されればよい、なのでスレッド外で誰かが管理しないといけない、ということです。

リストですと、シングルスレッドならば

for item in itemlist:

で片付きます。ところがマルチスレッドにするといきなりずっこけます。というか、どう言うコードを書いたらいいものかと考えてしまいます。

1つは明らかにitemlistのインデックスを管理するカウント変数を用意して、各スレッドは自分がitemを受け取ったらそのカウント変数を+1する方法です。当然ならそのカウント変数はグローバルになるでしょうから、スレッドセーフではありません。なので、itemを受け取ってカウント変数を+1するまでは、ロックしておかないといけません。

ところがこれがインデックスで管理できるものならよいのですが、たとえばディレクトリツリーを探索する、みたいな感じだとそうも言っていられません。ツリーが巨大になると、そのツリーをリスト化するだけでも結構な時間がかかります。

そこでgeneratorです。アイテムを返すジェネレータを1つだけ用意し、各スレッドから排他的にデータを受け取れれば、二重に処理されるということもありません。

ということで、こんな風にしてみました。


import threading
from random import random
from time import sleep



def get_word():
	l = """
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
"""
	s = l.split(" ")
    for t in s:
        yield t

def tthread(name=""):
    waittime = random()
    while True:
        with lock:
            try:
                t = gen.__next__()
            except StopIteration as e:
                break
        print(f"{t}", end=" ")
        sleep(waittime)


def main():
    t1 = threading.Thread(target=tthread)
    t2 = threading.Thread(target=tthread)
    t1.start()
    t2.start()

gen= get_word()
lock = threading.Lock()
main()
もとの文は PEP 20です。
lockを取得できたら1単語もらって表示する、というだけですが、__next__()がいい仕事してくれます。

IntelのドライバがWindowsUpdateで巻き戻される件。

ffmpeg や QSVEnc の関係で、Intel の Quick Sync Video を使っているので、ドライバのバージョンを勝手に上げられたり巻き戻されたりするのはすごくやっかいです。

デバイスドライバだけ Windows Update の対象外にできないか、と思ったのですが、検索してみたら次の記事が引っかかりました。


これによると、PowerShellの管理者モードで、レジストリに以下のキーと値を設定することで回避できる、とのこと。

reg add HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate /v ExcludeWUDriversInQualityUpdate /t REG_DWORD /d 1 /f

最近ちょくちょく PowerShell は使っているのですが、ここはオーソドックスに Regedit でやってみました。

  1. HKLM\SOFTWARE\Policies\Microsoft\Windows に行く。
  2. 新規にキー WindowsUpdate を作成する。
  3. キーに移動して、新規に DWORD で ExcludeWUDriversInQualityUpdate を作成し、値を 1 にする。
これでほんとに回避できるかどうか、しばらく見てみようと思います。

Path.globでファイル名に[]を含む場合のエスケープ。

 Pythonのpathlib.Pathにあるglob()ですが、どうしても引っ掛けられないファイル名が出てきて、どうすればよいか調べ中です。

具体的には、globの引数に "*[123].txt" というような、[] で囲まれた文字があるファイル名を指定したいのですが、それが引っかかってこない。どうやらワイルドカードの特殊文字になっているようです。

別のライブラリ glob には glob.escape() というメソッドがあるのですが、それを使うと今度は "*" がエスケープされてしまい、やっぱりファイルが引っかかりません。

はてさてどうしたものか……。

足りない頭で考えましたが、結局pathlib.Path.glob()で扱うことは諦めて、glob.glob()とglob.escape()を組み合わせて使うことにしました。

from glob import glob, escape
from pathlib import Path

def bracketglob(s: str):
    """generate a list of filenames from a string that contains "[]".

    []で囲まれた文字列を含むワイルドカードを展開する。

    Args:
        s(str): ファイル名文字列

    Returns:
        f(Path): glob展開されたファイル名

    """
    #
    # [] で囲まれた文字とそれ以外を別扱いにするひつようがありそう
    #
    lsqbpos = rsqbpos = -1
    l1 = l2 = l3 = ""
    if (lsqbpos := s.find("[")) != -1:
        l1 = s[0:lsqbpos]
    if (rsqbpos := s.rfind("]")) != -1:
        l3 = s[rsqbpos + 1 :]
    l2 = s[lsqbpos : rsqbpos + 1]
    if (rsqbpos - lsqbpos > 0) and (lsqbpos > 0):
        for f in glob(l1 + escape(l2) + l3):
            yield Path(f)
    else:
        for f in glob(s):
            yield Path(f)
なんとなく yield 使ってますが、そもそも for ループで glob() 使ってるからいらないんじゃね?的なギモンはあるし、力技でエレガントではない感じがジンジョーじゃない……。
処理としては、"[]" で囲まれた文字列とそれ以外を分離して3つに分けて、"[]" 文字列をescape します。"[]"は最大長で解釈させているので、

"吾輩ハ猫デアル [夏目漱石][上][大倉書店][1905].doc"

などという場合、"[夏目漱石][上][大倉書店][1905]" はすべてエスケープされるので、たぶんこの中の文字も検索対象になる…と思います。検索したいパターンに "*[大倉*]*" などとするときっと無理ですが。

Windowsでシンボリックリンクを試してみる。

きっかけは、1つのファイルを別の名前で起動したら違う動きになるようなスクリプトを書く、でした。  busybox なんかでは、同じ実行形式ファイルの名前を、lsにすればlsと同じ、cpとすればcpと同じ動作をするようにしてますが、Pythonスクリプトでそれと同じように argv...