likes
comments
collection
share

我也来爬一爬12306 - Day4 车次今日的主要工作内容,是通过分析12306页面和相关的信息查询接口,发现可以是基

作者站长头像
站长
· 阅读数 55

本文是《我也来爬一爬12306》系列文件中,第四天的内容,本系列文章的起始文章是:

概述

在前三天的工作中,虽然看起来我们开展的实际工作并不多,但实际上,是在为后面的工作,建立一个比较好的基础。包括第一天进行了总体设计;第二天分析了12306主页的内容、HTTP请求处理的过程、HTML文本内容解析;第三天建立了数据库的操作模式。

在第四天,我们就可以进入项目的第二阶段的目标,就是获取车次信息。

车站关联车次

在12306主页上面,是没有办法直接看到一个车次列表的。但是,经过实际操作和应用的分析,我们可以在主页上,看到一个“常用查询”的版块。当我们输入或者选择车站的时候,会在下面的“车次”选择框中,发现和这个车站关联的所有车次的信息。这样,理论上来说,我们就可以通过遍历车站,获得所有车次的编码了。

我也来爬一爬12306 - Day4 车次今日的主要工作内容,是通过分析12306页面和相关的信息查询接口,发现可以是基

进一步分析这个页面的操作,和网络请求的状态,我们发现,上面的操作,是通过一个接口来实现的。即车站车次接口。

车站车次接口

如果进一步进行检查和分析,我们可以发现并归纳这个接口的技术特性如下:

  • 地址: www.12306.cn/index/otn/z…
  • 请求方式: POST,FORM方式
  • 请求参数: train_station_code(车站编码,电报码,如SHH)
  • 响应内容: JSON格式,主要内容是一个车次代码的列表,封装在data属性中
  • 响应性能: 以SSH为例,4KB,200ms,性能一般

下列是相关的请求和响应信息(来自Chrome开发工具):

// Request 
1 requests 3.1 kB transferred 8.7 kB resources
Request URL: https://www.12306.cn/index/otn/zwdch/queryCC
Request Method: POST
Status Code: 200 OK
Remote Address: 183.222.232.186:443
Referrer Policy: strict-origin-when-cross-origin

// Payload Form Data
train_station_code=SHH

// Reponse Header
access-control-allow-origin: *
connection: keep-alive
content-encoding: gzip
content-type: application/json;charset=UTF-8
ct: C2_240_46_6
date: Thu, 01 Aug 2024 10:10:18 GMT
transfer-encoding: chunked
...

