Skip to content

Instantly share code, notes, and snippets.

@seraphy
Last active August 4, 2024 04:41
Show Gist options
  • Save seraphy/60fc782e8222ab27d87834f2c22a35c8 to your computer and use it in GitHub Desktop.
Save seraphy/60fc782e8222ab27d87834f2c22a35c8 to your computer and use it in GitHub Desktop.
python3による、2つ以上のファイルの突き合わせ結果を出力するコンソールツール

crosscheck.py

概要

2つ以上のテキストファイルを突き合わせて、すべて一致、もしくは一部一致、一致なし、のいずれかのモードに従って、その結果を出力します。

例えば、

data1
data2

data2
data3

の場合、

python3 crosscheckp.py text1 text2

とすると、

data2

デフォルトはallモードであるため、両方に一致するdata2が表示されます。

モード

  • all (match) すべて一致した場合
  • any (unmatch) 一致していないものがある場合
  • single 1つだけ一致(=どれにも一致していない)

テキストをパスとして比較する場合

テキストをパスとして比較する場合、パス区切ったトークン列の末尾から数えた数で一致させることができます。

./aaa/bbb/ccc/ddd

という場合、オプション -t 2 を指定した場合

ccc/ddd

という文字列として比較されます。

この場合、もとのテキストを出力結果とした場合は、オプション-s 1のようにファイルの順序を指定します。

[end]

#! /usr/bin/env python3
import os
import sys
# 複数のファイルを読み込んで重複する行を集計する
def crosscheck(files, common_part_size=0):
contents={}
for fidx, file in enumerate(files, start=1):
with open(file, 'r') as fh:
for line in fh:
text = line.strip()
if text == '':
continue
common_part = get_file_common_part(text, common_part_size)
if common_part in contents:
fmap = contents[common_part]
else:
fmap = {}
contents[common_part] = fmap
if (fidx, text) in fmap:
fmap[(fidx, text)] += 1
else:
fmap[(fidx, text)] = 1
return contents
# ファイル名の配列と共通部分のサイズを指定して重複するファイル名を表示する
def crosscheck_print(contents, selector, select=0, verbose=False, eol=os.linesep):
for common_part, fmap in contents.items():
# fmapとfiles_lenが一致しているか
fidxs = set(sorted([fidx for (fidx, text), count in fmap.items() if count > 0]))
if verbose:
print("<%s> : %s" % (common_part, fidxs), file=sys.stderr)
if selector(fidxs):
if select == 0:
print(common_part, end=eol)
else:
texts = [text for (fidx, text), count in fmap.items() if fidx == select]
if texts:
print(texts[0], end=eol)
# ファイルをトークンに分解し指定した末尾数で共通部分を取得する
def get_file_common_part(file, common_part_size):
tokens=[]
while True:
file, token = os.path.split(file)
if token != '':
tokens.insert(0, token)
else:
break
return os.path.sep.join(tokens[-common_part_size:])
# Main
if __name__ == "__main__":
import argparse
# 引数の解析
parser = argparse.ArgumentParser(description='''
[Crosscheck クロスチェック.
引数に指定したファイルを読み込み、重複する行を集計します.
重複する行がある場合は、その行を出力します.
複数ファイルを指定した場合は、すべてのファイルに存在する行のみを出力します.
オプションで逆に、すべてのファイルに存在しない行を出力することもできます.
比較する行は、デフォルトでは行一致ですが、パスとみなして末尾から指定した数のトークン数で比較できます.
''')
# パスを短縮する数値指定オプション
parser.add_argument('-t', '--token', type=int, default=0,
help='パスの末尾からのトークン数、1はファイル名のみ、2は親ディレクトリ+ファイル、0はすべて')
parser.add_argument('-s', '--select', type=int, default=0,
help='出力するファイル名のソース元、0は共通、1以降はファイル引数の順序')
parser.add_argument('-m', '--match-mode', type=str, help='一致モード(match, unmatch, single, all, any)')
parser.add_argument('-v', '--verbose', action='store_true', help='詳細')
parser.add_argument('-0', '--print0', action='store_true', help='改行コードをnulにする')
parser.add_argument('files', nargs='*', help='ファイル名')
args = parser.parse_args()
files_len = len(args.files)
match_mode = args.match_mode if args.match_mode else 'match'
if match_mode == 'match' or match_mode == 'all':
selector = lambda fidxs: len(fidxs) == files_len
elif match_mode == 'unmatch' or match_mode == 'any':
selector = lambda fidxs: len(fidxs) != files_len
elif match_mode == 'single':
selector = lambda fidxs: len(fidxs) == 1
else:
# それ以外はエラーとする
print("Error: unknown match mode '%s'" % match_mode, file=sys.stderr)
sys.exit(1)
eol = os.linesep if not args.print0 else '\0'
contents = crosscheck(args.files, args.token)
crosscheck_print(contents, selector, select=args.select, verbose=args.verbose, eol=eol)
#!/bin/bash
# スクリプト自身の実際のパスを取得
SCRIPT_PATH=$(readlink -f "$0")
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
# Pythonスクリプトの名前(必要に応じて変更してください)
PYTHON_SCRIPT="crosscheck.py"
# Pythonスクリプトを実行
exec python3 "$SCRIPT_DIR/$PYTHON_SCRIPT" "$@"
/mnt/c/temp/111
/mnt/c/temp/222
/mnt/c/temp/333
/mnt/c/temp/444
./mnt2/c/temp/111
./mnt2/c/temp/333
./mnt2/c/temp/555
./mnt2/c/temp/666
.PHONY: test install uninstall
DESTDIR = ~
PREFIX = /bin
TARGET = crosscheck
SOURCE = crosscheck.py
WRAPPER = crosscheck_wrapper.sh
INSTALL_DIR = $(shell pwd)
test:
@echo "[Running tests]"
python3 ${SOURCE} -t 2 -s 1 data1.txt data2.txt
@echo "[Running tests with single mode]"
python3 ${SOURCE} -m single -t 2 -s 2 data1.txt data2.txt
install:
@echo "Installing..."
mkdir -p $(DESTDIR)$(PREFIX)
chmod a+x $(WRAPPER)
ln -sf $(INSTALL_DIR)/$(WRAPPER) $(DESTDIR)$(PREFIX)/$(TARGET)
@echo "Done!"
uninstall:
@echo "Uninstalling..."
rm -f $(DESTDIR)$(PREFIX)/$(TARGET)
@echo "Done!"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment