187 lines
6.8 KiB
Python
187 lines
6.8 KiB
Python
import os
|
||
import sys
|
||
import traceback
|
||
import pandas as pd
|
||
from datetime import datetime
|
||
|
||
# === Вспомогательное ===
|
||
def get_script_dir() -> str:
|
||
try:
|
||
return os.path.dirname(os.path.abspath(__file__))
|
||
except NameError:
|
||
return os.getcwd()
|
||
|
||
def log(msg: str):
|
||
ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
line = f'[{ts}] {msg}'
|
||
print(line)
|
||
try:
|
||
with open(log_file, 'a', encoding='utf-8') as f:
|
||
f.write(line + '\n')
|
||
except Exception:
|
||
pass
|
||
|
||
def is_temp_or_hidden(name: str) -> bool:
|
||
return name.startswith('~$') or name.startswith('.')
|
||
|
||
def build_ozonart(df: pd.DataFrame) -> pd.DataFrame:
|
||
"""
|
||
Создаёт/перезаписывает колонку OZONart по правилу:
|
||
- если оба значения (Артикул, PartNumber) непустые -> 'Артикул-PartNumber'
|
||
- иначе -> ''
|
||
Под "пустым" понимаем NaN/None/пустая строка/строка из пробелов.
|
||
"""
|
||
if 'Артикул' not in df.columns or 'PartNumber' not in df.columns:
|
||
# Если вдруг где-то нет нужных колонок — оставим пустую OZONart и залогируем
|
||
if 'OZONart' not in df.columns:
|
||
df['OZONart'] = ''
|
||
log('ПРЕДУПРЕЖДЕНИЕ: Отсутствует колонка "Артикул" или "PartNumber" — OZONart заполнен пустыми значениями.')
|
||
return df
|
||
|
||
art = df['Артикул']
|
||
part = df['PartNumber']
|
||
|
||
# Признак непустоты: не NaN и не пустая строка после strip()
|
||
art_nonempty = art.notna() & art.astype(str).str.strip().ne('')
|
||
part_nonempty = part.notna() & part.astype(str).str.strip().ne('')
|
||
mask = art_nonempty & part_nonempty
|
||
|
||
# По умолчанию — пустые строки
|
||
ozon = pd.Series([''] * len(df), index=df.index, dtype=object)
|
||
# Там, где оба непустые — склейка
|
||
ozon.loc[mask] = art.loc[mask].astype(str).str.strip() + '-' + part.loc[mask].astype(str).str.strip()
|
||
|
||
df['OZONart'] = ozon
|
||
|
||
# Попробуем разместить OZONart сразу после PartNumber
|
||
try:
|
||
cols = list(df.columns)
|
||
if 'OZONart' in cols and 'PartNumber' in cols:
|
||
cols.remove('OZONart')
|
||
idx = cols.index('PartNumber') + 1
|
||
cols.insert(idx, 'OZONart')
|
||
df = df.reindex(columns=cols)
|
||
except Exception:
|
||
# Не критично — просто оставим как есть
|
||
pass
|
||
|
||
return df
|
||
|
||
# === Пути ===
|
||
script_dir = get_script_dir()
|
||
folder_path = os.path.join(script_dir, 'Files-todo')
|
||
|
||
timestamp = datetime.now().strftime('%Y%m%d-%H%M')
|
||
output_filename = f'All-todo-{timestamp}.xlsx'
|
||
output_file = os.path.join(folder_path, output_filename)
|
||
|
||
log_file = os.path.join(folder_path, 'merge_log.txt')
|
||
|
||
BASE_URL = "https://www.zarahome.com/pl/en/"
|
||
|
||
def main():
|
||
log('=== Старт объединения Excel-файлов ===')
|
||
log(f'Папка: {folder_path}')
|
||
|
||
if not os.path.isdir(folder_path):
|
||
log(f'ОШИБКА: Папка не найдена: {folder_path}')
|
||
sys.exit(1)
|
||
|
||
names = sorted(os.listdir(folder_path))
|
||
files = []
|
||
for name in names:
|
||
if is_temp_or_hidden(name):
|
||
continue
|
||
if not name.lower().endswith('.xlsx'):
|
||
continue
|
||
if name.startswith('All-todo-'):
|
||
continue
|
||
full = os.path.join(folder_path, name)
|
||
if os.path.isfile(full):
|
||
files.append(full)
|
||
|
||
if not files:
|
||
log('Нет входных .xlsx файлов для слияния. Завершение.')
|
||
return
|
||
|
||
log(f'Найдено файлов: {len(files)}')
|
||
|
||
dfs = []
|
||
all_columns = []
|
||
seen_cols = set()
|
||
processed = 0
|
||
errors = 0
|
||
|
||
for i, path in enumerate(files, start=1):
|
||
fname = os.path.basename(path)
|
||
try:
|
||
df = pd.read_excel(path, engine='openpyxl')
|
||
if df is None or df.empty:
|
||
log(f'ПРЕДУПРЕЖДЕНИЕ: Пустой файл — пропуск: {fname}')
|
||
continue
|
||
|
||
# Добавляем колонку OZONart согласно правилу
|
||
df = build_ozonart(df)
|
||
|
||
# Зафиксируем исходный порядок колонок первого удачного файла
|
||
if not all_columns:
|
||
for c in df.columns:
|
||
if c not in seen_cols:
|
||
all_columns.append(c)
|
||
seen_cols.add(c)
|
||
|
||
# Добавляем служебные источники
|
||
stem, _ = os.path.splitext(fname)
|
||
df['SourceFile'] = fname
|
||
df['SourceFileUrl'] = BASE_URL + stem
|
||
|
||
dfs.append(df)
|
||
processed += 1
|
||
log(f'[{i}/{len(files)}] OK: {fname} — строк: {len(df)}')
|
||
except Exception as e:
|
||
errors += 1
|
||
log(f'[{i}/{len(files)}] ОШИБКА: {fname}: {e}')
|
||
log(traceback.format_exc())
|
||
|
||
if not dfs:
|
||
log('Все файлы пустые или не прочитаны — нечего сохранять.')
|
||
return
|
||
|
||
combined = pd.concat(dfs, ignore_index=True)
|
||
|
||
# Порядок колонок:
|
||
# 1) как в первом удачном файле,
|
||
# 2) затем прочие (кроме служебных) по алфавиту,
|
||
# 3) затем служебные SourceFile и SourceFileUrl в конце.
|
||
extra_cols = [c for c in combined.columns if c not in all_columns and c not in ('SourceFile', 'SourceFileUrl')]
|
||
extra_cols.sort()
|
||
final_cols = all_columns + extra_cols
|
||
|
||
# Гарантируем размещение OZONart сразу после PartNumber, если они есть
|
||
if 'OZONart' in final_cols and 'PartNumber' in final_cols:
|
||
final_cols.remove('OZONart')
|
||
pn_idx = final_cols.index('PartNumber')
|
||
final_cols.insert(pn_idx + 1, 'OZONart')
|
||
|
||
if 'SourceFile' not in final_cols:
|
||
final_cols.append('SourceFile')
|
||
if 'SourceFileUrl' not in final_cols:
|
||
final_cols.append('SourceFileUrl')
|
||
|
||
combined = combined.reindex(columns=final_cols)
|
||
|
||
try:
|
||
with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
|
||
combined.to_excel(writer, index=False, sheet_name='Sheet')
|
||
log(f'Готово: {output_file}')
|
||
log(f'Итоговых строк: {len(combined)}')
|
||
log(f'Успешно обработано файлов: {processed}, ошибок: {errors}')
|
||
except Exception as e:
|
||
log(f'ОШИБКА сохранения {output_file}: {e}')
|
||
log(traceback.format_exc())
|
||
|
||
log('=== Готово ===')
|
||
|
||
if __name__ == '__main__':
|
||
main()
|