ファイルを高速で検索する
ファイルを検索するとき、普通はルートから順に再帰的に検索をかけていきます。Pythonではそれに便利なglobと言うモジュールもあります。
でも
遅っせえです。まあ、当たり前ですよね。あと、glob.globはちょっとアレな所があります。ちょっと色々ハマったし、WindowsだとPathlibしか使わん。なのかもしれません。
速くするにはどうすればいい?
一番簡単なのは、インデックスを作成してデータベース化してしまうことです。でも、たかが1アプリのためにわざわざそんな物作る理由も意味もありません。
WindowsにはEverythingがある!
Everythingと言うソフトがあります。常駐してインデックスを作成しています。便利なソフトなので私は使っています。
このソフトでは、ETPサーバーと言うものを使うことができます。これはちょっと毛が生えたFTPです。
PythonからETPサーバーにアクセスすれば簡単に全ドライブ検索が実装できるんじゃなくね?
と、思ってweb検索してみたのですが、Javaでの実装しか無かったので、Pythonで作ってみました。
from dataclasses import dataclass
from ftplib import FTP
import os,os.path
FTP.encoding = "utf-8"
@dataclass(frozen = True)
class ETPServerinfo:
host: str = 'localhost'
port: int = 21
user: str = 'anonymous'
passwd: str = 'foo@bar'
timeout: int = 500
@dataclass
class ETPQuery:
query : str = ''
case : bool = False #大小文字を区別
whole_word : bool = False #単語にマッチ
path : bool = False #パスにマッチ
diacritics : bool = True #補助符号にマッチ
regex : bool = False #正規表現検索を行う
r_sort : str = 'NAME_ASCENDING'
r_offset : int = 0
r_count : int = 0xffffffff
r_size_column : bool = False
r_date_created_column : bool = False
r_date_modified_column : bool = False
r_attributes_column : bool = False
r_path_column : bool = True
r_file_list_filename_column : bool = False
class ETP:
def __init__(self, server : ETPServerinfo):
self.ftp = FTP()
self.ftp.connect(host = server.host, port = server.port, timeout = server.timeout)
self.ftp.set_pasv('true')
self.ftp.login(user = server.user,passwd = server.passwd)
self.ftp.cwd('/')
c = self.ftp.sendcmd('FEAT')
if c.find('EVERYTHING') == -1:
raise ValueError('FTP not supported Everything command')
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.ftp.quit()
def close(self):
self.ftp.quit()
def _r_out(self, q : ETPQuery):
_s = self._s
self.ftp.sendcmd('EVERYTHING SORT ' + q.r_sort)
self.ftp.sendcmd('EVERYTHING OFFSET ' + str(q.r_offset))
self.ftp.sendcmd('EVERYTHING COUNT ' + str(q.r_count))
self.ftp.sendcmd('EVERYTHING SIZE_COLUMN' + _s(q.r_size_column))
self.ftp.sendcmd('EVERYTHING DATE_CREATED_COLUMN' + _s(q.r_date_created_column))
self.ftp.sendcmd('EVERYTHING DATE_MODIFIED_COLUMN' + _s(q.r_date_modified_column))
self.ftp.sendcmd('EVERYTHING ATTRIBUTES_COLUMN' + _s(q.r_attributes_column))
self.ftp.sendcmd('EVERYTHING PATH_COLUMN' + _s(q.r_path_column))
self.ftp.sendcmd('EVERYTHING FILE_LIST_FILENAME_COLUMN' + _s(q.r_file_list_filename_column))
def _r_fmt(self):
def cmp(s,cmp):
if s.startswith(cmp):
r = s[len(cmp)+1:]
return int(r) if r.isdigit() else r
else:
return None
def pj(path,fn):
if path is not None:
return os.path.join(path,fn)
else:
return None
a = self.ftp.sendcmd('EVERYTHING QUERY').split('\n')
r = []
ri = {}
cnt = None
for i in a:
if i.startswith('200-Query'):
continue
if i.startswith(' RESULT_COUNT'):
cnt = int(i[14:])
c = cmp( i, ' PATH')
if c is not None:
ri['Path'] = c
continue
c = cmp( i, ' ATTRIBUTES')
if c is not None:
ri['Attributes'] = c
continue
c = cmp( i, ' SIZE')
if c is not None:
ri['Size'] = c
continue
c = cmp( i, ' DATE_CREATED')
if c is not None:
ri['Date_Created'] = c
continue
c = cmp( i, ' DATE_MODIFIED')
if c is not None:
ri['Date_Modified'] = c
continue
f = cmp(i,' FOLDER')
if f is not None:
ri['isdir'] = True
ri['isfile'] = False
ri['Name'] = f
ri['PathName'] = pj(ri['Path'],f)
r.append(ri)
ri = {}
f = cmp(i,' FILE')
if f is not None:
ri['isdir'] = False
ri['isfile'] = True
ri['Name'] = f
ri['PathName'] = pj(ri['Path'],f)
r.append(ri)
ri = {}
return (cnt,r)
def _s(self, p : bool):
return ' 1' if p else ' 0'
def _sendcmd(self, q: ETPQuery, fc: str):
_s = self._s
ec = 'EVERYTHING '+fc
self.ftp.sendcmd(ec+'CASE' + _s(q.case))
self.ftp.sendcmd(ec+'WHOLE_WORD' + _s(q.whole_word))
self.ftp.sendcmd(ec+'PATH' + _s(q.path))
self.ftp.sendcmd(ec+'DIACRITICS' + _s(q.diacritics))
self.ftp.sendcmd(ec+'REGEX' + _s(q.regex))
self.ftp.sendcmd(ec+'SEARCH ' + q.query)
def query(self, q : ETPQuery):
self._r_out( q )
self._sendcmd( q, '')
return self._r_fmt()
def filter_query(self, q : ETPQuery):
self._r_out( q )
self._sendcmd( q, 'FILTER_')
return self._r_fmt()
def raw_cmd(self, q: ETPQuery, filtersw):
self._r_out( q )
if filtersw:
self._sendcmd( q, 'FILTER_')
else:
self._sendcmd( q, '')
def raw_query(self):
return self.ftp.sendcmd('EVERYTHING QUERY')
""" SORT PARAM
'NAME_ASCENDING','NAME_DESCENDING',
'PATH_ASCENDING','PATH_DESCENDING',
'SIZE_ASCENDING','SIZE_DESCENDING',
'EXTENSION_ASCENDING','EXTENSION_DESCENDING',
'DATE_CREATED_ASCENDING','DATE_CREATED_DESCENDING',
'DATE_MODIFIED_ASCENDING','DATE_MODIFIED_DESCENDING',
'ATTRIBUTES_ASCENDING','ATTRIBUTES_DESCENDING',
'FILE_LIST_FILENAME_ASCENDING','FILE_LIST_FILENAME_DESCENDING'
"""
if __name__ == '__main__':
with ETP(ETPServerinfo(port = 9021)) as f:
c,r = f.query(ETPQuery(query = '*.log'))
for i in r:
print(i['PathName'])
print(c,'件')
print('='*20)
f.raw_cmd(ETPQuery(query='python'),False)
f.raw_cmd(ETPQuery(query='whl'),True)
print(f.raw_query())
from ETPlib import ETPServerinfo,ETPQuery,ETP
with ETP(ETPServerinfo(port = 9021)) as f:
c,r = f.query(ETPQuery(query='うまぴょい'))
print(r)
まず、Everythingの設定を変更して下さい。
ETP/FTPサーバーを有効(E) にチェックを入れて、必要に応じてポート番号、ユーザー名、パスワードを設定して下さい。
図ではポート番号を標準の21から9021に変更しています。
使い方
from ETPlib import ETPServerinfo,ETPQuery,ETP
でimportしてください。
server = ETPServerinfo ( host= ホスト名 , port= ポート番号 , user= ユーザー名 , passwd= パスワード , timeout = タイムアウト)
のように設定します。初期値として
host = 'localhost'
port = 21
user = 'anonymous'
passwd = 'foo@bar'
timeout = 500
が設定されているので、ローカルホストでの使用では特に変更する設定は無いと思います。ETPServerinfoはイミュータブルです。宣言以降値の変更はできません。
etp = ETP( server )
で接続します。
server はこの後使用しないので、複数回接続するのでなければサンプルのように
etp = ETP( ETPServerinfo( .... ))
のように使用しても問題はありません。
ETPQueryを引数として検索をします。
q = ETPQuery()
以下のプロパティがあります。
・検索に関するオプション
ETPQuery.query : str # 検索する文字列
ETPQuery.case : bool = False # 大文字小文字を区別するか
ETPQuery.whole_word : bool = False #単語にマッチ
ETPQuery.path : bool = False #パスにマッチ
ETPQuery.diacritics : bool = True #補助符号にマッチ
ETPQuery.regex : bool = False #正規表現検索を行う
・結果表示に関するオプション
ETPQuery.r_sort : str = 'NAME_ASCENDING' # 結果をどういう順序でソートするか。ソース中の SORT PARAM のコメント参照
ETPQuery.r_offset : int = 0 # 結果を何番目から表示するか
ETPQuery.r_count : int = 0xffffffff # 結果を幾つ表示するか
ETPQuery.r_size_column : bool = False # 結果にサイズを含めるか
ETPQuery.r_date_created_column : bool = False # 結果に作成日を含めるか
ETPQuery.r_date_modified_column : bool = False # 結果に更新日を含めるか
ETPQuery.r_attributes_column : bool = False # 結果に属性を含めるか
ETPQuery.r_path_column : bool = True # 結果にファイルパスを含めるか
ETPQuery.r_file_list_filename_column : bool = False # 使用しません
結果表示は基本変更する必要はありません。ローカル検索を前提にしているので、各種ファイル情報は直接調べれば良いと言う判断からです。変更する場合、ETPQuery.r_pathだけは注意してください。これをFalseにすると、ファイルがどの場所にあるかわからなくなります。
c , r = etp.query( q : ETPQuery )
で検索します。cはヒット数、rはファイル情報のリストです。
ファイル情報には
r[n]['Name'] #ファイル名
r[n]['Path'] #ファイルのあるパス ※ETPQuery.pathがFalseだと存在しません
r[n]['PathName'] #フルパス名 ※ETPQuery.pathがFalseだと存在しません
r[n]['isdir'] #ディレクトリならTrue
r[n]['isfile'] #ファイルならTrue
・ETPQuery.r_....で、結果を追加している場合、
r[n]['Size'] #ファイルサイズ
r[n]['Attribites'] #属性
r[n]['Date_Created'] #ファイル作成日(int)
r[n]['Date_Modified'] #ファイル更新日(int)
が含まれます。
一度検索をした後に、
c , r = etp.filter_query( q : ETPQuery )
で絞り込み検索ができます。
件数が多くなると、リストへ変換するのに時間がかかります。そういう場合はraw_cmd,raw_queryを使ってください。
etp.raw_cmd( q : ETPQuery , filter = bool )
で結果を表示せずに検索だけを行います。filter が Trueなら絞り込み検索です
etp.raw_query()
で結果をとりだします。この結果は文字列です
etp.close()
で切断します
ダメなところ
エラーチェックがザルです。
ETPサーバーかどうかのチェックはかろうじてしていますが、それ以外のエラーは全く考慮していません。