LoopNet Listings Scraper to Excel

A new extension

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})();
LoopNet Listings Scraper to Excel | Robomonkey