深圳幻海软件技术有限公司 欢迎您!

使用NodeJS请求抓取带有进程Cookie认证的站点

2023-02-28

作者|LokeshJoshi译者|张哲刚审校丨Noe简介当前,NodeJS拥有大量的库,基本上可以解决所有的常规需求。网络抓取是一项门槛较低的技术,衍生了大量自由职业者以及开发团队。自然而然,NodeJS的库生态系统几乎包含了网络解析所需的一切。本文论述中,首先假定已经运行在应用程序皆为NodeJS

  作者 | Lokesh Joshi

  译者 | 张哲刚

  审校丨Noe

简介

  当前,NodeJS拥有大量的库,基本上可以解决所有的常规需求。网络抓取是一项门槛较低的技术,衍生了大量自由职业者以及开发团队。自然而然,NodeJS的库生态系统几乎包含了网络解析所需的一切。

  本文论述中,首先假定已经运行在应用程序皆为NodeJS解析而工作的核心设备之上。此外,我们将研究一个示例,从销售品牌服装和饰品的网站https://outlet.scotch-soda.com 站点中的几百个页面里收集数据。这些代码示例类似于一些真实的抓取应用程序,其中一个就是在Yelp抓取中使用。

  当然,由于本文研究所限,示例中删除了一些生产组件,例如数据库、容器化、代理连接以及进程管理工具(例如pm2)。另外,在诸如linting这类显而易见的事务上也不会停止。

  但是,我们会保证项目的基本结构完善,将使用最流行的库(Axios,Cheerio,Lodash),使用Puppeter提取授权密钥,使用NodeJS流抓取数据并将其写入文件。

术语规定

  本文将使用以下术语:NodeJS应用程序——服务器应用程序;网站 outlet.scotch-soda.com ——Web资源网站服务器为Web服务器。大体来说,首先是在Chrome或Firefox中探究网站网络资源及其页面,然后运行一个服务器应用程序,向Web服务器发送HTTP请求,最后收到带有相应数据的响应。

获取授权Cookie

  outlet.scotch-soda.com的内容仅对授权用户开放。本示例中,授权将通过由服务器应用程序控制的Chromium浏览器实施,cookie也是从其中接收。这些Cookie将包含在每个向web服务器发出的HTTP请求上的HTTP标头中,从而允许应用程序访问这些授权内容。当抓取具有数万乃至数十万页面的大量资源时,接收到的Cookie需要更新一些次数。

  该应用程序将具有以下结构:

  cookieManager.js:带有Cookie管理器类的文件,用以负责获取cookie;

  cookie-storage.js: cookie 变量文件;

  index.js:安排Cookie管理器调用点;

  .env:环境变量文件。

  /project_root

  |__ /src

  |   |__ /helpers

  |      |__ **cookie-manager.js**

  |      |__ **cookie-storage.js**

  |**__ .env**

  |__ **index.js**

  主目录和文件结构

  将以下代码添加到应用程序中:

// index.js

// including environment variables in .env
require('dotenv').config();

const cookieManager = require('./src/helpers/cookie-manager');
const { setLocalCookie } = require('./src/helpers/cookie-storage');

// IIFE - application entry point
(async () => {
  // CookieManager call point
  // login/password values are stored in the .env file
  const cookie = await cookieManager.fetchCookie(
    process.env.LOGIN,
    process.env.PASSWORD,
  );

  if (cookie) {
    // if the cookie was received, assign it as the value of a storage variable
    setLocalCookie(cookie);
  } else {
    console.log('Warning! Could not fetch the Cookie after 3 attempts. Aborting the process...');
    // close the application with an error if it is impossible to receive the cookie
    process.exit(1);
  }
})();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

  在cookie-manager.js中:

// cookie-manager.js

// 'user-agents' generates 'User-Agent' values for HTTP headers
// 'puppeteer-extra' - wrapper for 'puppeteer' library
const _ = require('lodash');
const UserAgent = require('user-agents');
const puppeteerXtra = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');

// hide from webserver that it is bot
puppeteerXtra.use(StealthPlugin());

