阅读 115

使用deno爬取红蓝球号码

这是我参与更文挑战的第 17 天,活动详情查看:更文挑战


前情介绍(为什么用deno?)

头两天看到一篇 使用 httpRequest + Jsoup 爬取红蓝球号码的文章,这是一个很简单的入门级爬虫案例。

我更熟悉前端的技术栈,使用 js 编写比较顺手。相对 node.js,deno 前期配置工作更少。

分析需求

  1. 确定达成目标:定期获取红蓝球的号码。
  2. 确定数据源:在这里
  3. 确定从数据源获取数据的方法:分析网页结构

打开上面提到的数据源连接,通过 F12 查看 网络选项卡 发现并没有调用 API 接口。如果发现有 API 接口调用,可以直接模拟请求接口收集数据

image.png

打开 list.html 发现所有的数据都在 html 文件中

image.png

因此,要想获得数据,只能通过分析 list.html 的网页结构

  1. 确定模拟请求和解析文档的工具
  • 模拟请求:deno 实现了一套浏览器 api,可以使用 fetch 方法直接获取
  • 解析文档:使用 deno-dom 库,可以将请求到的文档转换为 dom 树,这样就可以使用 document.querySelectorAll 这类方法来分析网页结构,获取数据。

实现

  1. 获取文档
import { DOMParser, Element } from "deno-dom";

const result = await fetch(`http://kaijiang.zhcw.com/zhcw/html/ssq/list.html`);
const html = await result.text();
复制代码

这里要特意提一句,如果你直接写如下代码,

import { DOMParser, Element } from "deno-dom";
复制代码

运行时会报错:

Relative import path "deno-dom" not prefixed with / or ./ or ../ from "file:///D:/caipiao/index.ts"
复制代码

这是正常的。这里的写法使用了 deno 的 import_map 特性

import_map 文件配置:

{
    "imports": {
       "deno-dom":"https://deno.land/x/deno_dom@v0.1.12-alpha/deno-dom-wasm.ts"
    }
}
复制代码
  1. 分析文档内容

image.png

使用 元素选项卡 对左侧表格进行审查。这是包含开奖信息的行数。

<tr>
    <!-- 开奖时间 -->
    <td align="center">2021-06-15</td>
    <!-- 期数 -->
    <td align="center">2021066</td>
    <td align="center" style="padding-left:10px;">
        <!-- 红色球,class="rr" -->
        <em class="rr">02</em>
        <em class="rr">06</em>
        <em class="rr">19</em>
        <em class="rr">26</em>
        <em class="rr">30</em>
        <em class="rr">33</em>
        <!-- 唯一的蓝球 -->
        <em>15</em>
    </td>
    <!-- 销售额 -->
    <td><strong>343,529,368</strong></td>
    <!-- 一等奖数量 -->
    <td align="left" style="color:#999;"><strong>14</strong></td>
    <!-- 二等奖数量 -->
    <td align="center"><strong class="rc">145</strong></td>
    <td align="center">
        <a href="http://www.zhcw.com/ssq/kjgg/" target="_blank"><img
                src="http://images.zhcw.com/zhcw2010/kaijiang/zhcw/ssqpd_42.jpg" width="16" height="16"
                align="absmiddle" title="详细信息"></a>
        <a href="http://www.zhcw.com/video/kaijiangshipin/" target="_blank"><img
                src="http://images.zhcw.com/zhcw2010/kaijiang/zhcw/ssqpd_43.jpg" width="16" height="16"
                align="absmiddle" title="开奖视频"></a>
    </td>
</tr>
复制代码

可以发现:

  • 每一组中奖数据按照开奖时间、期数、中奖号码、销售额、一等奖数量、二等奖数量依次排序
  • 中奖号码中,蓝色球在最后一个位置。每个中奖号码均被 <em> 标签包裹

同时观察表格可以发现,前两行是表头,从第三行到倒数第二行为中奖数据,最后一行是分页符。

所以,解析文档的方式就很明显了:

const doc = new DOMParser().parseFromString(html, "text/html");
if (doc) {
  // 去掉表头。从第三行开始包含中奖数据,前两行一定是表头。
  const rows = Array.from(doc.querySelectorAll("tr"))
  const dataRows = rows.slice(2, rows.length - 1);
  const data = dataRows.map((row) => {
    const cells = Array.from((row as Element).querySelectorAll("td"));
    const json = cells.reduce((collection, value, index) => {
      // 每一组中奖数据按照开奖时间、期数、中奖号码、销售额、一等奖数量、二等奖数量依次排序
      const dataKey = [
        "开奖时间", "期数", "中奖号码", "销售额", "一等奖数量", "二等奖数量"
      ]

      // 评估结果
      const evalFunctions = [
        (cell: Element) => cell.innerText,
        (cell: Element) => cell.innerText,
        (cell: Element) => Array.from(cell.querySelectorAll('em')).map(em => (em as Element).innerText),
        (cell: Element) => cell.innerText.replaceAll(',', ''),
        (cell: Element) => parseInt(cell.innerText.trim()),
        (cell: Element) => parseInt(cell.innerText),
      ]

      if(index < dataKey.length){
        // 只有在 dataKey 中的值,才存储
        collection.set(dataKey[index], evalFunctions[index](value as Element));
      }

      return collection;
    }, new Map())
    
    // Map 转换为 Object
    let obj = Object.create(null);
    for (let [k, v] of json) {
      obj[k] = v;
    }
    return obj;
  });
}
复制代码

之后可以将这组数据进行持久化保存。

运行脚本,

deno run --allow-net --import-map=import_map.json index.ts
复制代码

得到最终的数据如下:

image.png

文章分类
前端
文章标签