スクリプトファイルを追加

This commit is contained in:
2026-04-03 18:22:33 +09:00
commit 809d3bc3e4
14 changed files with 1956 additions and 0 deletions

View File

@@ -0,0 +1,324 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
複数フォルダ配下の PHP ファイルを比較し、A/B/C... の内容グループで Excel に結果を書き出すスクリプト。
- フォルダ一覧はテキストファイル1行: 「ラベル:/absolute/path」 形式。#始まりと空行は無視)
- 比較キーは既定で「ファイル名のみ」(--key name)。相対パスで比較する場合は --key relative。
- 改行コード差CRLF/LFは既定で無視 (--keep-eol を付けると無視しない)。
- --ignore-whitespace で空白/タブ/改行を全無視して比較。
- NEW: --ignore-comments で PHP コメント(//, #, /* */, /** */)を除去して比較。
- 進捗は「n/m 個目のフォルダ」「現在のファイル」を改行なしで表示。
- 出力: Excel (結果シート + 必要に応じて重複シート)
"""
import argparse
import os
from pathlib import Path
import hashlib
import re
from collections import defaultdict, OrderedDict
from typing import Dict, List, Tuple, Optional
from openpyxl import Workbook
from openpyxl.utils import get_column_letter
# -----------------------------
# 入力folders.txt読み込み
# -----------------------------
def read_folder_list(list_file: Path):
"""
folders.txt の形式:
ラベル:/absolute/path/to/folder
を読み込み、[(label, Path), ...] のリストを返す
"""
result = []
for line in list_file.read_text(encoding="utf-8").splitlines():
s = line.strip()
if not s or s.startswith("#"):
continue
if ":" not in s:
raise ValueError(f"ラベル:フォルダパス の形式で記述してください → {s}")
label, folder = s.split(":", 1)
label = label.strip()
folder = folder.strip()
p = Path(folder).expanduser().resolve()
if not p.exists() or not p.is_dir():
raise FileNotFoundError(f"フォルダが存在しません: {folder}")
result.append((label, p))
if not result:
raise ValueError("フォルダ一覧が空です。")
return result
# -----------------------------
# ファイル列挙
# -----------------------------
def iter_php_files(root: Path, recursive: bool = True):
"""root 配下の .php を列挙。戻り値: (ファイル名, 相対フォルダパス, 相対パス, 絶対パス)"""
if recursive:
for dirpath, _, filenames in os.walk(root):
dirpath_p = Path(dirpath)
for fn in filenames:
if fn.lower().endswith(".php"):
rel_path = dirpath_p.relative_to(root) / fn if dirpath_p != root else Path(fn)
rel_dir = rel_path.parent.as_posix() if str(rel_path) != fn else ""
yield (fn, rel_dir, rel_path.as_posix(), (dirpath_p / fn))
else:
for fn in os.listdir(root):
if fn.lower().endswith(".php"):
yield (fn, "", fn, (root / fn))
# -----------------------------
# コメント除去(--ignore-comments
# -----------------------------
_COMMENT_BLOCK = re.compile(r"/\*.*?\*/", flags=re.DOTALL) # /* ... */ と /** ... */
_COMMENT_SLASH_SLASH = re.compile(r"//.*?$", flags=re.MULTILINE) # // ...
_COMMENT_HASH = re.compile(r"#.*?$", flags=re.MULTILINE) # # ...
def strip_php_comments_from_bytes(b: bytes) -> bytes:
"""
PHP のコメント(//, #, /* */, /** */)を削除。
- バイナリ→latin1文字列で無損失に扱い、正規表現適用後に再度 bytes 化
- PHP タグ外のテキストでもパターンに合致すれば削除されます(必要に応じ高度化可能)
"""
s = b.decode("latin1")
s = _COMMENT_BLOCK.sub("", s)
s = _COMMENT_SLASH_SLASH.sub("", s)
s = _COMMENT_HASH.sub("", s)
return s.encode("latin1")
# -----------------------------
# 正規化EOL/空白/コメント)
# -----------------------------
def normalize_bytes_for_hash(
data: bytes,
keep_eol: bool,
ignore_whitespace: bool,
ignore_comments: bool,
) -> bytes:
"""比較前のバイト列正規化"""
b = data
# コメント無視先に行うことで、その後のEOL/空白処理の影響も受ける)
if ignore_comments:
try:
b = strip_php_comments_from_bytes(b)
except Exception:
# コメ除去中に失敗しても比較を止めない
pass
if not keep_eol:
# CRLF/LF/CR の差異を LF に正規化
b = b.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
if ignore_whitespace:
# 全ホワイトスペースを除去空白・タブ・LF
table = {ord(' '): None, ord('\t'): None, ord('\n'): None, ord('\r'): None}
s = b.decode('latin1') # バイト値を壊さないダミーエンコーディング
s = s.translate(table)
b = s.encode('latin1')
return b
def file_hash(
path: Path,
keep_eol: bool,
ignore_whitespace: bool,
ignore_comments: bool,
) -> str:
with path.open("rb") as f:
data = f.read()
data = normalize_bytes_for_hash(
data,
keep_eol=keep_eol,
ignore_whitespace=ignore_whitespace,
ignore_comments=ignore_comments,
)
return hashlib.sha256(data).hexdigest()
# -----------------------------
# ユーティリティ
# -----------------------------
def excel_letter_index(n: int) -> str:
"""
0 -> 'A', 1 -> 'B', ..., 25 -> 'Z', 26 -> 'AA' ... のようにグループ記号を生成
Excel列名と同様のロジック
"""
s = []
n += 1
while n > 0:
n, r = divmod(n - 1, 26)
s.append(chr(65 + r))
return "".join(reversed(s))
def auto_width(ws):
col_widths = defaultdict(int)
for row in ws.iter_rows(values_only=True):
for i, val in enumerate(row, start=1):
if val is None:
continue
length = len(str(val))
if length > col_widths[i]:
col_widths[i] = length
for i, w in col_widths.items():
ws.column_dimensions[get_column_letter(i)].width = min(max(w + 2, 10), 80)
# -----------------------------
# メイン
# -----------------------------
def main():
parser = argparse.ArgumentParser(description="複数フォルダの PHP ファイルを内容比較して Excel に出力します。")
parser.add_argument("--list", required=True, help="フォルダ一覧テキストファイルのパス1行: ラベル:/path")
parser.add_argument("--output", default="php_compare.xlsx", help="出力Excelファイル名既定: php_compare.xlsx")
parser.add_argument("--key", choices=["name", "relative"], default="name",
help="比較キー: name=ファイル名, relative=相対パス(既定: name")
parser.add_argument("--no-recursive", action="store_true", help="再帰を無効にして直下のみを対象")
parser.add_argument("--keep-eol", action="store_true", help="改行コードの違いを無視しない(厳密一致)")
parser.add_argument("--ignore-whitespace", action="store_true", help="空白/タブ/改行等のホワイトスペースを無視して比較")
parser.add_argument("--ignore-comments", action="store_true", help="PHPコメント//, #, /* */, /** */)を無視して比較")
args = parser.parse_args()
list_file = Path(args.list)
out_path = Path(args.output).resolve()
folder_items = read_folder_list(list_file)
labels = [item[0] for item in folder_items]
folders = [item[1] for item in folder_items]
recursive = not args.no_recursive
# 各フォルダごと: key -> (abs_path, rel_dir, file_name)
per_folder_maps: List[Dict[str, Tuple[Path, str, str]]] = []
duplicate_report: List[Tuple[int, str, List[str]]] = [] # (folder_idx, key, [abs paths])
total_folders = len(folders)
print(f"対象フォルダ数: {total_folders}")
# フォルダ走査 & ハッシュ計算
per_folder_hashes: List[Dict[str, str]] = [] # key -> hash
for idx, root in enumerate(folders, start=1):
mapping: Dict[str, Tuple[Path, str, str]] = {}
key_to_abslist: Dict[str, List[str]] = defaultdict(list)
files = list(iter_php_files(root, recursive=recursive))
total_files = len(files)
for j, (fn, rel_dir, rel_path, abs_path) in enumerate(files, start=1):
# 進捗を改行せずに出力
print(f"\r[{idx}/{total_folders}] {j}/{total_files} {fn}", end="", flush=True)
key = fn if args.key == "name" else rel_path
key_to_abslist[key].append(str(abs_path))
# 代表(最初に出会ったもの)を保持
if key not in mapping:
mapping[key] = (abs_path, rel_dir, fn)
# 重複(同一フォルダで同じ key が複数)を記録
for key, lst in key_to_abslist.items():
if len(lst) > 1:
duplicate_report.append((idx, key, lst))
# ハッシュ計算
key_hash: Dict[str, str] = {}
for key, (abs_path, _rel_dir, _fn) in mapping.items():
try:
h = file_hash(
abs_path,
keep_eol=args.keep_eol,
ignore_whitespace=args.ignore_whitespace,
ignore_comments=args.ignore_comments,
)
except Exception as e:
h = f"__ERROR__:{e}"
key_hash[key] = h
per_folder_maps.append(mapping)
per_folder_hashes.append(key_hash)
print("") # フォルダ行終端
# 全キー(ファイル名 or 相対パス)のユニオン
all_keys: List[str] = sorted(set().union(*[set(m.keys()) for m in per_folder_maps]))
# Excel ワークブック作成
wb = Workbook()
ws = wb.active
ws.title = "結果"
# ヘッダ行
headers = ["フォルダパス", "ファイル名"]
for label in labels:
headers.append(label)
ws.append(headers)
# 各キー(=行)ごとに A/B/C... を割り当て
for key in all_keys:
group_map: "OrderedDict[str, str]" = OrderedDict() # hash -> letter
next_group_idx = 0
# 代表(最初に見つかった場所の情報)
rep_rel_dir: Optional[str] = None
rep_file_name: Optional[str] = None
for m in per_folder_maps:
if key in m:
_abs, rel_dir, fn = m[key]
rep_rel_dir = rel_dir if rel_dir else ""
rep_file_name = fn
break
if rep_rel_dir is None:
rep_rel_dir = ""
if rep_file_name is None:
rep_file_name = key if args.key == "name" else Path(key).name
row = [rep_rel_dir, rep_file_name]
# 各フォルダ列
for (m, hmap) in zip(per_folder_maps, per_folder_hashes):
if key not in m:
row.append("")
continue
h = hmap.get(key)
if h is None:
row.append("")
continue
if h.startswith("__ERROR__"):
row.append("ERR")
continue
if h not in group_map:
group_map[h] = excel_letter_index(next_group_idx)
next_group_idx += 1
row.append(group_map[h])
ws.append(row)
# 重複シート(同一フォルダ内で同名 or 同相対パスが複数ある)
if duplicate_report:
ws2 = wb.create_sheet(title="重複")
ws2.append(["フォルダ番号(1)", "キーname/relative", "該当ファイル(絶対パス)"])
for folder_idx, key, paths in duplicate_report:
ws2.append([folder_idx, key, paths[0]])
for p in paths[1:]:
ws2.append(["", "", p])
auto_width(ws2)
auto_width(ws)
wb.save(out_path)
print(f"完了: {out_path}")
if __name__ == "__main__":
main()

26
compare_php/folders.txt Normal file
View File

@@ -0,0 +1,26 @@
Reserve2:C:\work\ReserveMart\src\Reserve3_src\public_html\core
nagahama:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\nagahama
moriyama:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\moriyama
terasupo:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\terasupo
ena:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\ena
Reserve2.2:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\Reserve2.2
makubetsu:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\makubetsu
sanjo:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\sanjo
toki:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\toki
city_date:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\city_date
kasamatsu:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\kasamatsu
kuwana:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\kuwana
uki:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\uki
motomiya:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\motomiya
yatsushiro:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\yatsushiro
oita:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\oita
tatebayashi:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\tatebayashi
hyogo:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\hyogo
kazakoshi:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\kazakoshi
shinagawa:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\shinagawa
meikyokudo:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\meikyokudo
studio-node:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\studio-node
yamori:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\yamori
zipang:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\zipang
katariba:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\katariba
shinjukumura:C:\work\ReserveMart\src\Reserve3_src\public_html\overlay\shinjukumura

Binary file not shown.

30
compare_php/readme.md Normal file
View File

@@ -0,0 +1,30 @@
# PHPファイル差分リスト作成スクリプト
## はじめに
指定したフォルダ配下同士でPHPの差分の有り無しを自動で調査するスクリプトです。
差分がないファイル同士は同じアルファベットで表記されています。
## 準備
- folders.txt
- 調査結果Excelに表示するラベル名とフォルダパスを設定する
- ラベル名:C:\work......
## 実行例
```
■ 基本(フォルダパスも考慮して比較するファイルを決定、改行空白を無視、コメントを無視)
python compare_php_folders.py --list folders.txt --output php_compare.xlsx --key relative --ignore-comments
■ 改行差も空白も無視(より寛容)
python compare_php_folders.py --list folders.txt --output php_compare.xlsx --key relative --ignore-comments --ignore-whitespace
■ 直下のみ探索(再帰なし)
python compare_php_folders.py --list folders.txt --no-recursive --ignore-comments
```
## 実行結果
比較結果はphp_compare.xlsxに生成もしくは上書きされる。
※実行前に本Excelファイルは閉じておくこと