前端可调用的通用爬虫 FaaS

背景

之前写了一篇《不用爬虫,用 FaaS 来获取股票期权数据》,里面介绍了用服务端(FaaS)绕过跨域的限制,直接获取一些网站的接口数据。

但是,这里面的实现太具体了,这个 FaaS 只适用于「爬取新浪财经数据」这一个场景。如果还有别的场景,还需要再建一个 FaaS,再修改一下逻辑。

于是乎,为了解决这个问题,笔者专门抽象出了一个通用 FaaS,所有需要的参数从前端传入就行了,这样一个 FaaS 就能搞定所有场景了,岂不美哉。

闲话少说,上代码。

正文

服务端代码

js 复制代码
const http = require('http');
const https = require('https');
const getRawBody = require('raw-body');
const getFormBody = require('body/form');
const body = require('body');

const request = (options, type) =>
  new Promise((resolve, reject) => {
    console.log(options, type);
    const req = (type === 'https' ? https : http).request(options, (res) => {
      let data = '';
      // A chunk of data has been received.
      res.on('data', (chunk) => {
        data += chunk;
      });
      // The whole response has been received.
      res.on('end', () => {
        resolve(data);
      });
    });
    // Handle errors.
    req.on('error', (error) => {
      reject(error);
    });
    // End the request.
    req.end();
  });

exports.handler = (req, res, context) => {
  try {
    if (req.method === 'POST') {
      getRawBody(
        req,
        {
          length: req.headers['content-length'],
          encoding: 'utf8', // 指定数据编码方式,可以是 'utf8' 或 'binary'
        },
        async (err, string) => {
          if (err) {
            res.setStatusCode(400);
            res.send('Bad Request');
          } else {
            // 在这里可以对解析后的对象进行处理
            try {
              const { options, type, responseHeaders } = JSON.parse(string);
              const result = await request(options, type);
              res.setStatusCode(200);
              if (responseHeaders) {
                Object.entries(responseHeaders).forEach(([key, val]) => {
                  res.setHeader(key, val);
                });
              }
              res.send(result);
            } catch (e) {
              console.log(e);
              res.setStatusCode(500);
              res.send(e.stack);
            }
          }
        }
      );
    } else {
      res.setStatusCode(200);
      res.setHeader('Content-Type', 'text/plain');
      res.send('Hello World!');
    }
  } catch (e) {
    res.setStatusCode(500);
    res.send(e.stack);
  }
};

服务端,也就是 FaaS 的代码比较格式化。除了封装了 request 方法,和 FaaS 的模板代码外,需要特别说明的是接口参数的设计。

首先,接口的 method 是 POST,比较方便参数的传输。

然后,可以看到 body 一共有 options、type、responseHeaders 三个参数。后两个比较简单,type 的取值就是 http/httpsresponseHeaders 是个对象,也就是控制 response 返回时候的 headers。

这里解释一下,有的时候如果不给 response 声明 'content-type': 'application/json',数据会出现乱码的情况。所以特意加了 responseHeaders 这个参数。如果还有其它需求,按着这个思路加参数就行了。

我们重点说一下 options 这个参数。从代码可以看出,它是直接被 http.request 消费的,也就是大概长这样(更多见 官方文档):

json 复制代码
var options = {
  host: 'www.google.com',
  port: 80,
  path: '/upload',
  method: 'POST'
};

所以我们可以预料到,这个options 的处理逻辑,其实都放到了客户端去实现。这样实际上给了客户端更多自由的空间,同时也需要处理更多的逻辑。所以客户端也需要一定的封装,我们上代码。

客户端代码

注:因为是在做微信小程序的时候积累的素材,所以这部分的代码用的是 uni-app 的代码,不影响逻辑的理解。只需要注意 2 点:

  1. uni.request 是 uni-app 提供的内置方法,类似于 fetch、axios 这些,可以看它的 文档 了解更多;
  2. 因为小程序环境下,没有一些浏览器内置的方法和对象,比如 URLSearchParams,所以解析 URL 的逻辑,得自己实现;
