tweaked some code after looking at real images and added a readme
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
"""
|
||||
Step 3: Reorganize flat exported files into a proper folder structure.
|
||||
|
||||
Parses FCCS export filenames using known folder names and date anchors
|
||||
Parses FCCS export filenames using known folder templates and date anchors
|
||||
to reconstruct the original drawer/folder hierarchy.
|
||||
|
||||
Filename format:
|
||||
{drawer_id}_{client_name}_{folder_name}_{MM-DD-YYYY}_{document_name}.ext
|
||||
Example: 01069_SMITH, BOB_2007 Tax Documents_02-10-2009_2007 Form 1040A Page 1.pdf
|
||||
{drawer_id}_{client_name}_{folder_name}_{creation_date MM-DD-YYYY}_{document_name}.ext
|
||||
|
||||
Examples:
|
||||
01069_ABRAHAM, REBEKAH L._2025 Tax Documents_03-03-2026_030126 E-mail re Tax Info.pdf
|
||||
01069_ABRAHAM, REBEKAH L._Permanent File_02-24-2018_Driver's License.pdf
|
||||
01069_ABRAHAM, REBEKAH L._UltraTax CS 12-31-2008_02-12-2009_2008 Form 1040 Filing Instructions.doc
|
||||
|
||||
Output structure:
|
||||
output/{drawer_id}_{client_name}/{folder_name}/{original_filename}
|
||||
@@ -19,54 +23,66 @@ import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from fccs_config import parse_args, load_config, make_logger, load_folder_list
|
||||
from fccs_config import (
|
||||
parse_args, load_config, make_logger,
|
||||
load_folder_list, build_folder_patterns,
|
||||
)
|
||||
|
||||
# Regex for the creation date field (MM-DD-YYYY) bounded by underscores
|
||||
_DATE_RE = re.compile(r"_(\d{2}-\d{2}-\d{4})_")
|
||||
|
||||
|
||||
def parse_filename(filename, known_folders):
|
||||
def parse_filename(filename, folder_patterns):
|
||||
"""
|
||||
Parse an FCCS export filename into (drawer_id, client_name, folder_name,
|
||||
date, doc_name_with_ext) or return None if it cannot be parsed.
|
||||
|
||||
Strategy: anchor on drawer_id (left), date MM-DD-YYYY (middle), and match
|
||||
a known folder name between client_name and date.
|
||||
folder_patterns is a list of (compiled_regex, template_name) from
|
||||
build_folder_patterns(), sorted longest-first.
|
||||
|
||||
Strategy:
|
||||
1. Drawer ID: first token before first underscore
|
||||
2. Creation date: find MM-DD-YYYY pattern as anchor
|
||||
3. Folder name: regex-match a known folder template at the end of
|
||||
the text between client_name and creation_date
|
||||
4. Client name: whatever is left between drawer_id and folder_name
|
||||
"""
|
||||
stem, ext = os.path.splitext(filename)
|
||||
|
||||
# Drawer ID is always the first underscore-delimited token
|
||||
# 1. Drawer ID — first underscore-delimited token
|
||||
sep = stem.find("_")
|
||||
if sep == -1:
|
||||
return None
|
||||
drawer_id = stem[:sep]
|
||||
rest = stem[sep + 1:]
|
||||
|
||||
# Find all date-pattern occurrences (MM-DD-YYYY) in rest
|
||||
# We search with a leading underscore context to ensure proper boundaries
|
||||
date_pattern = re.compile(r"_(\d{2}-\d{2}-\d{4})_")
|
||||
# Prepend underscore so the first potential date at position 0 of rest is caught
|
||||
# 2. Find all creation date candidates (MM-DD-YYYY bounded by underscores)
|
||||
# Prepend underscore so a date right at the start of `rest` is also found
|
||||
search_str = "_" + rest
|
||||
matches = list(date_pattern.finditer(search_str))
|
||||
|
||||
if not matches:
|
||||
date_matches = list(_DATE_RE.finditer(search_str))
|
||||
if not date_matches:
|
||||
return None
|
||||
|
||||
# Try each date match; known_folders is already sorted longest-first
|
||||
for m in matches:
|
||||
# 3. Try each date match; for each, try to match a known folder template
|
||||
for m in date_matches:
|
||||
date_str = m.group(1)
|
||||
# Position in `rest` where date starts (adjust for prepended _)
|
||||
date_start_in_rest = m.start() - 1 # -1 for the prepended _
|
||||
# But the match includes the leading _, so the actual content before date:
|
||||
before_date = rest[:date_start_in_rest]
|
||||
after_date = rest[date_start_in_rest + len(date_str) + 1:] # +1 for trailing _
|
||||
# Calculate positions relative to `rest` (adjust for prepended _)
|
||||
# m.start() is the position of the leading _ in search_str
|
||||
# In `rest`, the content before the date ends at (m.start() - 1)
|
||||
before_date = rest[: m.start() - 1]
|
||||
after_date_pos = m.start() - 1 + len("_") + len(date_str) + len("_")
|
||||
doc_name = rest[after_date_pos:]
|
||||
|
||||
# Try to match a known folder at the end of before_date
|
||||
for folder in known_folders:
|
||||
if before_date.endswith(folder):
|
||||
# Check there's an underscore separator before the folder name
|
||||
prefix_end = len(before_date) - len(folder)
|
||||
if prefix_end > 0 and before_date[prefix_end - 1] == "_":
|
||||
client_name = before_date[: prefix_end - 1]
|
||||
doc_name = after_date + ext
|
||||
return (drawer_id, client_name, folder, date_str, doc_name)
|
||||
# Try each folder pattern against the end of before_date
|
||||
for pattern, template_name in folder_patterns:
|
||||
match = pattern.search(before_date)
|
||||
if match and match.end() == len(before_date):
|
||||
# Folder matched at the end — check for underscore separator before it
|
||||
folder_start = match.start()
|
||||
if folder_start > 0 and before_date[folder_start - 1] == "_":
|
||||
client_name = before_date[: folder_start - 1]
|
||||
folder_name = match.group() # the actual expanded name
|
||||
return (drawer_id, client_name, folder_name, date_str, doc_name + ext)
|
||||
|
||||
return None
|
||||
|
||||
@@ -84,11 +100,11 @@ def main():
|
||||
log(f"ERROR: export directory not found: {export_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
known_folders = load_folder_list(folder_list_path)
|
||||
if not known_folders:
|
||||
log("WARNING: folder list is empty. All files will go to _unparsed/.")
|
||||
# Sort longest-first to prevent partial matches
|
||||
known_folders.sort(key=len, reverse=True)
|
||||
folder_templates = load_folder_list(folder_list_path)
|
||||
if not folder_templates:
|
||||
log("WARNING: folder list is empty. Only built-in patterns (UltraTax CS) will match.")
|
||||
folder_patterns = build_folder_patterns(folder_templates)
|
||||
log(f"Loaded {len(folder_patterns)} folder patterns")
|
||||
|
||||
files = [
|
||||
f for f in os.listdir(export_dir)
|
||||
@@ -101,7 +117,7 @@ def main():
|
||||
failed = 0
|
||||
|
||||
for filename in files:
|
||||
result = parse_filename(filename, known_folders)
|
||||
result = parse_filename(filename, folder_patterns)
|
||||
|
||||
if result is None:
|
||||
log(f" UNPARSED: {filename}")
|
||||
|
||||
Reference in New Issue
Block a user