网络用nodejs刮擦家得宝搜索
#node #webscraping #serpapi

将被刮擦

what

完整代码

如果您不需要解释,请看一下full code example in the online IDE

import dotenv from "dotenv";
import { config, getJson } from "serpapi";
import readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

dotenv.config();
const rl = readline.createInterface({ input, output });
config.api_key = process.env.API_KEY; //your API key from serpapi.com

const engine = "home_depot"; // search engine
const resultsLimit = 40; // hardcoded limit for demonstration purpose
const params = {
  q: "inverter generatot", // Parameter defines the search query
  page: 1, // Value is used to get the items on a specific page
};

const getSearchRefinement = async () => {
  const preResults = await getJson(engine, params);
  let fixedQuery;
  if (preResults?.search_information?.spelling_fix) {
    fixedQuery = preResults.search_information.spelling_fix;
  }
  return { query: fixedQuery, filters: preResults.filters };
};

const applyNewSearchParams = async ({ query, filters }) => {
  if (query) {
    const answer = await rl.question(`Do you want to change the search query from "${params.q}" to "${query}"? y/n: `);
    if (answer.toLowerCase() === "y") {
      params.q = query;
      console.log(`Now the search query is: "${query}"`);
    } else {
      console.log(`The search query didn't change`);
    }
  }
  if (filters) {
    const appliedFilters = [];
    let tokens = "";
    for (const filter of filters) {
      const answer = await rl.question(`Do you want to apply some filter from "${filter.key}" category? y/n: `);
      if (answer.toLowerCase() === "y") {
        for (const filterValue of filter.value) {
          const answer = await rl.question(`Do you want to apply "${filterValue.name}" filter from "${filter.key}" category? y/n: `);
          if (answer.toLowerCase() === "y") {
            tokens += `${filterValue.value},`;
            appliedFilters.push(`${filter.key}: ${filterValue.name}`);
          }
        }
      }
    }
    rl.close();
    if (tokens) {
      params.hd_filter_tokens = tokens.slice(0, -1);
    }
  }
};

const getResults = async () => {
  const results = [];
  while (true) {
    const json = await getJson(engine, params);
    if (json.products) {
      results.push(...json.products);
      params.page += 1;
    } else break;
    if (results.length >= resultsLimit) break;
  }
  return results;
};

getSearchRefinement()
  .then(applyNewSearchParams)
  .then(getResults)
  .then((result) => console.dir(result, { depth: null }));

为什么要从serpapi?

使用The Home Depot API

使用API​​通常可以解决在创建自己的解析器或爬网时可能会遇到的所有或大多数问题。从网络剪接的角度来看,我们的API可以帮助解决最痛苦的问题:

  • 通过求解验证码或IP块,来自受支持的搜索引擎的旁路块。
  • 无需从头开始创建解析器并维护它。
  • 支付代理和验证码求解器。
  • 如果需要更快地提取数据,则不需要使用浏览器自动化。

前往Playground进行现场互动演示。

准备

首先,我们需要创建一个node.js* project并添加koude0 packages koude1koude2

为此,在我们项目的目录中,打开命令行并输入:

$ npm init -y

,然后:

$ npm i serpapi dotenv

*如果您没有安装node.js,则可以download it from nodejs.org并遵循安装documentation

  • SERPAPI软件包用于使用SERPAPI刮擦和解析搜索引擎结果。从Google,Bing,Bing,Yandex,Yahoo,Home Depot,eBay等获取搜索结果。

  • dotenv软件包是一个零依赖性模块,将环境变量从.env文件加载到process.env

接下来,我们需要在我们的package.json文件中添加一个带有“模块”值的顶级“类型”字段,以允许using ES6 modules in Node.JS

ES6Module

目前,我们完成了项目的设置node.js环境,然后转到分步代码说明。

代码说明

First, we need to import dotenv from koude2 library, config and getJson from koude1 library, readline from koude11 Node.js built-in library, stdin and stdout (declare them as input and output) from koude17 Node.js built-在图书馆:

import dotenv from "dotenv";
import { config, getJson } from "serpapi";
import readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

然后,我们应用一些配置。调用dotenv koude19方法,调用带有参数inputoutputreadline koude21方法,然后将其设置为rl常数,将您的serpapi私有API设置为global koude8对象。


dotenv.config();
const rl = readline.createInterface({ input, output });
config.api_key = process.env.API_KEY; //your API key from serpapi.com
  • dotenv.config()将读取您的.env文件,解析内容,分配给process.env,并用parsed键返回包含已加载内容或error键的对象。
  • readline.createInterface()创建了一个新的readlinePromises.Interface实例。一旦创建了readlinePromises.Interface实例,最常见的情况就是聆听'line'事件。
  • config.api_key您通过修改配置对象来声明全局api_key值。

接下来,我们编写必要的搜索参数。我们定义搜索engine,我们想要接收多少结果(resultsLimit常数)和带有qpage参数的params对象用于提出请求:

ð注意:我在搜索查询中特别犯了一个错误,以演示The Home Depot Spell Check API的工作方式。

const engine = "home_depot"; // search engine
const resultsLimit = 40; // hardcoded limit for demonstration purpose
const params = {
  q: "inverter generatot", // Parameter defines the search query
  page: 1, // Value is used to get the items on a specific page
};

您可以使用下一个搜索参数:

  • q参数定义搜索查询。您可以使用常规的Home Depot搜索中使用的任何东西。
  • hd_sort参数定义了以不同选项排序的结果。它可以设置为:top_sellers(畅销书),price_low_to_high(价格低到高),price_high_to_low(价格高至低),top_rated(最高评分),best_match(最佳匹配)。
  • hd_filter_tokens过去可以通过逗号划分的过滤器令牌。 Filter tokens可以从API响应中获得。
  • delivery_zip邮政编码。通过选定的区域过滤运输产品。
  • store_id商店ID仅通过特定商店过滤产品。
  • lowerbound定义了美元的下限。
  • upperbound定义了上价的上限。
  • nao定义了产品结果的偏移。单页包含24产品。第一页偏移量为0,第二-> 24,第三-> 48等。
  • page值用于将项目在特定页面上获取。 (例如,1(默认)是结果的第一页,2是结果的第二页,3是结果的第三页,等等)。
  • ps确定每个页面的项目数。在某些情况下,Home Depot覆盖了PS值。默认情况下,Home Depot返回24结果。
  • no_cache参数将迫使Serpapi获取App Store搜索结果,即使已经存在缓存版本。仅当查询和所有参数完全相同时,才能提供缓存。 1小时后缓存到期。缓存的搜索是免费的,并且不计入您每月的搜索。它可以设置为false(默认值)以允许缓存的结果,也可以将true的结果设置为禁止缓存结果。 no_cacheasync参数不应一起使用。
  • async参数定义了要将搜索提交给SERPAPI的方式。可以将其设置为false(默认值)以打开HTTP连接并保持打开状态,直到获得搜索结果,或者true仅将搜索提交给SERPAPI并以后将其检索。在这种情况下,您需要使用我们的Searches Archive API来检索结果。 asyncno_cache参数不应一起使用。 async不应在启用Ludicrous Speed的帐户上使用。

接下来,我们声明获得初步结果的功能getSearchRefinement。在此功能中,我们返回此搜索的可用过滤器,并检查preResults是否具有spelling_fix,将其设置为fixedQuery,然后返回:

const getSearchRefinement = async () => {
  const preResults = await getJson(engine, params);
  let fixedQuery;
  if (preResults?.search_information?.spelling_fix) {
    fixedQuery = preResults.search_information.spelling_fix;
  }
  return { query: fixedQuery, filters: preResults.filters };
};

接下来,我们声明允许设置固定查询和过滤器的函数applyNewSearchParams。我们需要destructure函数参数反对queryfilters

const applyNewSearchParams = async ({ query, filters }) => {
  ...
};

在此功能中,我们需要检查是否存在query,然后在此之后打印一个有关控制台中更改搜索查询的问题(koude84方法),并等待用户的答案。如果用户的答案为“ y”,我们将新的搜索查询设置为params对象:

if (query) {
  const answer = await rl.question(`Do you want to change the search query from "${params.q}" to "${query}"? y/n: `);
  if (answer.toLowerCase() === "y") {
    params.q = query;
    console.log(`Now the search query is: "${query}"`);
  } else {
    console.log(`The search query didn't change`);
  }
}

接下来,我们需要通过使用koude86循环运行每个过滤器结果来询问有关应用接收过滤器的相同问题:

if (filters) {
  const appliedFilters = [];
  let tokens = "";
  for (const filter of filters) {
    const answer = await rl.question(`Do you want to apply some filter from "${filter.key}" category? y/n: `);
    if (answer.toLowerCase() === "y") {
      for (const filterValue of filter.value) {
        const answer = await rl.question(`Do you want to apply "${filterValue.name}" filter from "${filter.key}" category? y/n: `);
        if (answer.toLowerCase() === "y") {
          tokens += `${filterValue.value},`;
          appliedFilters.push(`${filter.key}: ${filterValue.name}`);
        }
      }
    }
  }
  ...
}

接下来,我们关闭readline流(rl.close()方法),如果用户选择了一些过滤器,请在tokens字符串末端删除逗号,然后将其设置为params对象中的hd_filter_tokens值:

rl.close();
if (tokens) {
  params.hd_filter_tokens = tokens.slice(0, -1);
}

接下来,我们声明从所有页面获得结果(使用分页)并返回的函数getResults

const getResults = async () => {
  ...
};

在此功能中,我们需要声明一个空的results数组,然后使用koude94循环获得带有结果的json,从每个页面中添加products并设置下一页index(to params.page value)。

>

如果页面上没有更多结果,或者接收结果的数量超过resultsLimit,我们会停止循环(使用koude99)并返回带有结果的数组:

const results = [];
while (true) {
  const json = await getJson(engine, params);
  if (json.products) {
    results.push(...json.products);
    params.page += 1;
  } else break;
  if (results.length >= resultsLimit) break;
}
return results;

最后,运行getSearchRefinement函数并使用Promise chaining运行applyNewSearchParamsgetResults函数。然后,我们使用koude103方法在控制台中打印所有接收的信息,该方法允许您使用带有必要参数的对象来更改默认输出选项:

getSearchRefinement()
  .then(applyNewSearchParams)
  .then(getResults)
  .then((result) => console.dir(result, { depth: null }));

输出

[
  {
    "position": 1,
    "product_id": "318783386",
    "title": "2500-Watt Recoil Start Ultra-Light Portable Gas and Propane Powered Dual Fuel Inverter Generator with CO Shield",
    "thumbnails": [
      [
        "https://images.thdstatic.com/productImages/95951f39-703a-4efc-b12b-ad3a3276e73c/svn/champion-power-equipment-inverter-generators-201122-64_65.jpg",
        "https://images.thdstatic.com/productImages/95951f39-703a-4efc-b12b-ad3a3276e73c/svn/champion-power-equipment-inverter-generators-201122-64_100.jpg",
        "https://images.thdstatic.com/productImages/95951f39-703a-4efc-b12b-ad3a3276e73c/svn/champion-power-equipment-inverter-generators-201122-64_145.jpg",
        "https://images.thdstatic.com/productImages/95951f39-703a-4efc-b12b-ad3a3276e73c/svn/champion-power-equipment-inverter-generators-201122-64_300.jpg",
        "https://images.thdstatic.com/productImages/95951f39-703a-4efc-b12b-ad3a3276e73c/svn/champion-power-equipment-inverter-generators-201122-64_400.jpg",
        "https://images.thdstatic.com/productImages/95951f39-703a-4efc-b12b-ad3a3276e73c/svn/champion-power-equipment-inverter-generators-201122-64_600.jpg",
        "https://images.thdstatic.com/productImages/95951f39-703a-4efc-b12b-ad3a3276e73c/svn/champion-power-equipment-inverter-generators-201122-64_1000.jpg"
      ],
      [
        "https://images.thdstatic.com/productImages/27667698-e4fc-488f-b713-7f51e83d5b5b/svn/champion-power-equipment-inverter-generators-201122-e4_65.jpg",
        "https://images.thdstatic.com/productImages/27667698-e4fc-488f-b713-7f51e83d5b5b/svn/champion-power-equipment-inverter-generators-201122-e4_100.jpg",
        "https://images.thdstatic.com/productImages/27667698-e4fc-488f-b713-7f51e83d5b5b/svn/champion-power-equipment-inverter-generators-201122-e4_145.jpg",
        "https://images.thdstatic.com/productImages/27667698-e4fc-488f-b713-7f51e83d5b5b/svn/champion-power-equipment-inverter-generators-201122-e4_300.jpg",
        "https://images.thdstatic.com/productImages/27667698-e4fc-488f-b713-7f51e83d5b5b/svn/champion-power-equipment-inverter-generators-201122-e4_400.jpg",
        "https://images.thdstatic.com/productImages/27667698-e4fc-488f-b713-7f51e83d5b5b/svn/champion-power-equipment-inverter-generators-201122-e4_600.jpg",
        "https://images.thdstatic.com/productImages/27667698-e4fc-488f-b713-7f51e83d5b5b/svn/champion-power-equipment-inverter-generators-201122-e4_1000.jpg"
      ]
    ],
    "link": "https://www.homedepot.com/p/Champion-Power-Equipment-2500-Watt-Recoil-Start-Ultra-Light-Portable-Gas-and-Propane-Powered-Dual-Fuel-Inverter-Generator-with-CO-Shield-201122/318783386",
    "serpapi_link": "https://serpapi.com/search.json?delivery_zip=04401&engine=home_depot_product&product_id=318783386&store_id=2414",
    "model_number": "201122",
    "brand": "Champion Power Equipment",
    "collection": "https://www.homedepot.com/collection/Outdoors/Champion-Inverter-Generators-Accessories-Collection/Family-311405669?omsid=318783386",
    "rating": 4.7821,
    "reviews": 1606,
    "price": 899,
    "delivery": {
      "free": true,
      "free_delivery_threshold": false
    },
    "pickup": {
      "free_ship_to_store": true
    }
  }
]

链接

如果您希望将其他功能添加到此博客文章中,或者您想查看Serpapi,write me a message的某些项目。


加入我们的Twitter | YouTube

添加一个Feature Requestð«或Bugð