[object Object]

← 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

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 →