Size
10.7 KB
Version
1.0.1
Created
Nov 10, 2025
Updated
about 1 month ago
1// ==UserScript==
2// @name LoopNet Listings Scraper to Excel
3// @description A new extension
4// @version 1.0.1
5// @match https://*.loopnet.com/*
6// @icon https://www.loopnet.com/favicon.ico?v=a9e78ae7bf796dfbce4aef53e0a498ce
7// @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js
8// ==/UserScript==
9(function() {
10 'use strict';
11
12 console.log('LoopNet Scraper initialized');
13
14 // State management
15 let isScrapingInProgress = false;
16 let allListingsData = [];
17 let currentPage = 1;
18 let totalPages = 0;
19
20 // Function to extract data from a single listing
21 function extractListingData(listing) {
22 try {
23 // Extract address/property name
24 const address = listing.querySelector('.left-h6, h6 a')?.textContent?.trim() || 'N/A';
25
26 // Extract price
27 const priceElement = listing.querySelector('li[name="Price"]');
28 const price = priceElement?.textContent?.trim().replace(/\s+/g, '') || 'N/A';
29
30 // Extract units
31 const units = listing.querySelector('.right-h4, h4 a')?.textContent?.trim() || 'N/A';
32
33 // Extract location
34 const location = listing.querySelector('.right-h6')?.textContent?.trim() || 'N/A';
35
36 // Extract all data points
37 const dataPoints = Array.from(listing.querySelectorAll('.data-points-a li, .data-points-2c li'));
38
39 // Extract Cap Rate
40 const capRateElement = dataPoints.find(li => li.textContent.includes('Cap Rate'));
41 const capRate = capRateElement?.textContent?.trim() || 'N/A';
42
43 // Extract Square Footage
44 const sfElement = dataPoints.find(li => li.textContent.includes('SF'));
45 const sf = sfElement?.textContent?.trim() || 'N/A';
46
47 // Extract Build Date
48 const buildDateElement = dataPoints.find(li => li.textContent.includes('Built'));
49 const buildDate = buildDateElement?.textContent?.trim().replace('Built in ', '') || 'N/A';
50
51 // Extract direct link
52 const linkElement = listing.querySelector('a[href*="/Listing/"]');
53 const directLink = linkElement?.href || 'N/A';
54
55 return {
56 Address: address,
57 Price: price,
58 Units: units,
59 Location: location,
60 SF: sf,
61 BuildDate: buildDate,
62 CapRate: capRate,
63 DirectLink: directLink
64 };
65 } catch (error) {
66 console.error('Error extracting listing data:', error);
67 return null;
68 }
69 }
70
71 // Function to scrape current page
72 function scrapeCurrentPage() {
73 console.log(`Scraping page ${currentPage}...`);
74
75 const listings = document.querySelectorAll('article.placard');
76 console.log(`Found ${listings.length} listings on current page`);
77
78 listings.forEach((listing, index) => {
79 const data = extractListingData(listing);
80 if (data) {
81 allListingsData.push(data);
82 console.log(`Scraped listing ${index + 1}:`, data.Address);
83 }
84 });
85
86 updateScraperButton(`Scraped ${allListingsData.length} listings...`);
87 }
88
89 // Function to get total pages
90 function getTotalPages() {
91 const totalResultsText = document.querySelector('.total-results-paging-digits')?.textContent?.trim();
92 if (totalResultsText) {
93 // Extract total from "1-25 of 433" format
94 const match = totalResultsText.match(/of\s+(\d+)/);
95 if (match) {
96 const totalListings = parseInt(match[1]);
97 const listingsPerPage = 25;
98 return Math.ceil(totalListings / listingsPerPage);
99 }
100 }
101 return 1;
102 }
103
104 // Function to navigate to next page
105 function goToNextPage() {
106 const nextPageLink = document.querySelector('a.caret-right-large[data-automation-id="NextPage"]');
107 if (nextPageLink) {
108 console.log(`Navigating to page ${currentPage + 1}...`);
109 nextPageLink.click();
110 return true;
111 }
112 return false;
113 }
114
115 // Function to wait for page load
116 function waitForPageLoad() {
117 return new Promise((resolve) => {
118 // Wait for listings to appear
119 const checkListings = setInterval(() => {
120 const listings = document.querySelectorAll('article.placard');
121 if (listings.length > 0) {
122 clearInterval(checkListings);
123 // Additional delay to ensure all data is loaded
124 setTimeout(resolve, 2000);
125 }
126 }, 500);
127 });
128 }
129
130 // Function to export to Excel
131 function exportToExcel() {
132 console.log(`Exporting ${allListingsData.length} listings to Excel...`);
133
134 try {
135 // Create worksheet from data
136 const ws = XLSX.utils.json_to_sheet(allListingsData);
137
138 // Create workbook
139 const wb = XLSX.utils.book_new();
140 XLSX.utils.book_append_sheet(wb, ws, 'LoopNet Listings');
141
142 // Generate filename with timestamp
143 const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
144 const filename = `LoopNet_Listings_${timestamp}.xlsx`;
145
146 // Save file
147 XLSX.writeFile(wb, filename);
148
149 console.log(`Excel file saved: ${filename}`);
150 alert(`Successfully exported ${allListingsData.length} listings to ${filename}`);
151 } catch (error) {
152 console.error('Error exporting to Excel:', error);
153 alert('Error exporting to Excel. Check console for details.');
154 }
155 }
156
157 // Main scraping function
158 async function startScraping() {
159 if (isScrapingInProgress) {
160 alert('Scraping is already in progress!');
161 return;
162 }
163
164 isScrapingInProgress = true;
165 allListingsData = [];
166 currentPage = 1;
167
168 // Get total pages
169 totalPages = getTotalPages();
170 console.log(`Total pages to scrape: ${totalPages}`);
171
172 updateScraperButton(`Scraping page 1 of ${totalPages}...`);
173
174 try {
175 // Scrape first page
176 scrapeCurrentPage();
177
178 // Scrape remaining pages
179 for (let i = 2; i <= totalPages; i++) {
180 currentPage = i;
181 updateScraperButton(`Scraping page ${currentPage} of ${totalPages}...`);
182
183 const hasNextPage = goToNextPage();
184 if (!hasNextPage) {
185 console.log('No more pages to scrape');
186 break;
187 }
188
189 // Wait for next page to load
190 await waitForPageLoad();
191
192 // Scrape current page
193 scrapeCurrentPage();
194 }
195
196 console.log(`Scraping complete! Total listings: ${allListingsData.length}`);
197
198 // Export to Excel
199 exportToExcel();
200
201 // Reset button
202 updateScraperButton('Scrape All Pages to Excel');
203
204 } catch (error) {
205 console.error('Error during scraping:', error);
206 alert('Error during scraping. Check console for details.');
207 updateScraperButton('Scrape All Pages to Excel');
208 } finally {
209 isScrapingInProgress = false;
210 }
211 }
212
213 // Function to create and update scraper button
214 function updateScraperButton(text) {
215 const button = document.getElementById('loopnet-scraper-btn');
216 if (button) {
217 button.textContent = text;
218 button.disabled = isScrapingInProgress;
219 }
220 }
221
222 // Function to create scraper button
223 function createScraperButton() {
224 // Check if we're on a search results page
225 const placards = document.querySelector('.placards');
226 if (!placards) {
227 console.log('Not on a search results page');
228 return;
229 }
230
231 // Check if button already exists
232 if (document.getElementById('loopnet-scraper-btn')) {
233 return;
234 }
235
236 // Create button container
237 const buttonContainer = document.createElement('div');
238 buttonContainer.id = 'loopnet-scraper-container';
239 buttonContainer.style.cssText = `
240 position: fixed;
241 top: 20px;
242 right: 20px;
243 z-index: 10000;
244 background: white;
245 padding: 15px;
246 border-radius: 8px;
247 box-shadow: 0 4px 12px rgba(0,0,0,0.15);
248 `;
249
250 // Create button
251 const button = document.createElement('button');
252 button.id = 'loopnet-scraper-btn';
253 button.textContent = 'Scrape All Pages to Excel';
254 button.style.cssText = `
255 background: #0066cc;
256 color: white;
257 border: none;
258 padding: 12px 24px;
259 font-size: 14px;
260 font-weight: bold;
261 border-radius: 6px;
262 cursor: pointer;
263 transition: background 0.3s;
264 `;
265
266 button.addEventListener('mouseenter', () => {
267 if (!isScrapingInProgress) {
268 button.style.background = '#0052a3';
269 }
270 });
271
272 button.addEventListener('mouseleave', () => {
273 if (!isScrapingInProgress) {
274 button.style.background = '#0066cc';
275 }
276 });
277
278 button.addEventListener('click', startScraping);
279
280 buttonContainer.appendChild(button);
281 document.body.appendChild(buttonContainer);
282
283 console.log('Scraper button created');
284 }
285
286 // Initialize when page loads
287 function init() {
288 console.log('Initializing LoopNet scraper...');
289
290 // Wait for page to be ready
291 if (document.readyState === 'loading') {
292 document.addEventListener('DOMContentLoaded', createScraperButton);
293 } else {
294 createScraperButton();
295 }
296
297 // Also observe for dynamic content changes
298 const observer = new MutationObserver(() => {
299 if (!document.getElementById('loopnet-scraper-btn')) {
300 createScraperButton();
301 }
302 });
303
304 observer.observe(document.body, {
305 childList: true,
306 subtree: true
307 });
308 }
309
310 // Start the extension
311 init();
312
313})();