class CookieManager {
  // this.browser & this.page - Chromium window and page instances 
  constructor() {
    this.browser = null;
    this.page = null;
    this.cookie = null;
  }

  // getter
  getCookie() {
    return this.cookie;
  }

  // setter
  setCookie(cookie) {
    this.cookie = cookie;
  }

  async fetchCookie(username, password) {
    // give 3 attempts to authorize and receive cookies
    const attemptCount = 3;

    try {
      // instantiate Chromium window and blank page 
      this.browser = await puppeteerXtra.launch({
        args: ['--window-size=1920,1080'],
        headless: process.env.NODE_ENV === 'PROD',
      });

      // Chromium instantiates blank page and sets 'User-Agent' header
      this.page = await this.browser.newPage();
      await this.page.setUserAgent((new UserAgent()).toString());

      for (let i = 0; i < attemptCount; i += 1) {
        // Chromium asks the web server for an authorization page
        //and waiting for DOM
        await this.page.goto(process.env.LOGIN_PAGE, { waitUntil: ['domcontentloaded'] });

        // Chromium waits and presses the country selection confirmation button
        // and falling asleep for 1 second: page.waitForTimeout(1000)
        await this.page.waitForSelector('#changeRegionAndLanguageBtn', { timeout: 5000 });
        await this.page.click('#changeRegionAndLanguageBtn');
        await this.page.waitForTimeout(1000);

        // Chromium waits for a block to enter a username and password
        await this.page.waitForSelector('div.login-box-content', { timeout: 5000 });
        await this.page.waitForTimeout(1000);
        
        // Chromium enters username/password and clicks on the 'Log in' button      
        await this.page.type('input.email-input', username);
        await this.page.waitForTimeout(1000);
        await this.page.type('input.password-input', password);
        await this.page.waitForTimeout(1000);
        await this.page.click('button[value="Log in"]');
        await this.page.waitForTimeout(3000);

        // Chromium waits for target content to load on 'div.main' selector
        await this.page.waitForSelector('div.main', { timeout: 5000 });

        // get the cookies and glue them into a string of the form <key>=<value> [; <key>=<value>]
        this.setCookie(
          _.join(
            _.map(
              await this.page.cookies(),
              ({ name, value }) => _.join([name, value], '='),
            ),
            '; ',
          ),
        );

        // when the cookie has been received, break the loop
        if (this.cookie) break;
      }

      // return cookie to call point (in index.js)
      return this.getCookie();
    } catch (err) {
      throw new Error(err);
    } finally {
      // close page and browser instances
      this.page && await this.page.close();
      this.browser && await this.browser.close();
    }
  }
}

// export singleton
module.exports = new CookieManager();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.

  某些变量的值是链接到.env文件的。

  // .env

  NODE_ENV=DEV

  LOGIN_PAGE=https://outlet.scotch-soda.com/de/en/login

  LOGIN=tyrell.wellick@ecorp.com

  PASSWORD=i*m_on@kde

  例如,配置无头消息属性,发送到方法 puppeteerXtra.launch解析为布尔值,它取决于状态可变的process.env.node_env 。在开发过程中,变量被设置为DEV,无头变量被设置为false,因此Puppeteer能够明白它此刻应该在监视器上呈现执行Chromium 。

  方法page.cookies返回一个对象数组,每个对象定义一个 cookie 并包含两个属性:名称和值 。使用一系列 Lodash 函数,循环提取每个 cookie 的键值对,并生成类似于下面的字符串:

文件 cookie-storage.js:

// cookie-storage.js

//  cookie storage variable
let localProcessedCookie = null;

// getter
const getLocalCookie = () => localProcessedCookie;

// setter
const setLocalCookie = (cookie) => {
  localProcessedCookie = cookie;

  // lock the getLocalCookie function; 
  // its scope with the localProcessedCookie value will be saved
  // after the setLocalCookie function completes
  return getLocalCookie;
};

module.exports = {
  setLocalCookie,
  getLocalCookie,
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

  对于闭包的定义,明确的思路是:在该变量作用域内的函数结束后,保持对某个变量值的访问。通常,当函数完成执行返回操作时,它会离开调用堆栈,垃圾回收机制会从作用域内删除内存中的所有变量。

  上面的示例中,本地cookie设置器完成设置后,应该回收的本地已处理cookie变量的值将保留在计算机的内存中。这就意味着只要应用程序在运行,它就可以在代码中的任何地方获取这个值。

  这样,当调用setLocalCookie时,将从中返回getLocalCookie函数。一旦这个LocalCookie函数作用域面临回收时,NodeJS能够看到它具有getLocalCookie闭包函数。此时,垃圾回收机制将返回的获取器作用域内的所有变量都保留在内存中。由于可变的本地处理Cookie在getLocalCookie的作用域内,因此它将继续存在,保持与Cookie的绑定。

URL生成器

  应用程序需要一个url的主列表才能开始爬取。在生产过程中,爬取通常从Web资源的主页开始,经过一定数量次数的迭代,最终建立一个指向登录页面的链接集合。通常,一个Web资源有成千上万个这样的链接。

  在此示例中,爬取程序只会传输8个爬取链接作为输入,链接指向包含着主要产品分类目录的页面,它们分别是:

​​  https://outlet.scotch-soda.com/women/clothing​​

​​  https://outlet.scotch-soda.com/women/footwear​​

​​  https://outlet.scotch-soda.com/women/accessories/all-womens-accessories​​

​​  https://outlet.scotch-soda.com/men/clothing​​

​​  https://outlet.scotch-soda.com/men/footwear​​

​​  https://outlet.scotch-soda.com/men/accessories/all-mens-accessories​​

​​  https://outlet.scotch-soda.com/kids/girls/clothing/all-girls-clothing​​

​​  https://outlet.scotch-soda.com/kids/boys/clothing/all-boys-clothing​​

  使用这么长的链接字符,会影响代码美观性,为了避免这种情形,让我们用下列文件创建一个短小精悍的URL构建器:

  categories.js: 包含路由参数的文件;

  target-builder.js: 构建url集合的文件.

  /project_root

  |__ /src

  |   |__ /constants

  | |  |__ **categories.js**

  |   |__ /helpers

  |      |__ cookie-manager.js

  |      |__ cookie-storage.js

  |      |__ **target-builder.js**

  |**__ .env**

  |__ index.js

  添加以下代码:

  // .env

  MAIN_PAGE=https://outlet.scotch-soda.com

// index.js

// import builder function
const getTargetUrls = require('./src/helpers/target-builder');

(async () => {
  // here the proccess of getting cookie

  // gets an array of url links and determines it's length L
  const targetUrls = getTargetUrls();
  const { length: L } = targetUrls;

})();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
// categories.js

module.exports = [
  'women/clothing',
  'women/footwear',
  'women/accessories/all-womens-accessories',
  'men/clothing',
  'men/footwear',
  'men/accessories/all-mens-accessories',
  'kids/girls/clothing/all-girls-clothing',
  'kids/boys/clothing/all-boys-clothing',
];
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
// target-builder.js

const path = require('path');
const categories = require('../constants/categories');

// static fragment of route parameters
const staticPath = 'global/en';

// create URL object from main page address
const url = new URL(process.env.MAIN_PAGE);

// add the full string of route parameters to the URL object
// and return full url string
const addPath = (dynamicPath) => {
  url.pathname = path.join(staticPath, dynamicPath);

  return url.href;
};

// collect URL link from each element of the array with categories
module.exports = () => categories.map((category) => addPath(category));
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

  这三个代码片段构建了本段开头给出的8个链接,演示了内置的URL以及路径库的使用。可能有人会觉得,这不是大炮打蚊子嘛!使用插值明明更简单啊!

  有明确规范的NodeJS方法用于处理路由以及URL请求参数,主要是基于以下两个原因:

  1、插值在轻量级应用下还好;

  2、为了养成良好的习惯,应当每天使用。

爬网和抓取

  向服务器应用程序的逻辑中心添加两个文件:

  ·crawler.js:包含用于向 Web 服务器发送请求和接收网页标记的爬网程序类;

  ·parser.js:包含解析器类,其中包含用于抓取标记和获取目标数据的方法。

  /project_root

  |__ /src

  |   |__ /constants

  | |  |__ categories.js

  |   |__ /helpers

  |   |  |__ cookie-manager.js

  |   |  |__ cookie-storage.js

  |   |  |__ target-builder.js

  ****|   |__ **crawler.js**

  |   |__ **parser.js**

  |**__** .env

  |__ **index.js**

  首先,添加一个循环index.js,它将依次传递URL链接到爬取程序并接收解析后的数据:

// index.js

const crawler = new Crawler();

(async () => {
  // getting Cookie proccess
  // and url-links array...
  const { length: L } = targetUrls;

  // run a loop through the length of the array of url links
  for (let i = 0; i < L; i += 1) {
    // call the run method of the crawler for each link
    // and return parsed data
    const result = await crawler.run(targetUrls[i]);

    // do smth with parsed data...
  }
})();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

  爬取代码:

// crawler.js

require('dotenv').config();
const cheerio = require('cheerio');
const axios = require('axios').default;
const UserAgent = require('user-agents');

const Parser = require('./parser');
// getLocalCookie - closure function, returns localProcessedCookie
const { getLocalCookie } = require('./helpers/cookie-storage');

module.exports = class Crawler {
  constructor() {
    // create a class variable and bind it to the newly created Axios object
    // with the necessary headers
    this.axios = axios.create({
      headers: {
        cookie: getLocalCookie(),
        'user-agent': (new UserAgent()).toString(),
      },
    });
  }

  async run(url) {
    console.log('IScraper: working on %s', url);

    try {
      // do HTTP request to the web server
      const { data } = await this.axios.get(url);
      // create a cheerio object with nodes from html markup
      const $ = cheerio.load(data);

      // if the cheerio object contains nodes, run Parser
      // and return to index.js the result of parsing
      if ($.length) {
        const p = new Parser($);

        return p.parse();
      }
      console.log('IScraper: could not fetch or handle the page content from %s', url);
      return null;
    } catch (e) {
      console.log('IScraper: could not fetch the page content from %s', url);

      return null;
    }
  }
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.

  解析器的任务是在接收到 cheerio 对象时选择数据,然后为每个 URL 链接构建以下结构:

[
  {
    "Title":"Graphic relaxed-fit T-shirt | Women",
    "CurrentPrice":25.96,
    "Currency":"€",
    "isNew":false
  },
  {
    // at all 36 such elements for every url-link
  }
] 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

  解析代码:

// parser.js

require('dotenv').config();
const _ = require('lodash');

module.exports = class Parser {
  constructor(content) {
    // this.$ - this is a cheerio object parsed from the markup
    this.$ = content;
    this.$$ = null;
  }

  // The crawler calls the parse method
  // extracts all 'li' elements from the content block
  // and in the map loop the target data is selected
  parse() {
    return this.$('#js-search-result-items')
      .children('li')
      .map((i, el) => {
        this.$$ = this.$(el);

        const Title = this.getTitle();
        const CurrentPrice = this.getCurrentPrice();

        // if two key values are missing, such object is rejected
        if (!Title || !CurrentPrice) return {};

        return {
          Title,
          CurrentPrice,
          Currency: this.getCurrency(),
          isNew: this.isNew(),
        };
      })
      .toArray();
  }

  // next - private methods, which are used at 'parse' method
  getTitle() {
    return _.replace(this.$$.find('.product__name').text().trim(), /\s{2,}/g, ' ');
  }

  getCurrentPrice() {
    return _.toNumber(
      _.replace(
        _.last(_.split(this.$$.find('.product__price').text().trim(), ' ')),
        ',',
        '.',
      ),
    );
  }

  getCurrency() {
    return _.head(_.split(this.$$.find('.product__price').text().trim(), ' '));
  }

  isNew() {
    return /new/.test(_.toLower(this.$$.find('.content-asset p').text().trim()));
  }
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.

  爬取程序和解析器运行的结果将是8个内部包含对象的数组,并传递回index.js文件的for循环。

  流写入文件

  要写入一个文件,须使用可写流。流是一种JS对象,包含了许多用于处理按顺序出现的数据块的方法。所有的流都继承自EventEmitter类(即事件触发器),因此,它们能够对运行环境中的突发事件做出反应。或许有人遇到过下面这种情形:

myServer.on('request', (request, response) => {
  // something puts into response
});

// or

myObject.on('data', (chunk) => {
  // do something with data
});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

  这是NodeJS流的优秀范例,尽管它们的名字不那么原始:我的服务器和我的对象。在此示例中,它们侦听某些事件:HTTP请求(事件)的到达和一段数据(事件)的到达,然后安排它们各就各位去工作。“流式传输”的立足之本在于它们使用片状数据片段,并且只需要极低的运行内存。

  在此情形下,for循环按顺序接收 8 个包含数据的数组,这些数组将按顺序写入文件,而无需等待累积完整的集合,也无需使用任何累加器。执行示例代码时,由于能够得知下一部分解析数据到达for循环的确切时刻,所以无需任何侦听,就可以使用内置于流中的方法立即写入。

  写入到何处

  /project_root

  |__ /data

  |   |__ **data.json**

  ...

// index.js

const fs = require('fs');
const path = require('path');
const { createWriteStream } = require('fs');

// use constants to simplify work with addresses
const resultDirPath = path.join('./', 'data');
const resultFilePath = path.join(resultDirPath, 'data.json');

// check if the data directory exists; create if it's necessary
// if the data.json file existed - delete all data
//  ...if not existed - create empty
!fs.existsSync(resultDirPath) && fs.mkdirSync(resultDirPath);
fs.writeFileSync(resultFilePath, '');

(async () => {
  // getting Cookie proccess
  // and url-links array...

  // create a stream object for writing
  // and add square bracket to the first line with a line break
  const writer = createWriteStream(resultFilePath);
  writer.write('[\n');

  // run a loop through the length of the url-links array 
  for (let i = 0; i < L; i += 1) {
    const result = await crawler.run(targetUrls[i]);

    // if an array with parsed data is received, determine its length l
    if (!_.isEmpty(result)) {
      const { length: l } = result;

      // using the write method, add the next portion 
      //of the incoming data to data.json
      for (let j = 0; j < l; j += 1) {
        if (i + 1 === L && j + 1 === l) {
          writer.write(`  ${JSON.stringify(result[j])}\n`);
        } else {
          writer.write(`  ${JSON.stringify(result[j])},\n`);
        }
      }
    }
  }
})();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.

  嵌套的for循环解决了一个问题:为在输出中获取有效的json文件,需要注意结果数组中的最后一个对象后面不要有逗号。嵌套for循环决定哪个对象是应用程序中最后一个撤消插入逗号的。

  如果事先创建data/data.json并在代码运行时打开,就可以实时看到可写流是如何按顺序添加新数据片段的。

  结论

  输出结果是以下形式的JSON对象:

[
  {"Title":"Graphic relaxed-fit T-shirt | Women","CurrentPrice":25.96,"Currency":"€","isNew":false},
  {"Title":"Printed mercerised T-shirt | Women","CurrentPrice":29.97,"Currency":"€","isNew":true},
  {"Title":"Slim-fit camisole | Women","CurrentPrice":32.46,"Currency":"€","isNew":false},
  {"Title":"Graphic relaxed-fit T-shirt | Women","CurrentPrice":25.96,"Currency":"€","isNew":true},
  ...
  {"Title":"Piped-collar polo | Boys","CurrentPrice":23.36,"Currency":"€","isNew":false},
  {"Title":"Denim chino shorts | Boys","CurrentPrice":45.46,"Currency":"€","isNew":false}
]
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

  应用程序授权处理时间约为20秒。

  完整的开源项目代码存在GitHub上。并附有依赖关系程序包package.json。

译者介绍

  张哲刚,51CTO社区编辑,系统运维工程师,国内较早一批硬件评测及互联网从业者,曾入职阿里巴巴。十余年IT项目管理经验,具备复合知识技能,曾参与多个网站架构设计、电子政务系统开发,主导过某地市级招生考试管理平台运维工作。

  原文标题:Web Scraping Sites With Session Cookie Authentication Using NodeJS Request

  链接:

​​  https://hackernoon.com/web-scraping-sites-with-session-cookie-authentication-using-nodejs-request​​