← back to Commercialrealestate
CRCP crexi DB scraper: harden per Kimi review — scalar results parsed only after stripping the boxed preamble, 'Listed by' keeps $ in names, strips a same-line price, word-bounded, never locks the agent name in as the firm (TK-12033)
cd843138ffd0bf620eb1e9a9a0259ff3b5e0fcd8 · 2026-09-23 12:27:39 -0700 · Steve Abrams
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7HcZ26MePoJ5QpcvDhKmH
Files touched
M scripts/scrape-crexi-loopnet.js
Diff
commit cd843138ffd0bf620eb1e9a9a0259ff3b5e0fcd8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 23 12:27:39 2026 -0700
CRCP crexi DB scraper: harden per Kimi review — scalar results parsed only after stripping the boxed preamble, 'Listed by' keeps $ in names, strips a same-line price, word-bounded, never locks the agent name in as the firm (TK-12033)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7HcZ26MePoJ5QpcvDhKmH
---
scripts/scrape-crexi-loopnet.js | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/scripts/scrape-crexi-loopnet.js b/scripts/scrape-crexi-loopnet.js
index a5e7703..0180aea 100644
--- a/scripts/scrape-crexi-loopnet.js
+++ b/scripts/scrape-crexi-loopnet.js
@@ -62,10 +62,15 @@ function oc(args, timeout = 45000) { try { return execFileSync(OC, args, { encod
function parseOcJson(out) {
if (!out) return null;
try { return JSON.parse(out); } catch {}
+ // 1) drop the boxed preamble (box-drawing lines) and parse what is left — this is the only path that may yield a
+ // bare scalar (Kimi: trusting any trailing digit line would read a raw string "error:\n123" as the number 123).
+ const cleaned = out.split("\n").filter(l => !/^\s*[\u2500-\u257F\u25C7]/.test(l)).join("\n").trim();
+ if (cleaned && cleaned !== out.trim()) { try { return JSON.parse(cleaned); } catch {} }
+ // 2) otherwise find the first line that starts an object/array/quoted string and parse from there
const lines = out.split("\n");
for (let i = 0; i < lines.length; i++) {
const s = lines.slice(i).join("\n").trim();
- if (/^[\[{"]/.test(s) || /^(true|false|null|-?\d)/.test(s)) { try { return JSON.parse(s); } catch {} }
+ if (/^[\[{"]/.test(s)) { try { return JSON.parse(s); } catch {} }
}
return out;
}
@@ -187,8 +192,13 @@ function detailAgent(url) {
// PRIMARY firm source (2026-09 page): the structured "Listed by <firm>[, <firm dba>]" line — it is present on the
// detail page and in the Listing Contacts block; the reverse line-scan below misread section headers
// ("Property Tax") and masked lines as the firm (TK-12033). Fall back to the scan only when it is absent.
- const lbLine = ((d.lc || '') + '\n' + head).match(/Listed by\s+([^\n$]{2,120}?)(?=\s*(?:\n|\$|$))/i);
- if (lbLine) firm = lbLine[1].split(',')[0].trim() || null;
+ const lbLine = ((d.lc || '') + '\n' + head).match(/\bListed by\s+([^\n]{2,120}?)(?=\s*(?:\n|$))/i);
+ if (lbLine) {
+ // drop a trailing same-line price ("Equity Union $6,500,000"), take the first comma segment, and refuse a
+ // PERSON name (if Crexi ever emits "Listed by <agent>, <firm>" we must not lock the agent in as the firm).
+ const seg = lbLine[1].replace(/\s*\$[\d,.\sKkMm]+.*$/, '').split(',')[0].trim();
+ if (seg && !(agent && seg === agent)) firm = seg;
+ }
// fallback: the last content line of the block that isn't the name / phone / email / license marker
for (let i = lines.length - 1; i >= 0 && !firm; i--) {
const l = lines[i];
← 46f7721 CRCP crexi DB scraper: count brokers actually INSERTED on --
·
back to Commercialrealestate
·
auto-data-snapshot: 2026-09-23T12:42:16 (8 data files) — dat 0aa4c7e →