UNPKG

opensea-scraper

Version:

Scraping floor prices from opensea.

171 lines (150 loc) 6.81 kB
// puppeteer-extra is a drop-in replacement for puppeteer, // it augments the installed puppeteer with plugin functionality const puppeteer = require('puppeteer-extra'); // add stealth plugin and use defaults (all evasion techniques) const StealthPlugin = require('puppeteer-extra-plugin-stealth'); puppeteer.use(StealthPlugin()); // load helper function to detect stealth plugin const { warnIfNotUsingStealth, sleep } = require("../helpers/helperFunctions.js"); /** * scrapes opensea offers for a given collection by scrolling * through the page and fetching all offers "manually". * if you need less than 20 offers, please use the regular `offers()` * method, since it is significantly more efficient. * * INPUTS: * slug [String]: collection identifier by opensea) * options [Object]: { * debug [Boolean]: launches chromium locally, omits headless mode (default: `false`) * logs [Boolean]: display logs in the console (default: `false`) * sort [Boolean]: sorts the offers by lowest to highest (default: `true`) * browserInstance [PuppeteerBrowser]: bring your own browser instance for more control * } * RETURNS: * object with keys "offers" (array of offer objects) and "stats" (object with metadata). * an offer object holds additional information, not only the floor price, example: * { * floorPrice: { * amount: 19, * currency: "ETH", * }, * name: "cool cat #231", * tokenId: 234, * offerUrl: "https://opensea.io/assets/0x1a92f7381b9f03921564a437210bb9396471050c/231", * displayImageUrl, "https://lh3.googleusercontent.com/-vBw0jsFjmRF7hsrh26ky0XY2FXhConjTchjpKHBuj6L5Os4i9iu4Fl4ZzTjQJiMkgIEZw8hZCpK0GCUxto637wmIxOd64DSm_Y34w=w600" * } */ const offersByScrolling = async (slug, resultSize, optionsGiven = {}) => { const url = `https://opensea.io/collection/${slug}?search[sortAscending]=true&search[sortBy]=PRICE&search[toggles][0]=BUY_NOW`; return await offersByScrollingByUrl(url, resultSize, optionsGiven); } /** * use custom url to scrape offers * Opensea supports encoding filtering in the URL so this method is helpful for getting * a specific asset (for example floor price for a LAND token from the sandbox collection) */ const offersByScrollingByUrl = async (url, resultSize, optionsGiven = {}) => { if (!resultSize) { throw new Error(`Invalid 'resultSize', please provide a number. Got: ${resultSize}`); } const optionsDefault = { debug: false, logs: false, sort: true, // sorts the returned offers by lowest to highest price additionalWait: 0, // waittime in milliseconds, after page loaded, but before stating to scrape browserInstance: undefined, }; const options = { ...optionsDefault, ...optionsGiven }; const { debug, logs, browserInstance, sort, additionalWait } = options; const customPuppeteerProvided = Boolean(optionsGiven.browserInstance); // add mandatory query params // fixes a bug, see following link: // https://github.com/dcts/opensea-scraper/pull/26 const mandatoryQueryParam = "search[toggles][0]=BUY_NOW"; if (!url.includes(mandatoryQueryParam)) { const joinChar = url.includes("?") ? "&" : "?"; url += `${joinChar}${mandatoryQueryParam}`; } logs && console.log(`=== scraping started ===\nScraping Opensea URL: ${url}`); logs && console.log(`\n=== options ===\ndebug : ${debug}\nlogs : ${logs}\nbrowserInstance: ${browserInstance ? "provided by user" : "default"}`); // init browser let browser = browserInstance; if (!customPuppeteerProvided) { browser = await puppeteer.launch({ headless: !debug, // when debug is true => headless should be false args: ['--start-maximized'], }); } customPuppeteerProvided && warnIfNotUsingStealth(browser); logs && console.log("\n=== actions ==="); logs && console.log("new page created"); const page = await browser.newPage(); await page.goto(url); // ...🚧 waiting for cloudflare to resolve logs && console.log("🚧 waiting for cloudflare to resolve"); await page.waitForSelector('.cf-browser-verification', {hidden: true}); // additional wait? if (additionalWait > 0) { logs && console.log(`additional wait active, waiting ${additionalWait / 1000} seconds...`); await sleep(additionalWait); } // expose all helper functions logs && console.log("expose all helper functions"); await page.addScriptTag({path: require.resolve("../helpers/offersByScrollingHelperFunctions.js")}); // scrape offers until target resultsize reached or bottom of page reached logs && console.log("scrape offers until target resultsize reached or bottom of page reached"); let [offers, totalOffers] = await Promise.all([ _scrollAndFetchOffers(page, resultSize), _extractTotalOffers(page), ]); if (!customPuppeteerProvided && !debug) { logs && console.log("closing browser..."); await browser.close(); } if (sort) { offers = offers.sort((a,b) => a.floorPrice.amount - b.floorPrice.amount); } return { offers: offers.slice(0, resultSize), stats: { totalOffers: totalOffers, } }; } async function _scrollAndFetchOffers(page, resultSize) { return await page.evaluate((resultSize) => new Promise((resolve) => { // keep in mind inside the browser context we have the global variable "dict" initialized // defined inside src/helpers/offersByScrollingHelperFunctions.js let currentScrollTop = -1; const interval = setInterval(() => { console.log("another scrol... dict.length = " + Object.keys(dict).length); window.scrollBy(0, 50); // fetchOffers is a function that is exposed through page.addScript() and // is defined inside src/helpers/offersByScrollingHelperFunctions.js fetchOffers(dict); const endOfPageReached = document.documentElement.scrollTop === currentScrollTop; const enoughItemsFetched = Object.keys(dict).length >= resultSize; if(!endOfPageReached && !enoughItemsFetched) { currentScrollTop = document.documentElement.scrollTop; return; } clearInterval(interval); resolve(Object.values(dict)); }, 120); }), resultSize); } async function _extractTotalOffers(page) { try { // set timeout to 1 sec, no need to extensively wait since page should be loaded already const element = await page.waitForSelector('.AssetSearchView--results-count', {timeout: 1000}); const resultsText = await element.evaluate(el => el.textContent); // grab the textContent from the element, by evaluating this function in the browser context const dotsRemoved = resultsText.replace(/\./g,''); return Number(dotsRemoved.split(" ")[0]); } catch (err) { return undefined; } } module.exports = { offersByScrolling, offersByScrollingByUrl };