js 复制代码
const parseUrl = (url) => {
  const match = url.match(
    /^(https?:)?\/\/([^/?#]+)?([^?#]+)?(\?[^#]*)?(#.*)?$/
  );
  const [, protocol, hostname, pathname, search] = match || [];
  return {
    protocol: protocol || '',
    hostname: hostname || '',
    pathname: pathname || '',
    search: search || '',
  };
};

export const serverRequest = (url, options = {}, responseHeaders) =>
  new Promise((rev, rej) => {
    // 解析 URL,提高易用性
    const { protocol, hostname, pathname, search } = parseUrl(url);
    const { method = 'GET', ...rest } = options;
    uni.request({
      url: 'Your FaaS API URL',
      method: 'POST',
      header: {
        'Content-Type': 'application/json',
      },
      data: {
        options: {
          hostname,
          path: pathname + search,
          method,
          ...rest,
        },
        type: protocol.slice(0, -1),
        responseHeaders,
      },
      success(res) {
        rev(res);
      },
      fail(err) {
        rej(err);
      },
    });
  });
  
// Usage Eg:
const fetchDemo1 = () =>
  serverRequest('https://ft.iqdii.com/views/eipo/eipo_pc?style=w&Lan=CN');

const fetchDemo2 = (Cookie) =>
  serverRequest(
    'https://www.jisilu.cn/webapi/cb/list/',
    {
      headers: {
        Host: 'www.jisilu.cn',
        Referer: 'https://www.jisilu.cn/web/data/cb/list',
        Cookie,
        Init: '1',
      },
    },
    {
      'content-type': 'application/json',
    }
  );

这里为了调用起来更方便,封装了自动解析 URL 的逻辑,然后回填到服务端需要的 options 参数里,其它的比如 headers 的这种额外属性,统一用 ...rest 处理。

从最下面的 2 个 Demo 可以看出,serverRequest 的使用方法跟主流的网络请求库差不多。到此,我们基本就实现了「前端跨域爬数据自由」。如果还需要更多的自由度,在此基础上稍作调整即可。

结语

最近的这几篇文章其实有一定的门槛,因为好多同学对云开发还不是很熟悉。云开发其实挺考验使用者的工程系统能力的。

比如这几篇文章里,我都没有说如何让 FaaS 外网能够访问,这还需要申请域名。如果要接入小程序,还需要申请 HTTPS 的证书。还有各种云产品一定要在同一个区域内(如华北2),否则在绑定的时候,根本就找不到......等等问题

其实最近的这几篇文章主要是写给笔者自己看的,俗话说,「好记性不如烂笔头子」。相信大家也一定经历过,认不出来一个月前自己写的代码,这种情况。随着年龄不断增长,干的事情越来越多,这种现象越发的明显。

于是,把这些复用性比较强的技术点写出来,尤其是思路和代码,就显得性价比比较高了。

好了,絮絮叨叨说了一堆有的没的。最后还是强烈建议大家没事玩玩云开发,这方面大厂的同学会比较有优势,因为大厂内部基本都已经服务上云了。没有这个环境的同学们,只能自己多研究了。

共勉~~

相关推荐
小安运维日记1 小时前
Linux云计算 |【第四阶段】NOSQL-DAY1
linux·运维·redis·sql·云计算·nosql
waterHBO2 小时前
python 爬虫 selenium 笔记
爬虫·python·selenium
萌新求带啊QAQ8 小时前
腾讯云2024年数字生态大会开发者嘉年华(数据库动手实验)TDSQL-C初体验
云计算·腾讯云·tdsql-c
ZHOU西口9 小时前
微服务实战系列之玩转Docker(十五)
nginx·docker·微服务·云原生·swarm·docker swarm·dockerui
苓诣10 小时前
Submariner 部署全过程
云计算·k8s
GDAL15 小时前
全面讲解GNU:从起源到应用
服务器·云计算·gnu
bugtraq202115 小时前
闲鱼网页版开放,爬虫的难度指数级降低。
爬虫
无名之逆16 小时前
云原生(Cloud Native)
开发语言·c++·算法·云原生·面试·职场和发展·大学期末
Richardlygo17 小时前
(k8s)Kubernetes部署Promehteus
云原生·容器·kubernetes