Findet Betriebe, bei denen sich dein Angebot lohnt — und sortiert die aus, die nur so aussehen.
Der Radar holt eine Firmenliste aus OpenStreetMap, liest von jeder Firma die Startseite plus Karriere-, Technik- und Über-uns-Seite und bewertet, ob dort gerade Bedarf für dein Thema entsteht. Ergebnis ist eine durchsuchbare HTML-Auswertung mit Direktlinks auf Website und Karriereseite.
config.toml ──▶ seed_from_osm.py ──▶ companies.csv ──▶ marktradar.py ──▶ results.csv ──▶ report.html
Die Auswertung — eine eigenständige HTML-Datei, durchsuchbar und verschickbar. Sämtliche Daten hier sind simuliert.
├── run.sh alle Befehle
├── config.toml Beispiel-Konfiguration (Region + Suchbegriffe)
├── src/ der Code
├── privat/ deine Daten – wird nie versioniert
│ ├── config.toml deine eigene Konfiguration
│ ├── companies.csv gefundene Firmen
│ ├── results.csv Bewertungen
│ ├── report.html Auswertung
│ ├── exclude.txt Ausschlussliste
│ └── .env Zugangsdaten
└── Dockerfile, docker-compose.yml
Alles Persönliche liegt in einem Ordner. privat/ steht in .gitignore,
du kannst das Repo also ohne weitere Prüfung veröffentlichen. Existiert dort
keine eigene config.toml, greift die Beispiel-Konfiguration aus dem
Projektstamm — ein frischer Klon läuft damit sofort.
Das mitgelieferte Beispiel sucht Fertigungsbetriebe mit Bedarf an MES/MDE/BDE-Systemen. Für ein anderes Thema änderst du eine Datei, siehe Auf dein Thema anpassen.
./run.sh setup # Einrichtung im Browser -> config.toml
./run.sh seed # Firmen aus OpenStreetMap -> companies.csv
./run.sh radar # crawlen und bewerten -> results.csv + report.html
./run.sh top 10 # Kurzliste im Terminalrun.sh legt die virtuelle Umgebung beim ersten Start selbst an und lädt
.env. Kein source .venv/bin/activate nötig, kein API-Key.
| Befehl | Was passiert |
|---|---|
./run.sh setup |
Einrichtung im Browser öffnen |
./run.sh seed |
Firmen aus OpenStreetMap holen, companies.csv ergänzen |
./run.sh seed --dry-run |
nur zeigen, was dazukäme |
./run.sh seed --replace |
Liste neu bauen statt ergänzen |
./run.sh seed --region <name> |
nur eine Region aus config.toml |
./run.sh seed-search |
zusätzlich über eine SearXNG-Instanz suchen |
./run.sh seed-places |
zusätzlich über Google Places (kostet, Key nötig) |
./run.sh radar |
alle Firmen crawlen → results.csv + report.html |
./run.sh resume |
abgebrochenen Lauf fortsetzen, nur Neuzugänge crawlen |
./run.sh report |
Auswertung neu bauen und öffnen |
./run.sh top [N] |
Kurzliste im Terminal (Default 10) |
./run.sh all |
seed (falls nötig) → radar → top |
Nach jedem seed willst du resume, nicht radar — radar fängt von vorn
an, resume crawlt nur die neuen Firmen.
Die Oberfläche unter http://127.0.0.1:8765: Region festlegen, Firmenliste
pflegen, Läufe starten und live mitlesen. Auch hier simulierte Daten.
Läuft komplett lokal, öffnet keinen Port.
docker compose up -d --build # startet die Oberfläche
open http://127.0.0.1:8765 # konfigurieren, Läufe starten, mitlesen
docker compose down # beendenDer Container arbeitet in ./privat — demselben Ordner wie run.sh. Lokal und
im Container siehst du also immer denselben Stand.
Das ist der bequemste Weg: der Container startet die Weboberfläche, und von
dort erledigst du alles. Der Port ist in docker-compose.yml an 127.0.0.1
gebunden — auch aus dem Container heraus ist nichts aus dem Netzwerk
erreichbar.
Einzelne Läufe gehen weiterhin über die Kommandozeile:
docker compose run --rm marktradar seed
docker compose run --rm marktradar radarEinzelne Läufe gehen auch ohne Oberfläche:
docker compose run --rm marktradar seed
docker compose run --rm marktradar radarAlle Dateien liegen in ./privat auf deinem Rechner und überleben jeden
Container-Neustart. Fehlt dort eine config.toml, wird die Beispiel-Datei
hineinkopiert — ändern kannst du sie im Editor, ohne das Image neu zu bauen.
Zusätzlich gibt es shell für eine Shell im Container.
Eine Datei: privat/config.toml. Kein Python, kein Code. Sie beantwortet
zwei Fragen — wo suchst du, und wonach. Als Vorlage dient
config.toml im Projektstamm; die bleibt unangetastet.
./run.sh setupÖffnet http://127.0.0.1:8765. Ein winziger Server aus der Standardbibliothek,
der nur auf 127.0.0.1 lauscht — aus dem Netzwerk ist nichts erreichbar.
Er lädt deine bestehende config.toml, und Speichern schreibt sie direkt
zurück (vorher als .bak gesichert). Kein Herunterladen und Verschieben.
Zugangsdaten trägst du dort ebenfalls ein — Kontakt für den Crawler,
Google-Key, SearXNG-Adresse. Sie landen in privat/.env mit Dateirechten 600,
nicht in der config.toml: die ist zum Teilen und Versionieren gedacht, ein
Schlüssel darin landet früher oder später im Repo. Der Key verlässt den Server
nie wieder, angezeigt wird nur AIza…7890.
Die Firmenliste bearbeitest du dort ebenfalls. Haken weg heißt: raus aus
companies.csv. Zusätzlich ausschließen heißt: kommt auch beim nächsten
Suchlauf nicht zurück — die Domain landet in exclude.txt. Ohne das würde der
nächste seed die Firma wieder einsammeln. Sammelaktionen: nur Leads behalten,
Aussortierte wegwerfen, alles/nichts — jeweils bezogen auf den aktiven Filter.
Vor jedem Speichern wird companies.csv.bak angelegt, und eine komplett leere
Liste lehnt der Server ab.
Änderungen an der Konfiguration speichern sich selbst, gut eine Sekunde
nachdem du aufhörst zu tippen — die Statuszeile nennt die Uhrzeit. Eine
Sicherung entsteht einmal je Serverstart als config.toml.bak; bei jedem
Tastendruck eine neue anzulegen wäre wertlos.
Die Läufe startest du aus derselben Seite: Firmen suchen, Crawlen, Fortsetzen, Auswertung. Fortschrittsbalken, Firmenname und Live-Log laufen mit, Abbrechen stoppt sauber — der Zwischenstand bleibt erhalten.
Nach einem erfolgreichen Crawl baut sich die Auswertung von selbst; ein
Knopf öffnet sie danach. Sonst zeigte report.html den Stand von vorher, und
man müsste daran denken.
Dahinter stecken Server-Sent Events, kein WebSocket: Fortschritt fließt nur in eine Richtung (Server → Browser), das ist gewöhnliches HTTP ohne Handshake, ohne Framing, ohne Bibliothek — und der Browser verbindet sich nach einem Abbruch von selbst neu. Schließt du den Tab mitten im Lauf, läuft er weiter; beim nächsten Öffnen ist der Stand wieder da.
Gestartet werden können nur die vier fest hinterlegten Läufe — aus der Oberfläche lässt sich nichts anderes ausführen.
Die Region gibst du als Ortsname plus Umkreis an, die Koordinaten rechnet die Seite aus — dafür fragt sie OpenStreetMap, sonst geht nichts nach außen. Ungültiges TOML wird abgewiesen, bevor etwas überschrieben wird.
Ohne Server (setup.html direkt im Browser geöffnet) funktioniert alles außer
dem Speichern; dort gibt es dann Herunterladen und Bestehende laden.
Wer lieber im Editor arbeitet, ändert config.toml direkt — die Datei ist
kommentiert und bleibt die einzige Quelle.
[[regions]] # beliebig viele Blöcke
name = "Franken/Oberpfalz"
bbox = [49.2, 10.4, 50.4, 12.2] # [süd, west, nord, ost]
[[regions]]
name = "Schwaben"
bbox = [48.2, 9.8, 48.8, 11.0]Die Ecken liest du auf openstreetmap.org ab. Mehrere Regionen dürfen weit
auseinanderliegen; jede wird einzeln gecacht, du kannst also eine ändern, ohne
die anderen neu abzufragen. ./run.sh seed --region schwaben nimmt nur eine.
Für die Suchmaschinen-Seeder zusätzlich:
[search]
branchen = ["Kunststoffverarbeitung Spritzguss", "Werkzeugbau Formenbau", ...]
orte = ["Forchheim", "Erlangen", ...]Abgefragt wird jede Branche in jedem Ort — 10 × 10 ergibt 100 Anfragen.
Fünf Begriffslisten, jede mit einem Gewicht:
| Liste | Frage |
|---|---|
[signals.fit] |
Ist das überhaupt die Art Betrieb, die du bedienst? |
[signals.invest] |
Reden sie öffentlich über dein Thema? |
[signals.job] |
Suchen sie gerade Leute dafür? |
[signals.tech] |
Womit arbeiten sie? (schwacher Kontext) |
[signals.off_target] |
Verkaufen sie das selbst? (Handel, Wettbewerb) |
Dazu [thresholds] für die Einstufung, [crawl] für Tempo und Seitenzahl und
[page_hints] für die Unterseiten, die gelesen werden.
Alles klein schreiben. Wortgrenzen werden beachtet, mes trifft also nicht in
„Messtechnik". Fehler in der Datei melden sich beim Start mit Klartext und
Zeilennummer statt mit einem Stacktrace.
Im Ordner vorlagen/ liegen komplette Konfigurationen:
Fertigung, Lebensmittelherstellung, Elektro/SHK-Handwerk, Logistik,
Kfz-Werkstätten. Kopieren, Region setzen, loslegen:
cp vorlagen/lebensmittel.toml privat/config.tomlJede Vorlage beschreibt zwei Achsen, die man getrennt tauschen kann: die
Branche (wen du ansprichst — [osm] selectors und [signals.fit]) und
dein Angebot (was du verkaufst — [signals.invest] und [signals.job]).
Alle mitgelieferten Vorlagen haben dasselbe Angebot: Produktions- und
Prozess-IT. Wer derselben Branche etwas anderes verkauft, tauscht nur die
beiden Angebots-Listen. Details in vorlagen/README.md.
Der übrige Code weiß nichts über Fertigung, MES oder Franken. Drei Dinge
machen dein Thema aus, alle drei stehen in der config.toml:
[osm]
selectors = [ # was überhaupt als Betrieb gilt
'["tourism"="hotel"]',
'["amenity"="restaurant"]',
]Die Vorgabe sucht Fabriken, Handwerks- und Industriebetriebe. Für andere Branchen tauschst du die Filter — Beispiele stehen als Kommentar in der Datei:
| Zielgruppe | Filter |
|---|---|
| Hotels, Gastronomie | ["tourism"="hotel"], ["amenity"="restaurant"] |
| Arzt- und Zahnarztpraxen | ["amenity"="doctors"], ["amenity"="dentist"] |
| Pflegeheime | ["amenity"="nursing_home"], ["social_facility"] |
| Autohäuser, Werkstätten | ["shop"="car"], ["shop"="car_repair"] |
| Logistik, Lager | ["landuse"="industrial"]["name"] |
| Landwirtschaft | ["landuse"="farmyard"]["name"] |
Dazu die Begriffslisten und die Region — fertig. Alles auch in der Oberfläche unter Feineinstellung pflegbar.
Wo es an Grenzen stößt: OpenStreetMap kennt Betriebe mit Adresse. Reine
Online-Geschäfte, Agenturen oder Beratungen ohne Standort stehen dort selten
mit Website. Für solche Zielgruppen ist der SearXNG-Seeder der bessere Weg —
oder du pflegst companies.csv von Hand, zwei Spalten name,domain.
Die naheliegende Variante — „zähl, wie oft die über mein Thema reden" — funktioniert nicht. Wer am lautesten über ein Thema redet, ist meist der Anbieter, nicht der Kunde. In der ersten Version dieses Projekts stand ein Elektrogroßhandel ganz oben, weil er Digitalisierung verkauft.
Deshalb ist fit die Voraussetzung: ohne passendes Betriebsprofil zählt kein
noch so hoher Bedarfs-Score. Und off_target kennzeichnet Handel, Systemhäuser
und Wettbewerb.
Umgekehrt gilt: Ein passender Betrieb ohne jedes Signal ist kein schlechter Lead, sondern Greenfield — da hat noch niemand angefangen. Solche Firmen haben oft nur eine dünne Website. Deshalb zählt auch der Firmenname beim Profil mit: „Musterfirma Kunststofftechnik" sagt schon alles, selbst wenn die Seite aus drei Sätzen besteht.
| Einstufung | Bedeutung | Was du damit machst |
|---|---|---|
| Aktiver Bedarf | investiert gerade oder sucht Leute dafür | direkt ansprechen, Karriereseite ist der Aufhänger |
| Thema angefasst | erste Signale | beobachten, in ein paar Monaten nachfassen |
| Unbeackert | passender Betrieb, kein Signal | Greenfield, nach Größe priorisieren |
| Vermutlich kein Kunde | Handel, Wettbewerb, Profil passt nicht | aussortiert |
Gezählt werden verschiedene Begriffe, nicht Treffer pro Seite — sonst bekäme eine Firma mit sechs lesbaren Unterseiten automatisch das Sechsfache einer mit nur einer, und der Score misst die Website-Größe statt des Signals. Zusätzlich wird die Mitarbeiterzahl aus dem Text gezogen, wo sie dort steht.
./run.sh seed fragt die Overpass-API ab: kostenlos, kein Key, ausdrücklich für
Massenabfragen gedacht. Eine Abfrage liefert alle Betriebe der Region, die
in OSM eine Website hinterlegt haben — Fabriken (man_made=works), Handwerks-
und Industriebetriebe, Firmensitze mit Branchenangabe.
Vorgefiltert wird über dieselben fit-Begriffe wie beim Scoring, damit keine
Bäckereien in der Crawl-Liste landen; --all nimmt alles mit. Regionen über
[[regions]] in config.toml.
Die Haupt-Instanz overpass-api.de ist oft überlastet — der Seeder probiert
vier Server durch. Die Antwort landet in .osm_cache.json, --refresh fragt
neu ab.
docker compose up startet eine eigene SearXNG-Instanz mit. Sie ist nur im
internen Docker-Netz erreichbar — kein Port nach außen, du belastest keine
fremde Instanz und keine fremde dich. ./run.sh seed-search nutzt sie
automatisch.
Ohne Docker setzt du SEARXNG_URL auf deine eigene Instanz. Öffentliche
Instanzen sperren die JSON-API meist ohnehin ab.
Erwarte davon nicht zu viel. SearXNG fragt echte Suchmaschinen ab, und die drosseln hart: nach rund 15 Anfragen antwortet DuckDuckGo mit CAPTCHA, danach kommen null Treffer. Der Seeder erkennt das und bricht mit Klartext ab, statt stumm leere Ergebnisse zu liefern. Verlässlich ist OpenStreetMap.
Nur geprüfte Engines sind aktiv (SEARXNG_ENGINES, Standard duckduckgo).
Bing lieferte auf „Werkzeugbau Formenbau Erlangen" Uhrzeiten und Kochrezepte —
wer weitere Engines ergänzt, sollte sie stichprobenartig prüfen.
Portale und Jobbörsen (wlw.de, gelbeseiten, jooble, StepStone, LinkedIn …) fliegen automatisch raus, sonst bestünde die halbe Liste daraus. Der Firmenname wird aus dem Seitentitel geschält, ein Teil mit Rechtsform bevorzugt.
Das Delay steht auf 6 Sekunden. Das ist kein Übervorsicht: Bei 1 Sekunde sperren DuckDuckGo, Brave und Startpage die Instanz binnen Minuten mit CAPTCHA — und zwar für alle ihre Nutzer. Der Seeder erkennt das und bricht mit Klartext ab, statt stumm leere Ergebnisse zu liefern.
./run.sh seed-places braucht einen Key in .env und Places API (New) im
Cloud-Projekt (die alte „Places API" reicht nicht). Kostet rund 32 USD je 1000
Anfragen; der Seeder zeigt vorher, wie viele Anfragen neu sind und was sie
kosten. Antworten landen in .places_cache.json.
Bei allen Seedern gilt: companies.csv wird ergänzt, nicht ersetzt, bei
null Treffern nie überschrieben, und vorher als .bak gesichert.
Eine Domain pro Zeile in exclude.txt (Vorlage: exclude.txt.example):
ehemaliger-arbeitgeber.de
bestandskunde.de
Alle Seeder und der Radar lesen die Datei. Ausgeschlossene Firmen landen
weder in companies.csv noch in results.csv — auch nicht nach einem neuen
Seed-Lauf. Sie nur aus der CSV zu löschen reicht also nicht. www. ist egal,
Subdomains sind mit abgedeckt (firma.de schließt auch shop.firma.de aus),
# leitet einen Kommentar ein.
Beide Seiten tragen dieselbe Optik: dunkler Grund mit feinem Raster, Haarlinien statt Flächen, Eckwinkel an den Rahmen, durchgehend Monospace, Zahlen groß und ruhig. Farbe trägt Bedeutung — Blau für Rubriken und Werte, Grün für Zustände, Rot für Fehler.
Die Gestaltung steht komplett in src/theme.css und wird beim
Ausliefern in die Seiten geschrieben. Farben änderst du dort, an einer Stelle,
für beide Seiten.
./run.sh report baut aus results.csv eine einzelne HTML-Datei und öffnet
sie. Kein Server, kein CDN, keine Internetverbindung nötig — du kannst sie so
wie sie ist per Mail verschicken.
Drin sind Kennzahlen, die stärksten Firmen und häufigsten Signalbegriffe als Diagramm, darunter die vollständige Tabelle: durchsuchbar nach Firma, Domain und Signalwort, filterbar nach Einstufung, sortierbar per Klick. Filter und Suche wirken auf Diagramme und Tabelle gleichzeitig. Jede Zeile verlinkt Website und Karriereseite. Hell/Dunkel folgt dem System, der Knopf oben rechts überschreibt es.
results.csv wird nach jeder Firma neu geschrieben, atomar über eine
.tmp-Datei. Ein Abbruch kostet höchstens die gerade laufende Firma;
./run.sh resume macht dort weiter, überspringt Fertiges und versucht
Fehlerfälle erneut. Eine einzelne kaputte Seite beendet den Lauf nicht — der
Fehler landet als Text in der Spalte error.
Rechne mit rund 15 Sekunden pro Firma — geteilt durch die Zahl der
gleichzeitigen Arbeiter. workers in config.toml steht auf 6, gemessen
etwa 3,5× schneller als seriell bei identischem Ergebnis.
Parallel bearbeitet werden verschiedene Firmen, nie mehrere Seiten
derselben Domain: die Pause von 1,5 s gilt weiterhin pro Domain, fremde Server
sehen also unverändert einen gemächlichen Besucher. Wer noch schneller will,
senkt max_pages_per_site — das kostet aber Signalqualität, weil die
Karriereseite dann eher wegfällt. -j 1 erzwingt strikt seriell.
Der Radar liest öffentlich zugängliche Firmenwebsites — dasselbe, was ein Mensch
beim Durchklicken sähe. Er respektiert robots.txt, macht 1,5 s Pause zwischen
Anfragen, liest maximal 6 Seiten pro Domain und nennt sich im User-Agent beim
Namen. Trage deinen Kontakt in CRAWL_CONTACT ein, damit man dich erreichen
kann.
Erfasst werden Firmendaten, keine Personendaten — Ansprechpartner werden nicht ausgelesen. Für die Ansprache gelten die üblichen Regeln (UWG §7, DSGVO): B2B-Telefonakquise braucht mutmaßliche Einwilligung, E-Mail in der Regel eine ausdrückliche. Das ist keine Rechtsberatung.
Overpass ist für Massenabfragen gedacht, SearXNG-Instanzen sind es nicht.
.gitignore hält alles Persönliche draußen. Vor dem ersten Push kontrollieren:
git status --short # nichts aus privat/ darf auftauchenEs genügt die eine Regel privat/ — dort liegt alles Persönliche: Firmenliste,
Ergebnisse, Ausschlussliste (sie verrät ehemalige Arbeitgeber und Kunden),
.env mit Keys und die Caches. Zusätzlich greifen Muster wie *.csv als
Sicherheitsnetz, falls doch mal etwas im Projektstamm landet.
Im Repo landen Code, Beispieldateien und diese Doku — 22 Dateien.
Über Issues — auch für „das verstehe ich nicht" oder „bei meiner Branche funktioniert das nicht". Gerade die Begriffslisten leben davon, dass jemand aus einem anderen Fach draufschaut.
Pull Requests sind willkommen. Wer die Signalbegriffe für eine andere Branche
zusammenstellt, hilft anderen mehr als jede Codeänderung: leg dafür einfach
eine Datei nach dem Muster der config.toml bei.
Sascha Böhm Software & App — MDE-, BDE- und ERP-Lösungen für produzierende Betriebe, entwickelt von einem Praktiker mit über 30 Jahren Produktionserfahrung.
Über 30 Jahre in der Fertigung: vom Kunststoff-Formgeber über die technische Leitung bis zur IT-Leitung einer Unternehmensgruppe. Sieben eigenentwickelte MES/MDE/BDE-Systeme laufen bei Kunden im Produktivbetrieb, das älteste seit 2009 — durchgängig open-source-basiert und on-premise.
Schwerpunkte: Fertigungssteuerung und Betriebsdatenerfassung, papierlose Fertigung, Maschinenanbindung über OPC UA und MQTT, ERP-Integration (ERPNext/Frappe, SAP), Workflow-Automatisierung und KI-gestützte Auswertungen.
website.industrie-4-0.org · service@industrie-4-0.org · +49 151 155 20 344 · Hohenschwärz 66, 91322 Gräfenberg
Zwei frei zugängliche Demosysteme zeigen die Arbeitsweise unmittelbar (alle Daten simuliert): mde.dms-iot.de für Maschinendatenerfassung mit OEE, Störungen und Schichtbilanzen, terminals.dms-iot.de für das Maschinenterminal im Schichtbetrieb.
MIT, siehe LICENSE. Die Signatur am Seitenfuß darfst du in einem Fork entfernen — über eine Erwähnung freue ich mich trotzdem.

