前端可调用的通用爬虫 FaaS

2024年 1月 28日 73.2k 0

背景

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

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

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

闲话少说,上代码。

正文

服务端代码

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 消费的,也就是大概长这样(更多见 官方文档):

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

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

客户端代码

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

  • uni.request 是 uni-app 提供的内置方法,类似于 fetch、axios 这些,可以看它的 文档 了解更多;
  • 因为小程序环境下,没有一些浏览器内置的方法和对象,比如 URLSearchParams,所以解析 URL 的逻辑,得自己实现;
  • 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),否则在绑定的时候,根本就找不到……等等问题

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

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

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

    共勉~~

    相关文章

    KubeSphere 部署向量数据库 Milvus 实战指南
    探索 Kubernetes 持久化存储之 Longhorn 初窥门径
    征服 Docker 镜像访问限制!KubeSphere v3.4.1 成功部署全攻略
    那些年在 Terraform 上吃到的糖和踩过的坑
    无需 Kubernetes 测试 Kubernetes 网络实现
    Kubernetes v1.31 中的移除和主要变更

    发布评论