Scanner: Parser nach Live-Test korrigiert

- BA-API v6: firma, stellenlokationen, datumErsteVeroeffentlichung,
  vertragsdauer; Land kommt als OESTERREICH ohne Umlaut
- StepStone: eingebettete <style>-Blöcke vor dem Auslesen entfernen
- karriere.at: Ort ohne Komma, Art nur aus passender Pille
- Firmenabgleich: GesmbH als Rechtsform

Live auf icrm.icon-experts.de getestet: 109 Anzeigen, 71 Treffer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-09-29 10:55:27 +02:00
co-authored by Claude Opus 5.5
parent 1b82f580db
commit a85daf01da
2 changed files with 36 additions and 11 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ export function scoreJob(agent, job) {
* "Müller Holding GmbH & Co. KG" -> "müller holding". * "Müller Holding GmbH & Co. KG" -> "müller holding".
*/ */
const LEGAL_FORMS = const LEGAL_FORMS =
/\b(gmbh|mbh|ag|se|kg|kgaa|ohg|gbr|ug|e\.?\s?v|e\.?\s?k|co|inc|ltd|llc|plc|sa|sarl|bv|nv|haftungsbeschränkt|gesellschaft mit beschränkter haftung|aktiengesellschaft)\b\.?/g; /\b(gmbh|gesmbh|mbh|ag|se|kg|kgaa|ohg|gbr|ug|e\.?\s?v|e\.?\s?k|co|inc|ltd|llc|plc|sa|sarl|bv|nv|haftungsbeschränkt|gesellschaft mit beschränkter haftung|aktiengesellschaft)\b\.?/g;
export function normalizeCompany(name) { export function normalizeCompany(name) {
return normalize(name) return normalize(name)
+35 -10
View File
@@ -136,21 +136,37 @@ async function fetchBa(keyword, { maxAge }) {
return list return list
.filter((job) => !/AUSBILDUNG|PRAKTIKUM/i.test(job.stellenangebotsart ?? job.angebotsart ?? '')) .filter((job) => !/AUSBILDUNG|PRAKTIKUM/i.test(job.stellenangebotsart ?? job.angebotsart ?? ''))
.map((job) => { .map((job) => {
const refnr = job.refnr ?? job.referenznummer ?? job.hashId; // v6 (Stand 29.09.2026): firma, stellenlokationen[].adresse,
const place = job.arbeitsort ?? job.arbeitsorte?.[0] ?? {}; // datumErsteVeroeffentlichung, externeURL, vertragsdauer. Die v4-Namen
// bleiben als Rueckfall stehen.
const refnr = job.referenznummer ?? job.refnr ?? job.hashId;
const place = job.stellenlokationen?.[0]?.adresse ?? job.arbeitsort ?? job.arbeitsorte?.[0] ?? {};
const land = clean(place.land); const land = clean(place.land);
return { return {
source: 'ba', source: 'ba',
externalId: String(refnr ?? job.stellenangebotsTitel ?? job.titel), externalId: String(refnr ?? job.stellenangebotsTitel ?? job.titel),
title: clean(job.stellenangebotsTitel ?? job.titel ?? job.beruf), title: clean(job.stellenangebotsTitel ?? job.titel ?? job.beruf),
company: clean(job.arbeitgeber ?? job.arbeitgeberName) || null, company: clean(job.firma ?? job.arbeitgeber ?? job.arbeitgeberName) || null,
location: clean([place.plz, place.ort].filter(Boolean).join(' ')) || clean(place.region) || null, location: clean([place.plz, place.ort].filter(Boolean).join(' ')) || clean(place.region) || null,
// Die BA fuehrt auch Stellen ausserhalb DACH - die fallen unten raus. // Die BA fuehrt auch Stellen ausserhalb DACH - die fallen unten raus.
country: !land || /deutschland/i.test(land) ? 'DE' : /österreich/i.test(land) ? 'AT' : /schweiz/i.test(land) ? 'CH' : null, // Die API schreibt ohne Umlaut: DEUTSCHLAND, OESTERREICH, SCHWEIZ.
employmentType: /SELBST/i.test(job.stellenangebotsart ?? '') ? 'Freiberuflich' : 'Anstellung', country: !land || /deutschland/i.test(land) ? 'DE' : /(ö|oe)sterreich/i.test(land) ? 'AT' : /schweiz/i.test(land) ? 'CH' : null,
url: job.externeUrl || `https://www.arbeitsagentur.de/jobsuche/jobdetail/${encodeURIComponent(refnr)}`, employmentType: /SELBST/i.test(job.stellenangebotsart ?? '')
description: clean(job.beruf) || null, ? 'Freiberuflich'
publishedAt: toDate(job.aktuelleVeroeffentlichungsdatum ?? job.veroeffentlichungsdatum ?? job.modifikationsTimestamp), : job.vertragsdauer === 'UNBEFRISTET'
? 'Festanstellung'
: job.vertragsdauer === 'BEFRISTET'
? 'Befristet'
: 'Anstellung',
// Die Detailseite der BA ist stabiler als der externe Bewerbungslink.
url: `https://www.arbeitsagentur.de/jobsuche/jobdetail/${encodeURIComponent(refnr)}`,
description: clean((job.alleBerufe ?? [job.hauptberuf ?? job.beruf]).filter(Boolean).join(', ')) || null,
publishedAt: toDate(
job.datumErsteVeroeffentlichung ??
job.veroeffentlichungszeitraum?.von ??
job.aktuelleVeroeffentlichungsdatum ??
job.veroeffentlichungsdatum
),
}; };
}) })
.filter((job) => job.title && job.externalId && job.country); .filter((job) => job.title && job.externalId && job.country);
@@ -163,6 +179,9 @@ async function fetchStepstone(keyword) {
const base = 'https://www.stepstone.de'; const base = 'https://www.stepstone.de';
const html = await request(`${base}/jobs/${encodeURIComponent(slug(keyword))}?action=sort_publish`); const html = await request(`${base}/jobs/${encodeURIComponent(slug(keyword))}?action=sort_publish`);
const $ = cheerio.load(html); const $ = cheerio.load(html);
// StepStone rendert CSS-in-JS: <style> steckt mitten in den Karten und
// landete sonst in Titel und Firmenname.
$('style, script').remove();
const jobs = []; const jobs = [];
$('[data-at="job-item-title"]').each((_, element) => { $('[data-at="job-item-title"]').each((_, element) => {
@@ -212,9 +231,15 @@ async function fetchKarriere(keyword) {
externalId: url.match(/\/jobs\/(\d+)/)?.[1] ?? url.split('?')[0], externalId: url.match(/\/jobs\/(\d+)/)?.[1] ?? url.split('?')[0],
title: clean(link.text()), title: clean(link.text()),
company: clean(card.find('.m-jobsListItem__company').first().text()) || null, company: clean(card.find('.m-jobsListItem__company').first().text()) || null,
location: clean(card.find('.m-jobsListItem__locations').first().text()) || null, location: clean(card.find('.m-jobsListItem__locations').first().text()).replace(/[\s,]+$/, '') || null,
country: 'AT', country: 'AT',
employmentType: clean(card.find('.m-jobsListItem__pill').first().text()) || null, // Die Pillen tragen Ort, Arbeitszeit und Art gemischt - nur die Art nehmen.
employmentType:
card
.find('.m-jobsListItem__pill')
.toArray()
.map((pill) => clean($(pill).text()))
.find((text) => /anstellung|vollzeit|teilzeit|befrist|freie|selbst|projekt/i.test(text)) ?? null,
url: url.split('?')[0], url: url.split('?')[0],
description: clean(card.find('.m-jobsListItem__snippet').first().text()) || null, description: clean(card.find('.m-jobsListItem__snippet').first().text()) || null,
publishedAt: parseRelativeDate(card.find('.m-jobsListItem__date').first().text()), publishedAt: parseRelativeDate(card.find('.m-jobsListItem__date').first().text()),