// Response
{
    "validateMessagesShowId": "_validatorMessage",
    "status": true,
    "httpstatus": 200,
    "data": [
        "K4915",
        "K359",
        "K2185",
        ...

// Timing
Request Sent: 0.26ms
Waiting for server response: 256.64ms
Content Download: 20.92ms
Explanation: 279.21ms

车站车次数据表

通过车站车次接口,我们可以获得车站和车次之间的对应关系。如果要记录这个关系,我们需要设计并创建这个车站车次表。 另外,虽然在车站信息表中,我们其实暂时无法看到车站的级别信息,在车站车次接口中,也无法看到这个车站在车次中的位置(如始发、终点、中间等)。但是,现在这些限制,不影响我们完成当前的目标。我们现在只需要一个简单的车站车次的关联信息表。 这个表的表结构可以先定义如下:

SQL_TABLE_STRAINS = `${SQL_CREATE} ${DB.TABLE_STRAINS} 
(scode text , tcode text, iorder integer default 0, iflag interger default 0, UNIQUE(scode,tcode))`,

这里的主要数据定义如下:

  • scode: 车站代码
  • tcode: 车次代码
  • iorder: 车站在车次中的次序,默认为0,未设置

车次信息获取和存储

在了解了以上的接口特性之后,我们就可以在数据库中,车站列表的基础上,来依次构造车站查询操作,并获取其对应的车次列表,并在数据库中,构造一个车站车次的对应关系。

相关示例代码如下:


const 
SQL_STATIONS = `select scode,name from ${DB.TABLE_STATIONS} order by tcount desc`,
SQL_STATION_TRAINS = `insert into ${DB.TABLE_STRAINS} (scode,tcode) values __VALUES__ on conflict(scode,tcode) do nothing`,
SQL_STRAIN_COUNT   = `update ${DB.TABLE_STATIONS} set tcount = ? where scode = ? `;

const getTrains = async(stationCode)=>{
    // load trains for station
    let rdata = await tlPost(tlConfig.URL_QUERYCC,{
        train_station_code: stationCode // "GZQ"
    });

    try {
        rdata = JSON.parse(rdata);        
        if (rdata?.status) return rdata.data;
    } catch (error) {
        console.log(error);
    }
    return null;
};

const loadStationTrains = async()=>{
    // 查询并加载车站表
    let svalue,rdata,slist = await dbQuery(SQL_STATIONS);

    // query station's trains by request 12306
    for(const station of slist) {
        // 获取车站车次
        rdata = await getTrains(station.scode);
        if (!rdata || rdata?.length == 0) continue;
        
        // 更新车站车次数量 
        await dbExec(SQL_STRAIN_COUNT,[rdata.length,station.scode]);
        
        // 插入车站车次记录
        svalue  =  rdata.map(v=>`("${station.scode}","${v}")`).join(",");
        // console.log(svalue);
        await dbExec(SQL_STATION_TRAINS.replace("__VALUES__",svalue));
        
        console.log(station.scode, rdata?.length); //break;
        // break; // 4 test;
    };
};

这里的基本过程和要点如下:

  • 从已有的车站表中,获取车站列表和编码
  • 遍历车站列表,以编码作为参数,通过一个分析页面得到的信息查询接口,获得此车站关联的车次列表
  • 将车站和车次对应关系和信息,写入数据库
  • 可以对车站对应的车次数量,初步评估此车站的基本(车次越多,级别越高)

Sleep

笔者在编码到这一阶段中,在实际的执行和操作中,发现了一个问题,就是迭代车次信息查询列车编码的时候,发现过一段时间这个接口就会报错了(好像是503错误),但实际上在手动操作的时候,都没有这个问题。

所以,笔者认为,这应当是12305的反爬虫机制起来作用,它会阻挡短时间内大量发起的网络查询请求,这个处理确实是对的,因为这并不是正常人工操作的结果。

经过研究,笔者使用一个冷却机制,随机的间隔一段时间再发起请求,来降低短时间内请求的数量,基本上解决了这个问题。当然代价就是要爬取全量的数据,要花的时间是比较长的。

这个操作的核心,就是一个sleep函数(定义在库文件中),它的定义是这样的:

// sleep 函数定义
const sleep = (timeout)=>new Promise(r=>setTimeout(()=> r(1), timeout));

// 调用方式,必须在异步函数中
await sleep(2000 + Math.random() * 2000);

问题和后续

前面已经获取了车站对应的车次列表,但我们发现返回的,就只有一个简单的对应关系,没有其他的信息,比如这个车站是起点站或者终点站,车次状态等等。

经过业务方面的研究,发现这实际上是一种业务方面的特点或者设计。这里的车次代码,只是一个代码,并不是一趟具体的运行计划。因为那个计划,是和时间是相关的。进一步理解,就是一个车次,在某个日期执行,它会被另外编一个代码(列车代码),然后相关的时刻表,是基于这个代码来构成的。相同的车次,在不同的日期中,可能它的列车代码也不同。这样,我们也可以理解,为什么车次这个信息,也会经常调整和变化了。

这样的分析,也能够进一步给我们一个思路,就是要获得时刻表,可能需要安装时间和车次来进行查询,这也是我们下一步的工作计划。

小结

今日的主要工作内容,是通过分析12306页面和相关的信息查询接口,发现可以是基于车站列表,来查询每个车站所管理的车次,这样可以通过遍历车站,获得一个比较完整的车站-车次映射表。在这一节点,并没有解决车站在车次中的状态和地位的问题。但通过业务分析,我们基本确定了通过车次结合时间来进一步获取列车编码和时刻表的思路。

转载自:https://juejin.cn/post/7400269590142730252
评论
请登录