Excel tool im Browser
This commit is contained in:
698
streamlit_app.py
Normal file
698
streamlit_app.py
Normal file
@@ -0,0 +1,698 @@
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
import re
|
||||
from io import BytesIO
|
||||
from typing import List, Dict, Any, Optional
|
||||
import time
|
||||
|
||||
# Seitenkonfiguration
|
||||
st.set_page_config(
|
||||
page_title="Excel Filter Tool",
|
||||
page_icon=None,
|
||||
layout="wide",
|
||||
initial_sidebar_state="expanded"
|
||||
)
|
||||
|
||||
# --- Regex Bausteine ---
|
||||
REGEX_BRICKS = {
|
||||
"Exakter Text": {
|
||||
"regex": "{}",
|
||||
"desc": "Findet den exakten Text, den du eingibst. Sonderzeichen werden automatisch maskiert.",
|
||||
"needs_input": True,
|
||||
"allows_quantifier": True
|
||||
},
|
||||
"Ziffer (0-9)": {
|
||||
"regex": r"\d",
|
||||
"desc": "Findet eine einzelne Ziffer von 0 bis 9.",
|
||||
"needs_input": False,
|
||||
"allows_quantifier": True
|
||||
},
|
||||
"Buchstabe (A-Z, a-z)": {
|
||||
"regex": r"[a-zA-Z]",
|
||||
"desc": "Findet einen einzelnen Buchstaben des deutschen Alphabets, sowohl Groß- als auch Kleinschreibung. Achtung, gilt nicht für Umlaute!",
|
||||
"needs_input": False,
|
||||
"allows_quantifier": True
|
||||
},
|
||||
"Leerzeichen": {
|
||||
"regex": r"\s",
|
||||
"desc": "Findet Leerzeichen, Tabulatoren und Zeilenumbrüche.",
|
||||
"needs_input": False,
|
||||
"allows_quantifier": True
|
||||
},
|
||||
"Beliebiges einzelnes Zeichen": {
|
||||
"regex": r".",
|
||||
"desc": "Findet genau ein beliebiges Zeichen (Buchstabe, Ziffer, Symbol oder Leerzeichen).",
|
||||
"needs_input": False,
|
||||
"allows_quantifier": True
|
||||
},
|
||||
"Beliebige Zeichenfolge": {
|
||||
"regex": r".*",
|
||||
"desc": "Findet null oder mehr beliebige Zeichen. Nützlich als breiter Platzhalter.",
|
||||
"needs_input": False,
|
||||
"allows_quantifier": False
|
||||
},
|
||||
"ODER (Alternative)": {
|
||||
"regex": r"|",
|
||||
"desc": "Funktioniert als logischer ODER-Operator. Das Muster findet entweder den Ausdruck davor oder danach.",
|
||||
"needs_input": False,
|
||||
"allows_quantifier": False
|
||||
},
|
||||
"Zeilenanfang": {
|
||||
"regex": r"^",
|
||||
"desc": "Verankert den Treffer am Anfang einer Zeile oder Zeichenkette.",
|
||||
"needs_input": False,
|
||||
"allows_quantifier": False
|
||||
},
|
||||
"Zeilenende": {
|
||||
"regex": r"$",
|
||||
"desc": "Verankert den Treffer am Ende einer Zeile oder Zeichenkette.",
|
||||
"needs_input": False,
|
||||
"allows_quantifier": False
|
||||
}
|
||||
}
|
||||
|
||||
QUANTIFIERS = {
|
||||
"Genau 1 (Standard)": "",
|
||||
"1 oder mehr (+)": "+",
|
||||
"0 oder mehr (*)": "*",
|
||||
"Optional: 0 oder 1 (?)": "?"
|
||||
}
|
||||
|
||||
def get_pattern_presets() -> Dict[str, str]:
|
||||
return {
|
||||
"Fehler & Warnungen": r"error|warning|critical|fehler|warnung",
|
||||
"Nur Fehler": r"error|fehler",
|
||||
"Nur Warnungen": r"warning|warnung",
|
||||
"Kritische Fehler": r"critical|kritisch",
|
||||
"E-Mail-Adressen": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
|
||||
"Telefonnummern": r"\+?[0-9\s-]{10,}",
|
||||
"Datum (JJJJ-MM-TT)": r"\d{4}-\d{2}-\d{2}",
|
||||
}
|
||||
|
||||
def init_session_state():
|
||||
defaults = {
|
||||
"df": None,
|
||||
"sheets": [],
|
||||
"selected_sheet": None,
|
||||
"columns": [],
|
||||
"filtered_df": None,
|
||||
"stats": None,
|
||||
"regex_enabled": True,
|
||||
"numeric_enabled": False,
|
||||
"column_selection_enabled": False,
|
||||
"selected_columns": [],
|
||||
"regex_pattern": "",
|
||||
"regex_test_text": "",
|
||||
"regex_blocks": [],
|
||||
"temp_block_val": "",
|
||||
"temp_quantifier": "Genau 1 (Standard)"
|
||||
}
|
||||
for key, val in defaults.items():
|
||||
if key not in st.session_state:
|
||||
st.session_state[key] = val
|
||||
|
||||
def load_excel_file(uploaded_file) -> tuple:
|
||||
try:
|
||||
file_bytes = BytesIO(uploaded_file.getvalue())
|
||||
xls = pd.ExcelFile(file_bytes)
|
||||
sheets = xls.sheet_names
|
||||
file_bytes.seek(0)
|
||||
df = pd.read_excel(file_bytes, sheet_name=sheets[0])
|
||||
return df, sheets, None
|
||||
except Exception as e:
|
||||
return None, [], str(e)
|
||||
|
||||
def apply_filters(df: pd.DataFrame, pattern: Optional[str] = None, regex_column: Optional[str] = None, numeric_filter: Optional[Dict[str, Any]] = None, selected_columns: Optional[List[str]] = None) -> tuple:
|
||||
start_time = time.time()
|
||||
input_rows = len(df)
|
||||
input_columns = len(df.columns)
|
||||
filtered_df = df.copy()
|
||||
filters_applied = []
|
||||
|
||||
if pattern and pattern.strip():
|
||||
try:
|
||||
columns_to_search = [regex_column] if regex_column and regex_column != "Alle Spalten" else df.columns.tolist()
|
||||
regex = re.compile(pattern, re.IGNORECASE)
|
||||
mask = filtered_df.apply(lambda row: any(regex.search(str(row[col])) for col in columns_to_search if col in row and pd.notna(row[col])), axis=1)
|
||||
filtered_df = filtered_df[mask]
|
||||
filters_applied.append("Regex")
|
||||
except re.error as e:
|
||||
return None, None, f"Ungültiges Regex-Muster: {e}"
|
||||
|
||||
if numeric_filter and numeric_filter.get("column"):
|
||||
try:
|
||||
column = numeric_filter["column"]
|
||||
operator = numeric_filter["operator"]
|
||||
value = numeric_filter["value"]
|
||||
|
||||
if column == "Alle Spalten":
|
||||
combined_mask = pd.Series([False] * len(filtered_df), index=filtered_df.index)
|
||||
for col in filtered_df.columns:
|
||||
num_series = pd.to_numeric(filtered_df[col], errors='coerce')
|
||||
col_mask = eval(f"num_series {operator} value")
|
||||
combined_mask = combined_mask | col_mask
|
||||
filtered_df = filtered_df[combined_mask]
|
||||
else:
|
||||
num_series = pd.to_numeric(filtered_df[column], errors='coerce')
|
||||
filtered_df = filtered_df[eval(f"num_series {operator} value")]
|
||||
filters_applied.append("Numerisch")
|
||||
except Exception as e:
|
||||
return None, None, f"Fehler beim Anwenden des numerischen Filters: {e}"
|
||||
|
||||
if selected_columns:
|
||||
available_columns = [col for col in selected_columns if col in filtered_df.columns]
|
||||
if available_columns:
|
||||
filtered_df = filtered_df[available_columns]
|
||||
|
||||
end_time = time.time()
|
||||
stats = {
|
||||
"input_rows": input_rows,
|
||||
"input_columns": input_columns,
|
||||
"output_rows": len(filtered_df),
|
||||
"output_columns": len(filtered_df.columns),
|
||||
"rows_removed": input_rows - len(filtered_df),
|
||||
"processing_time": end_time - start_time,
|
||||
"filters_applied": filters_applied,
|
||||
"retention_rate": (len(filtered_df) / input_rows * 100) if input_rows > 0 else 0
|
||||
}
|
||||
return filtered_df, stats, None
|
||||
|
||||
def explain_regex_german(blocks: List[Dict]) -> str:
|
||||
"""Übersetzt die Regex-Bausteine in einen deutschen Satz."""
|
||||
if not blocks:
|
||||
return "Muster ist leer."
|
||||
|
||||
explanations = []
|
||||
for block in blocks:
|
||||
b_type = block["key"]
|
||||
val = block.get("value", "")
|
||||
q_key = block.get("quantifier_key", "Genau 1 (Standard)")
|
||||
|
||||
# 1. Grundbegriff
|
||||
if "Exakter Text" in b_type: noun = f"den exakten Text '{val}'"
|
||||
elif "Ziffer" in b_type: noun = "eine Ziffer (0-9)"
|
||||
elif "Buchstabe" in b_type: noun = "einen Buchstaben (A-Z oder a-z)"
|
||||
elif "Leerzeichen" in b_type: noun = "ein Leerzeichen"
|
||||
elif "Beliebiges einzelnes Zeichen" in b_type: noun = "ein beliebiges einzelnes Zeichen"
|
||||
elif "Beliebige Zeichenfolge" in b_type: noun = "eine beliebige Zeichenfolge"
|
||||
elif "ODER" in b_type: noun = "ODER"
|
||||
elif "Zeilenanfang" in b_type: noun = "den Anfang der Zeichenkette"
|
||||
elif "Zeilenende" in b_type: noun = "das Ende der Zeichenkette"
|
||||
else: noun = "ein Element"
|
||||
|
||||
# 2. Quantoren anwenden
|
||||
if noun not in ["ODER", "den Anfang der Zeichenkette", "das Ende der Zeichenkette", "eine beliebige Zeichenfolge"]:
|
||||
if "1 oder mehr" in q_key:
|
||||
noun = f"eine oder mehr {noun.replace('eine ', '').replace('einen ', '').replace('ein ', '').replace('den ', '').replace('das ', '')}en" if any(noun.startswith(x) for x in ["eine ", "einen ", "ein ", "den ", "das "]) else f"ein oder mehr {noun}"
|
||||
elif "0 oder mehr" in q_key:
|
||||
noun = f"null oder mehr {noun.replace('eine ', '').replace('einen ', '').replace('ein ', '').replace('den ', '').replace('das ', '')}en" if any(noun.startswith(x) for x in ["eine ", "einen ", "ein ", "den ", "das "]) else f"null oder mehr {noun}"
|
||||
elif "Optional" in q_key:
|
||||
noun = f"ein optionales {noun.replace('eine ', '').replace('einen ', '').replace('ein ', '').replace('den ', '').replace('das ', '')}" if any(noun.startswith(x) for x in ["eine ", "einen ", "ein ", "den ", "das "]) else f"ein optionales {noun}"
|
||||
|
||||
explanations.append(noun)
|
||||
|
||||
# 3. Zusammenfügen
|
||||
sentence = ""
|
||||
for i, exp in enumerate(explanations):
|
||||
if i == 0:
|
||||
sentence += exp
|
||||
else:
|
||||
if exp == "ODER" or explanations[i-1] == "ODER":
|
||||
sentence += f" {exp} "
|
||||
else:
|
||||
sentence += f", gefolgt von {exp}"
|
||||
|
||||
return sentence[:1].upper() + sentence[1:] + "."
|
||||
|
||||
def apply_sleek_dark_theme():
|
||||
st.markdown("""
|
||||
<style>
|
||||
h1, h2, h3 { font-weight: 500 !important; letter-spacing: -0.5px; }
|
||||
.stApp { background-color: #0F1115; color: #FAFAFA; }
|
||||
|
||||
.stTextInput>div>div>input,
|
||||
.stSelectbox>div>div>div,
|
||||
.stNumberInput>div>div>input,
|
||||
.stTextArea>div>div>textarea {
|
||||
background-color: #1E2028 !important;
|
||||
border: 1px solid #2D3342 !important;
|
||||
color: #FAFAFA !important;
|
||||
border-radius: 6px !important;
|
||||
padding: 8px 12px !important;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #2D3342;
|
||||
}
|
||||
|
||||
.success-box {
|
||||
background-color: #1A3B2B;
|
||||
color: #4ADE80;
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid #224D38;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background-color: #1a233a;
|
||||
color: #8fa1c4;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #2d3748;
|
||||
}
|
||||
|
||||
.translation-box {
|
||||
background-color: #2D241E;
|
||||
color: #E29A5B;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid #5C3D22;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.block-brick {
|
||||
background-color: #1E2028;
|
||||
border: 1px solid #3B4252;
|
||||
border-left: 4px solid #F03E3E;
|
||||
color: #FAFAFA;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
[data-testid="stSidebar"] {
|
||||
background-color: #161A23 !important;
|
||||
border-right: 1px solid #2D3342;
|
||||
}
|
||||
hr { border-color: #2D3342 !important; }
|
||||
|
||||
[data-testid="stMetric"] {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
[data-testid="stMetricValue"] { color: #FFFFFF !important; font-size: 1.8rem !important; }
|
||||
|
||||
.stTabs [data-baseweb="tab-list"] { gap: 24px; background-color: transparent; }
|
||||
.stTabs [data-baseweb="tab"] { color: #A0AEC0; padding-top: 10px; padding-bottom: 10px; }
|
||||
.stTabs [aria-selected="true"] { color: #FFFFFF !important; }
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
def render_pipeline_tab():
|
||||
st.markdown('<div class="section-header">Schritt 1: Datei auswählen</div>', unsafe_allow_html=True)
|
||||
uploaded_file = st.file_uploader("Lade eine Excel-Datei hoch (.xlsx, .xls)", type=["xlsx", "xls"], label_visibility="collapsed")
|
||||
|
||||
if uploaded_file:
|
||||
if uploaded_file != st.session_state.get("last_uploaded"):
|
||||
st.session_state.last_uploaded = uploaded_file
|
||||
with st.spinner("Lade..."):
|
||||
df, sheets, error = load_excel_file(uploaded_file)
|
||||
if error:
|
||||
st.error(f"Fehler: {error}")
|
||||
else:
|
||||
st.session_state.df = df
|
||||
st.session_state.sheets = sheets
|
||||
st.session_state.selected_sheet = sheets[0]
|
||||
st.session_state.columns = df.columns.tolist()
|
||||
st.session_state.filtered_df = None
|
||||
st.session_state.stats = None
|
||||
st.session_state.selected_columns = list(df.columns)
|
||||
|
||||
st.markdown(f'<div class="success-box">Datei geladen: {uploaded_file.name}</div>', unsafe_allow_html=True)
|
||||
col1, _ = st.columns([1, 2])
|
||||
with col1:
|
||||
current_idx = st.session_state.sheets.index(st.session_state.selected_sheet) if st.session_state.selected_sheet in st.session_state.sheets else 0
|
||||
selected_sheet = st.selectbox("Arbeitsblatt auswählen", st.session_state.sheets, index=current_idx)
|
||||
if selected_sheet != st.session_state.selected_sheet:
|
||||
st.session_state.selected_sheet = selected_sheet
|
||||
st.session_state.df = pd.read_excel(BytesIO(st.session_state.last_uploaded.getvalue()), sheet_name=selected_sheet)
|
||||
st.session_state.columns = st.session_state.df.columns.tolist()
|
||||
st.session_state.selected_columns = list(st.session_state.df.columns)
|
||||
st.rerun()
|
||||
|
||||
if st.session_state.df is not None:
|
||||
st.markdown('<div class="section-header">Schritt 2: Filter konfigurieren</div>', unsafe_allow_html=True)
|
||||
filter_tabs = st.tabs(["Regex-Filter", "Numerischer Filter", "Spaltenauswahl"])
|
||||
|
||||
with filter_tabs[0]:
|
||||
st.write("")
|
||||
st.session_state.regex_enabled = st.checkbox("Regex-Filter aktivieren", value=st.session_state.regex_enabled)
|
||||
if st.session_state.regex_enabled:
|
||||
col1, col2 = st.columns([2, 1])
|
||||
with col1:
|
||||
presets = get_pattern_presets()
|
||||
preset_names = ["-- Vorlagen --"] + list(presets.keys())
|
||||
def on_preset_change():
|
||||
sel = st.session_state.preset_selector
|
||||
if sel != preset_names[0] and sel in presets:
|
||||
st.session_state.regex_pattern = presets[sel]
|
||||
st.selectbox("Vorlage laden", preset_names, key="preset_selector", on_change=on_preset_change)
|
||||
st.session_state.regex_pattern = st.text_input("Aktives Regex-Muster (Nutze den 'Regex-Builder' zum visuellen Erstellen)", value=st.session_state.regex_pattern, placeholder="z.B. ^Fehler.*")
|
||||
with col2:
|
||||
regex_column = st.selectbox("Spalte für Regex-Filter", ["Alle Spalten"] + st.session_state.columns)
|
||||
|
||||
with filter_tabs[1]:
|
||||
st.write("")
|
||||
st.session_state.numeric_enabled = st.checkbox("Numerischen Filter aktivieren", value=st.session_state.numeric_enabled)
|
||||
if st.session_state.numeric_enabled:
|
||||
col1, col2, col3 = st.columns([2, 2, 1])
|
||||
with col1:
|
||||
numeric_column = st.selectbox("Spalte", ["Alle Spalten"] + st.session_state.columns)
|
||||
with col2:
|
||||
ops = {">": ">", "<": "<", ">=": ">=", "<=": "<=", "=": "="}
|
||||
selected_op = st.selectbox("Vergleichsoperator", list(ops.values()))
|
||||
numeric_operator = [k for k, v in ops.items() if v == selected_op][0]
|
||||
with col3:
|
||||
numeric_value = st.text_input("Wert")
|
||||
|
||||
with filter_tabs[2]:
|
||||
st.write("")
|
||||
st.session_state.column_selection_enabled = st.checkbox("Spaltenauswahl aktivieren", value=st.session_state.column_selection_enabled)
|
||||
if st.session_state.column_selection_enabled:
|
||||
col_btn1, col_btn2, _ = st.columns([1, 1, 3])
|
||||
with col_btn1:
|
||||
if st.button("Alle auswählen", use_container_width=True):
|
||||
st.session_state.selected_columns = list(st.session_state.columns)
|
||||
st.rerun()
|
||||
with col_btn2:
|
||||
if st.button("Alle abwählen", use_container_width=True):
|
||||
st.session_state.selected_columns = []
|
||||
st.rerun()
|
||||
st.session_state.selected_columns = st.multiselect("Spalten auswählen", st.session_state.columns, default=st.session_state.get("selected_columns", st.session_state.columns))
|
||||
|
||||
st.markdown('<div class="section-header">Schritt 3: Ergebnisse & Export</div>', unsafe_allow_html=True)
|
||||
if st.button("Filter anwenden", type="primary"):
|
||||
val_error = None
|
||||
if st.session_state.regex_enabled and not st.session_state.regex_pattern: val_error = "Bitte gib ein Regex-Muster ein"
|
||||
if st.session_state.numeric_enabled and not numeric_value: val_error = "Bitte gib einen numerischen Wert ein"
|
||||
|
||||
if val_error:
|
||||
st.error(val_error)
|
||||
else:
|
||||
num_dict = {"column": numeric_column, "operator": numeric_operator, "value": float(numeric_value)} if (st.session_state.numeric_enabled and numeric_value) else None
|
||||
cols = st.session_state.selected_columns if st.session_state.column_selection_enabled else None
|
||||
with st.spinner("Verarbeite..."):
|
||||
filtered_df, stats, err = apply_filters(st.session_state.df, pattern=st.session_state.regex_pattern if st.session_state.regex_enabled else None, regex_column=regex_column if st.session_state.regex_enabled else None, numeric_filter=num_dict, selected_columns=cols)
|
||||
if err:
|
||||
st.error(f"Fehler: {err}")
|
||||
else:
|
||||
st.session_state.filtered_df = filtered_df
|
||||
st.session_state.stats = stats
|
||||
st.success("Filter erfolgreich angewendet.")
|
||||
|
||||
if st.session_state.filtered_df is not None and st.session_state.stats is not None:
|
||||
st.divider()
|
||||
sc1, sc2, sc3, sc4 = st.columns(4)
|
||||
sc1.metric("Eingabezeilen", f"{st.session_state.stats['input_rows']:,}")
|
||||
sc2.metric("Ausgabezeilen", f"{st.session_state.stats['output_rows']:,}")
|
||||
sc3.metric("Entfernte Zeilen", f"{st.session_state.stats['rows_removed']:,}")
|
||||
sc4.metric("Trefferquote", f"{st.session_state.stats['retention_rate']:.1f}%")
|
||||
|
||||
st.write("")
|
||||
st.dataframe(st.session_state.filtered_df.head(100), use_container_width=True, height=300)
|
||||
st.caption(f"Zeige {min(100, len(st.session_state.filtered_df))} von {len(st.session_state.filtered_df)} Zeilen")
|
||||
|
||||
out_buf = BytesIO()
|
||||
with pd.ExcelWriter(out_buf, engine='openpyxl') as writer:
|
||||
st.session_state.filtered_df.to_excel(writer, index=False)
|
||||
out_buf.seek(0)
|
||||
st.download_button("Gefilterte Datei herunterladen", out_buf, "gefilterte_ausgabe.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", type="primary")
|
||||
|
||||
def render_regex_builder_tab():
|
||||
st.markdown('<div class="section-header">Visueller Regex-Builder</div>', unsafe_allow_html=True)
|
||||
st.markdown("Erstelle Suchmuster durch Kombinieren von modularen Bausteinen. Das Muster wird von oben nach unten aufgebaut.")
|
||||
st.write("")
|
||||
|
||||
col_build, col_test = st.columns([1.2, 1])
|
||||
|
||||
with col_build:
|
||||
st.markdown("### 1. Bausteine hinzufügen")
|
||||
|
||||
selected_block_key = st.selectbox("Wähle einen Bausteintyp:", list(REGEX_BRICKS.keys()))
|
||||
block_data = REGEX_BRICKS[selected_block_key]
|
||||
|
||||
st.markdown(f'<div class="info-box"><b>Beschreibung:</b> {block_data["desc"]}</div>', unsafe_allow_html=True)
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
if block_data["needs_input"]:
|
||||
st.text_input("Text zum Suchen:", placeholder="z.B. Fehler", key="temp_block_val")
|
||||
else:
|
||||
st.session_state.temp_block_val = ""
|
||||
with col2:
|
||||
if block_data["allows_quantifier"]:
|
||||
st.selectbox("Wie oft:", list(QUANTIFIERS.keys()), key="temp_quantifier")
|
||||
else:
|
||||
st.session_state.temp_quantifier = "Genau 1 (Standard)"
|
||||
|
||||
def add_block_callback():
|
||||
st.session_state.regex_blocks.append({
|
||||
"key": selected_block_key,
|
||||
"value": st.session_state.temp_block_val if block_data["needs_input"] else "",
|
||||
"regex_format": block_data["regex"],
|
||||
"quantifier_key": st.session_state.temp_quantifier if block_data["allows_quantifier"] else "Genau 1 (Standard)"
|
||||
})
|
||||
st.session_state.temp_block_val = ""
|
||||
st.session_state.temp_quantifier = "Genau 1 (Standard)"
|
||||
|
||||
st.button("Baustein hinzufügen", use_container_width=True, on_click=add_block_callback)
|
||||
|
||||
st.write("---")
|
||||
st.markdown("### 2. Bausteine als Reihe")
|
||||
if not st.session_state.regex_blocks:
|
||||
st.info("Die Bausteinreihe ist leer. Wähle oben einen Baustein und klicke auf Hinzufügen.")
|
||||
else:
|
||||
for i, block in enumerate(st.session_state.regex_blocks):
|
||||
c1, c2 = st.columns([5, 1])
|
||||
with c1:
|
||||
val_display = f" <code style='color:#F03E3E; background:transparent;'>{block['value']}</code>" if block['value'] else ""
|
||||
q_key = block.get('quantifier_key', 'Genau 1 (Standard)')
|
||||
q_display = f" <span style='color:#8fa1c4; font-size: 0.8em;'>[ <i>{q_key}</i> ]</span>" if q_key != "Genau 1 (Standard)" else ""
|
||||
|
||||
st.markdown(f'<div class="block-brick">{block["key"]}{val_display}{q_display}</div>', unsafe_allow_html=True)
|
||||
with c2:
|
||||
if st.button("Entfernen", key=f"del_block_{i}", help="Diesen Baustein entfernen"):
|
||||
st.session_state.regex_blocks.pop(i)
|
||||
st.rerun()
|
||||
|
||||
# Kompiliere die visuellen Bausteine zu echtem Regex-Code
|
||||
regex_parts = []
|
||||
for block in st.session_state.regex_blocks:
|
||||
q_val = QUANTIFIERS[block.get("quantifier_key", "Genau 1 (Standard)")]
|
||||
if block["value"]:
|
||||
safe_val = re.escape(block["value"])
|
||||
if q_val:
|
||||
regex_parts.append(f"(?:{safe_val}){q_val}")
|
||||
else:
|
||||
regex_parts.append(safe_val)
|
||||
else:
|
||||
regex_parts.append(f"{block['regex_format']}{q_val}")
|
||||
|
||||
generated_regex = "".join(regex_parts)
|
||||
|
||||
with col_test:
|
||||
st.markdown("### 3. Muster testen")
|
||||
|
||||
st.markdown("**Muster-Erklärung:**")
|
||||
german_translation = explain_regex_german(st.session_state.regex_blocks)
|
||||
st.markdown(f'<div class="translation-box">"{german_translation}"</div>', unsafe_allow_html=True)
|
||||
|
||||
st.markdown("**Generiertes Regex:**")
|
||||
st.code(generated_regex if generated_regex else "(leer)", language="regex")
|
||||
|
||||
test_text = st.text_input("Testtext", value=st.session_state.regex_test_text, placeholder="Gib einen Beispieltext ein...")
|
||||
st.session_state.regex_test_text = test_text
|
||||
|
||||
test_btn = st.button("Muster testen", use_container_width=True)
|
||||
|
||||
if (test_btn or test_text) and generated_regex and test_text:
|
||||
try:
|
||||
regex = re.compile(generated_regex, re.IGNORECASE)
|
||||
matches = regex.findall(test_text)
|
||||
|
||||
if matches:
|
||||
st.success(f"{len(matches)} Treffer gefunden.")
|
||||
highlighted = test_text
|
||||
for match in set(matches):
|
||||
if match:
|
||||
highlighted = highlighted.replace(match, f"**{match}**")
|
||||
st.markdown(f"> {highlighted}")
|
||||
else:
|
||||
st.warning("Keine Treffer gefunden. Passe dein Muster oder den Testtext an.")
|
||||
except re.error:
|
||||
st.error("Ungültige Mustersyntax. Vervollständige die Musterfolge.")
|
||||
|
||||
st.write("")
|
||||
if st.button("Auf Excel-Pipeline anwenden", type="primary", use_container_width=True):
|
||||
if generated_regex:
|
||||
st.session_state.regex_pattern = generated_regex
|
||||
st.success("Muster angewendet. Wechsle zum Tab 'Excel-Filter' um deine Daten zu filtern.")
|
||||
else:
|
||||
st.error("Kann kein leeres Muster anwenden.")
|
||||
|
||||
def render_help_tab():
|
||||
st.markdown('<div class="section-header">Hilfe</div>', unsafe_allow_html=True)
|
||||
|
||||
st.markdown("""
|
||||
|
||||
### Tab: Excel-Pipeline
|
||||
|
||||
**Schritt 1: Datei auswählen**
|
||||
- Lade Excel-Dateien im Format `.xlsx` oder `.xls` hoch
|
||||
- Wähle das zu verarbeitende Arbeitsblatt, falls die Datei mehrere enthält
|
||||
|
||||
**Schritt 2: Filter konfigurieren**
|
||||
|
||||
*Regex-Filter:*
|
||||
- Gib ein reguläres Ausdrucksmuster manuell ein oder nutze den visuellen Regex-Builder
|
||||
- Wähle eine bestimmte Spalte oder durchsuche alle Spalten
|
||||
- Mustersuche ist standardmäßig Groß-/Kleinschreibung unabhängig
|
||||
|
||||
*Numerischer Filter:*
|
||||
- Filtere Zeilen basierend auf numerischen Vergleichen (>, <, >=, <=, =)
|
||||
- Wende auf eine bestimmte Spalte oder alle (numerischen) Spalten an
|
||||
|
||||
*Spaltenauswahl:*
|
||||
- Wähle, welche Spalten in der Ausgabe enthalten sein sollen
|
||||
- Nutze "Alle auswählen" / "Alle abwählen" für schnelle Auswahl
|
||||
|
||||
**Schritt 3: Ergebnisse & Export**
|
||||
- Überprüfe die Filterstatistiken (beibehaltene Zeilen, entfernte Zeilen, Trefferquote)
|
||||
- Zeige die gefilterten Daten an (erste 100 Zeilen)
|
||||
- Lade den gefilterten Datensatz als neue Excel-Datei herunter
|
||||
|
||||
---
|
||||
|
||||
### Tab: Visueller Regex-Builder
|
||||
|
||||
Erstelle reguläre Ausdrücke visuell durch Kombinieren modularer Bausteine:
|
||||
|
||||
**Verfügbare Bausteine:**
|
||||
|
||||
| Baustein | Beschreibung | Regex-Äquivalent |
|
||||
|----------|--------------|------------------|
|
||||
| Exakter Text | Findet wörtlichen Text | `text` |
|
||||
| Ziffer (0-9) | Findet eine einzelne Ziffer | `\d` |
|
||||
| Buchstabe (A-Z, a-z) | Findet einen Buchstaben | `[a-zA-Z]` |
|
||||
| Leerzeichen | Findet Leerzeichen, Tabs, Zeilenumbrüche | `\s` |
|
||||
| Beliebiges einzelnes Zeichen | Platzhalter für ein Zeichen | `.` |
|
||||
| Beliebige Zeichenfolge | Platzhalter für beliebigen Inhalt | `.*` |
|
||||
| ODER (Alternative) | Findet entweder Ausdruck davor oder danach | `\|` |
|
||||
| Zeilenanfang | Verankert am Anfang | `^` |
|
||||
| Zeilenende | Verankert am Ende | `$` |
|
||||
|
||||
**Quantifizierer:**
|
||||
- **Genau 1:** Findet das Element einmal (Standard)
|
||||
- **1 oder mehr (+):** Findet ein oder mehr Vorkommen
|
||||
- **0 oder mehr (*):** Findet null oder mehr Vorkommen
|
||||
- **Optional (?):** Findet null oder ein Vorkommen
|
||||
|
||||
**Beispiel: Fehlercodes finden**
|
||||
|
||||
Um Zeilen zu finden, die mit "Fehler" beginnen, gefolgt von einem Leerzeichen und Ziffern (z.B. "Fehler 404"):
|
||||
|
||||
1. Füge "Zeilenanfang" hinzu - verankert am Zeilenanfang
|
||||
2. Füge "Exakter Text" mit Wert "Fehler" hinzu
|
||||
3. Füge "Leerzeichen" mit Quantifizierer "1 oder mehr (+)" hinzu
|
||||
4. Füge "Ziffer (0-9)" mit Quantifizierer "1 oder mehr (+)" hinzu
|
||||
|
||||
Generiertes Muster: `^Fehler\s+\d+`
|
||||
|
||||
---
|
||||
|
||||
### Tipps für effektives Filtern
|
||||
|
||||
1. **Teste Muster zuerst:** Nutze die Testfunktion des Regex-Builders, bevor du ihn auf den kompletten Datensatz anwendest
|
||||
2. **Kombiniere Filter sorgfältig:** Mehrere Filter können gleichzeitig aktiviert werden; sie arbeiten nacheinander
|
||||
3. **Spaltenauswahl reduziert Dateigröße:** Exportiere nur benötigte Spalten um Ergebnisse zu straffen
|
||||
4. **Groß-/Kleinschreibung:** Alle Regex-Muster ignorieren standardmäßig Groß-/Kleinschreibung
|
||||
5. **Sonderzeichen in exaktem Text:** Der Builder maskiert spezielle Regex-Zeichen automatisch
|
||||
|
||||
---
|
||||
|
||||
|
||||
""")
|
||||
|
||||
def main():
|
||||
init_session_state()
|
||||
apply_sleek_dark_theme()
|
||||
|
||||
with st.sidebar:
|
||||
st.markdown("### Datenübersicht")
|
||||
|
||||
if st.session_state.df is not None:
|
||||
# Dateiinformationen
|
||||
st.markdown("**Infos**")
|
||||
st.caption(f"Aktives Blatt: {st.session_state.selected_sheet}")
|
||||
|
||||
st.divider()
|
||||
|
||||
# Zeilenstatistik
|
||||
st.markdown("**Statistik**")
|
||||
st.metric("Alle Zeilen", f"{len(st.session_state.df):,}")
|
||||
st.metric("Alle Spalten", f"{len(st.session_state.df.columns):,}")
|
||||
|
||||
st.divider()
|
||||
|
||||
# Datentypen-Zusammenfassung
|
||||
st.markdown("**Spaltentypen**")
|
||||
dtype_counts = st.session_state.df.dtypes.value_counts()
|
||||
for dtype, count in dtype_counts.items():
|
||||
st.caption(f"{dtype}: {count} Spalte(n)")
|
||||
|
||||
st.divider()
|
||||
|
||||
# Filterstatus
|
||||
if st.session_state.stats is not None:
|
||||
st.markdown("**Filterergebnisse**")
|
||||
st.metric("Ausgabezeilen", f"{st.session_state.stats['output_rows']:,}")
|
||||
st.metric("Trefferquote", f"{st.session_state.stats['retention_rate']:.1f}%")
|
||||
if st.session_state.stats['filters_applied']:
|
||||
st.caption(f"Filter: {', '.join(st.session_state.stats['filters_applied'])}")
|
||||
|
||||
st.divider()
|
||||
|
||||
# Speicherverbrauch
|
||||
memory_mb = st.session_state.df.memory_usage(deep=True).sum() / 1024 / 1024
|
||||
st.caption(f"Speicher: {memory_mb:.1f} MB")
|
||||
st.caption("*Version 0.1* ")
|
||||
|
||||
else:
|
||||
st.info("Lade eine Excel-Datei hoch, um Statistiken anzuzeigen.")
|
||||
|
||||
st.divider()
|
||||
st.markdown("**Anleitung**")
|
||||
st.markdown("""
|
||||
1. Excel-Datei hochladen
|
||||
2. Filter konfigurieren (optional)
|
||||
3. "Filter anwenden" klicken
|
||||
4. Ergebnisse herunterladen
|
||||
""")
|
||||
|
||||
st.title("Excel Filter Tool")
|
||||
st.markdown("*Temporäre Session: Es werden keine Daten und Einstellungen gespeichert!* ")
|
||||
st.write("")
|
||||
|
||||
main_tabs = st.tabs(["Excel-Filter", "Regex-Builder", "Hilfe"])
|
||||
with main_tabs[0]: render_pipeline_tab()
|
||||
with main_tabs[1]: render_regex_builder_tab()
|
||||
with main_tabs[2]: render_help_tab()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user