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:
co-authored by
Claude Opus 5.5
parent
1b82f580db
commit
a85daf01da
@@ -63,7 +63,7 @@ export function scoreJob(agent, job) {
|
||||
* "Müller Holding GmbH & Co. KG" -> "müller holding".
|
||||
*/
|
||||
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) {
|
||||
return normalize(name)
|
||||
|
||||
@@ -136,21 +136,37 @@ async function fetchBa(keyword, { maxAge }) {
|
||||
return list
|
||||
.filter((job) => !/AUSBILDUNG|PRAKTIKUM/i.test(job.stellenangebotsart ?? job.angebotsart ?? ''))
|
||||
.map((job) => {
|
||||
const refnr = job.refnr ?? job.referenznummer ?? job.hashId;
|
||||
const place = job.arbeitsort ?? job.arbeitsorte?.[0] ?? {};
|
||||
// v6 (Stand 29.09.2026): firma, stellenlokationen[].adresse,
|
||||
// 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);
|
||||
return {
|
||||
source: 'ba',
|
||||
externalId: String(refnr ?? job.stellenangebotsTitel ?? job.titel),
|
||||
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,
|
||||
// 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,
|
||||
employmentType: /SELBST/i.test(job.stellenangebotsart ?? '') ? 'Freiberuflich' : 'Anstellung',
|
||||
url: job.externeUrl || `https://www.arbeitsagentur.de/jobsuche/jobdetail/${encodeURIComponent(refnr)}`,
|
||||
description: clean(job.beruf) || null,
|
||||
publishedAt: toDate(job.aktuelleVeroeffentlichungsdatum ?? job.veroeffentlichungsdatum ?? job.modifikationsTimestamp),
|
||||
// Die API schreibt ohne Umlaut: DEUTSCHLAND, OESTERREICH, SCHWEIZ.
|
||||
country: !land || /deutschland/i.test(land) ? 'DE' : /(ö|oe)sterreich/i.test(land) ? 'AT' : /schweiz/i.test(land) ? 'CH' : null,
|
||||
employmentType: /SELBST/i.test(job.stellenangebotsart ?? '')
|
||||
? 'Freiberuflich'
|
||||
: 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);
|
||||
@@ -163,6 +179,9 @@ async function fetchStepstone(keyword) {
|
||||
const base = 'https://www.stepstone.de';
|
||||
const html = await request(`${base}/jobs/${encodeURIComponent(slug(keyword))}?action=sort_publish`);
|
||||
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 = [];
|
||||
|
||||
$('[data-at="job-item-title"]').each((_, element) => {
|
||||
@@ -212,9 +231,15 @@ async function fetchKarriere(keyword) {
|
||||
externalId: url.match(/\/jobs\/(\d+)/)?.[1] ?? url.split('?')[0],
|
||||
title: clean(link.text()),
|
||||
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',
|
||||
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],
|
||||
description: clean(card.find('.m-jobsListItem__snippet').first().text()) || null,
|
||||
publishedAt: parseRelativeDate(card.find('.m-jobsListItem__date').first().text()),
|
||||
|
||||
Reference in New Issue
Block a user