最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 爬虫初探(原神抽卡记录导出实践)

    正文概述 掘金(孤雨随风zz)   2021-04-10   2571

    背景

      二月份入坑了原神,原神的抽卡记录网页没有汇总的扇形图且展示Table被定死为了6,对于自己抽卡次数,每次的出货频率以及下一次保底的距离的计算是很麻烦的,本着技术服务生活,于是我打算自制一个网页,用于获取原神抽卡数据,自制为更加清晰的统计+扇形图展示
      先上结果图~

    爬虫初探(原神抽卡记录导出实践)

    预备工作

      获取原神抽卡页面的URL,手机打开原神抽卡页面,开启飞行模式,刷新页面即可获得一个错误网页,将其URL复制下来即可 爬虫初探(原神抽卡记录导出实践)

    获取数据的几种尝试

    静态爬虫

      初探爬虫我做的第一件事就是去百度,看网上说的爬取一个网页只需要用http或者一些第三方库访问对应页面,获取dom节点中的元素即可。于是开始了我的第一次尝试,我采用的是superagent

    // 引入所需要的第三方包
    const superagent= require('superagent');
    let recordData = [];
    superagent.get('目标URL').end((err, res) => {
    if (err) {
      console.log(`抽卡记录抓取失败 - ${err}`)
    } else {
     // 抓取热点新闻数据
     recordData = getRecord(res);// 从res中
    }
    });
    
    const cheerio = require('cheerio');
    let getRecord = (res) => {
    let recordData = [];
    
    /* 使用cheerio模块的cherrio.load()方法,将HTMLdocument作为参数传入函数
       以后就可以使用类似jQuery的$(selectior)的方式来获取页面元素
     */
    let $ = cheerio.load(res.text);
    
    // 找到目标数据所在的页面元素,获取数据
    $('.table-content>div>type').each((idx, ele) => {
      // cherrio中$('selector').each()用来遍历所有匹配到的DOM元素
      // 参数idx是当前遍历的元素的索引,ele就是当前便利的DOM元素
      recordData.push({
          type:$(ele).text()
      })              // 存入最终结果数组
    });
    // ... 将其他数据填入数组
    return recordData
    };
    

      于是尴尬的事情发生了,我一步一步log 发现返回的res里面压根就没有内部的表格元素,查阅资料以后才知道是因为Table的数据是请求了另一个接口获取的,对于这种动态页面有两种方案
    采取能够模拟浏览器或者通过抓包分析接口,自行请求。

    模拟浏览器

      模拟浏览器我采取了puppeteer库,通过模拟对于按钮,选择器的点击进行不同的请求从而获取最终的数据

    const puppeteer = require("puppeteer");
    const TEST_URL =""
    const getTargetData = async () => {
      // 启动浏览器
      const browser = await puppeteer.launch({
        headless: false, // 默认是无头模式,这里为了示范所以使用正常模式
      });
    
      // 控制浏览器打开新标签页面
      const page = await browser.newPage();
      page.setViewport({
        width: 0,
        height: 1500,
      });
      // 在新标签中打开要爬取的网页
      await page.goto(TEST_URL);
      // 存储数据
      const types = [];
      const names = [];
      const dates = [];
      let poolSelect = [
        { poolName: "常驻", records: [] },
        { poolName: "新手", records: [] },
        { poolName: "角色", records: [] },
        { pullName: "武器", records: [] },
      ];
      return  await new Promise((resolve, reject) => {
        const loadData = async (index) => {
          let end = false;
          await page.waitForTimeout(600 * index);
          await page.click(".select-container");
          await page.waitForSelector(".ul-list");
          await page.click(`.item:nth-child(${index+1})`);
          await page.waitForTimeout(300);
          while (!end) {
            const { currentData, length } = await page.evaluate(async () => {
              // 因为需要请求时间 所以需要一个delay
              const getData = async () => {
                const currentTypes = document
                  .querySelectorAll(".table-content>div")[1]
                  .querySelectorAll(".type");
                const currentNames = document
                  .querySelectorAll(".table-content>div")[1]
                  .querySelectorAll(".name");
                const currentTimes = document
                  .querySelectorAll(".table-content>div")[1]
                  .querySelectorAll(".time");
                const currentData = [...currentTypes].map((item, index) => {
                  const [name, level] = currentNames[index].innerHTML
                    .split("\n")
                    .filter(
                      (item) =>
                        item != undefined &&
                        item != null &&
                        item.trim().length !== 0
                    )
                    .map((item) => item.trim());
                  return {
                    type: currentTypes[index].innerHTML,
                    name,
                    level: level ? level : "(三星)",
                    time: currentTimes[index].innerHTML,
                  };
                });
                console.log(currentData);
    
                return {
                  length: currentTypes.length,
                  currentData,
                };
              };
              let { length, currentData } = await getData();
              console.log(currentData);
              return {
                length,
                currentData,
              };
            });
            poolSelect[index] = {
              ...poolSelect[index],
              records: [...poolSelect[index].records, ...currentData],
            };
    
            if (length < 6) {
              end = true;
              if (index >= poolSelect.length) {
                break;
              }
              loadData(index + 1);
            } else {
              page.click(".page-item.to-next.selected");
              await page.waitForTimeout(300);
            }
          }
        };
        loadData(0);
        resolve(poolSelect);
      })
        .then((res) => console.log(res))
        .catch((err) => console.log(err));
    };
    

    这回是可以获取一些数据的,但是可能是因为我网络的原因,对于一些请求速度较慢的数据产生了丢失,需要增长点击以后的等待时间才能正确的获取数据,那这样就导致爬取的时间很长。

    抓包分析接口

    抓包我采取的是 Mac 的 Charles青花瓷。访问目标网站即可抓包

    踩了一些坑

    • 抓到的数据包访问的显示是<unknown>爬虫初探(原神抽卡记录导出实践)

    这是因为原神抽卡记录的网址是https。需要配置SSL Proxying

    • Help>SSL Proxying>Install Charles Root Certificate 然后在钥匙串对于证书配置始终信任 爬虫初探(原神抽卡记录导出实践)
    • 以上是搜索到的内容,但是我昨晚这些仍然显示unknown,后来发现还需要Proxy>SSL Proxy Setting中设置

    爬虫初探(原神抽卡记录导出实践)

    • 做完以上工作以后就可以开始对于数据包分析了

    爬虫初探(原神抽卡记录导出实践)

    正式编码

    const router = require("koa-router")();
    const https = require("https");
    const queryString = require("query-string");
    const targetUrl =
    "https://hk4e-api.mihoyo.com/event/gacha_info/api/getGachaLog";
    router.get("/test", async (ctx, next) => {
    const { url } = ctx.request.query;
    const { query } = queryString.parseUrl(url);
    // 新手100 常驻200 角色301 武器302
    const gacha_types = [100,200,301,302];
    const page = 1;
    const size = 6;
    const end_id = 0;
    const createReq = (page, size, gacha_type, end_id) => {
      return new Promise((resolve, reject) => {
        // 抓包可知 首次请求end_id 为 0 之后每下一页 end_id 为上一页的最后一个,直到返回结果长度小于size说明最大,结束递归
        const targetQuery = { ...query, gacha_type, page, size, end_id };
        const finalUrl = `${targetUrl}?${queryString.stringify(targetQuery)}`;
        https.get(finalUrl,  (res) => {
          let info = "";
          res.on("data", function (chunk) {
            info += chunk;
          });
    
          res.on("end", async function (err) {
            const resultList = JSON.parse(info).data.list;
            if (resultList.length < size) {
              console.log(`====正在请求==${gacha_type}==页码${page}`);
              return resolve(resultList);
            }
            // end_id 上一页最后一个 
            const afterResultList = await createReq(page + 1, size, gacha_type, resultList[resultList.length-1].id)
            resolve([
              ...resultList,
              ...afterResultList
            ]);
          });
        });
      });
    };
    ctx.body = await new Promise((resolve) => {
      const promiseList = gacha_types.map((gacha_type) => {
        return new Promise(async (resolve) => {
          const data = await createReq(page, size, gacha_type, end_id);
          resolve({
            gacha_type,
            data: (await createReq(page, size, gacha_type, end_id)).reverse(),
          });
        });
      });
      resolve(
        Promise.all(promiseList).then((values) => {
          console.log(values);
          return values;
        })
      );
    });
    });
    

    前端编码

    使用antd+umi+axios+echarts进行前端页面的Code.

    //App.js
    export default function() {
      const [url, setUrl] = useState('');
      const [loading, setLoading] = useState(false);
      const [result, setResult] = useState({});
      const poolNameMap = {
        newHandPool: {
          gacha_type: 100,
          name: '新手池',
        },
        alwaysPool: {
          gacha_type: 200,
          name: '常驻池',
        },
        rolePool: {
          gacha_type: 301,
          name: '角色池',
        },
        armsPool: {
          gacha_type: 302,
          name: '武器池',
        },
      };
      const handleSearch = async () => {
        setLoading(true);
        const { data } = await Axios.get('/test', { params: { url } });
        setResult(groupBy(data, 'gacha_type'));
        setLoading(false);
      };
      useEffect(() => {
        console.log(result);
      }, [result]);
      return (
        <Spin tip="Loading... 读取数据中,可能需要几十秒" delay={200} spinning={loading}>
          <div
            style={{
              width: '100vw',
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
              justifyContent: 'center',
            }}
          >
            <div style={{ display: 'flex', width: '30%', marginBottom: 16 }}>
              <Input value={url} onChange={setUrl} />
              <Button
                type="primary"
                onClick={() => {
                  handleSearch();
                }}
                style={{ marginLeft: 8 }}
              >
                查询
              </Button>
            </div>
            <Row gutter={[16, 16]} style={{ width: '40%' }}>
              <Col span={24}>
                <PoolCard
                  name={poolNameMap.alwaysPool.name}
                  data={get(result, `${poolNameMap.alwaysPool.gacha_type}.0.data`)}
                />
              </Col>
              <Col span={24}>
                <PoolCard
                  name={poolNameMap.rolePool.name}
                  data={get(result, `${poolNameMap.rolePool.gacha_type}.0.data`)}
                />
              </Col>
              <Col span={24}>
                <PoolCard
                  name={poolNameMap.armsPool.name}
                  data={get(result, `${poolNameMap.armsPool.gacha_type}.0.data`)}
                />
              </Col>
              <Col span={24}>
                <PoolCard
                  name={poolNameMap.newHandPool.name}
                  data={get(result, `${poolNameMap.newHandPool.gacha_type}.0.data`)}
                />
              </Col>
            </Row>
          </div>
        </Spin>
      );
    }
    
    
    // PoolCard.jsx
    import React, { useState, useEffect } from 'react'
    import { Card, Empty, Tag } from 'antd'
    import { isNil, filter, get, map } from 'lodash'
    import PieChart from '../PieChart'
    const PoolCard = ({ name, data }) => {
        const [goldCards, setGoldCards] = useState([]);
        const [noGoldTimes, setNoGoldTimes] = useState();
        useEffect(() => {
            setGoldCards(get5LevelDetail(data))
        }, [data])
        const get5LevelDetail = (data) => {
            if (isNil(data)) {
                return;
            }
            let lastLocation = 0;
            let goldThings = []
            data.forEach((item, index) => {
                if (get(item, `rank_type`) === '5') {
                    goldThings.push(
                        {
                            nums: index - lastLocation + 1,
                            datail: item,
                        }
                    )
                    lastLocation = index + 1;
                }
            })
            setNoGoldTimes(data.length - lastLocation);
            return goldThings;
        }
        const getChartData = (data) => {
            if (isNil(data)) {
                return;
            }
            let level_5 = 0;
            let level_4 = 0;
            let level_3 = 0;
            data.forEach((item, index) => {
                if (get(item, `rank_type`) === '5') {
                    level_5++;
                }
                if (get(item, `rank_type`) === '4') {
                    level_4++;
                }
                if (get(item, `rank_type`) === '3') {
                    level_3++;
                }
            })
            return [{
                name: '五星', value: level_5
            }, {
                name: '四星', value: level_4
            }, {
                name: '三星', value: level_3
            }];
        }
        return (
            <Card style={{ position:'relative' }}>
                <div>
                    <h3>{name}</h3>
                    <div style={{ display: 'flex', alignItems: 'center' }}>
                        {!isNil(noGoldTimes) ? <div>
                            已经<Tag color='processing' style={{ marginLeft: 8 }}>{noGoldTimes}</Tag>发没出金,
                        距离大保底还有<Tag color='warning' style={{ marginLeft: 8 }}>{90 - noGoldTimes}</Tag>发
                    </div> : null}
                    </div>
                </div>
                {!isNil(data) ? <div style={{position:'absolute',right:0,top:0,bottom:0,margin:'auto 0'}}>
                    <PieChart name={name} data={getChartData(data)} />
                </div> : null}
                {!isNil(data) ? <>
                    <div style={{ display: 'flex', justifyContent: 'space-between',maxWidth:300 }}>
                        <div>
                            <p>
                                抽取次数:{data.length}
                            </p>
                            <p>
                                五星次数:
                            <span style={{ marginRight: 8 }}>
                                    {
                                        get(goldCards, 'length')
                                    }
                                </span>
                                {
                                    map(goldCards, goldCard => <Tag color='success'>
                                        {goldCard.datail.name}({goldCard.nums})
                                </Tag>)
                                }
                            </p>
                            <p>
                                四星次数:
                        {
                                    filter(
                                        data,
                                        item => get(item, `rank_type`) === '4',
                                    ).length
                                }
                            </p>
                            <p>
                                三星次数:
                        {
                                    filter(
                                        data,
                                        item => get(item, `rank_type`) === '3',
                                    ).length
                                }
                            </p>
                        </div>
                    </div>
                </> : (
                        <Empty />
                    )}
            </Card>
        )
    }
    export default PoolCard
    
    // PieChart.jsx
    import React, { useEffect, useState } from 'react'
    import ReactEcharts from 'echarts-for-react';
    import config from './config'
    const colorMap = {
        '五星': 'yellow',
        '四星': 'purple',
        '三星': 'blue'
    }
    const RecordPieChart = ({ name, data }) => {
        const [options, setOptions] = useState({});
        useEffect(() => {
            config.title.text = '';
            config.legend.data = data.map(item => {
                return {
                    name: item,
                    textStyle: {
                        color: colorMap[item.name]
                    }
                }
            });
            config.series[0].data = data;;
            setOptions(config);
        }, [name, data])
        return (
            <div>
                <ReactEcharts style={{ width: 400, height: 250 }} option={options} />
            </div>
        )
    }
    
    export default RecordPieChart
    
    

    小结

      经过一天的努力,终于可以愉快的查看自己的抽卡记录,安心抽卡了。
      于是

    爬虫初探(原神抽卡记录导出实践)

    爬虫初探(原神抽卡记录导出实践)

    即使最后没有人为你鼓掌,也要优雅的谢幕,感谢自己的认真付出


    起源地下载网 » 爬虫初探(原神抽卡记录导出实践)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元