Dzpszied

实现北京实时公交API(二) 解析市公交集团DOM

北京公交是我发现的第一个较为准确的实时公交查询网站,这篇以北京公共交通集团为例,分析其页面数据最终生成一套API方案。

请求分析

以634路为例,在实时公交页面进行一次查询操作,在 Chrome 控制台可以看到,在选择线路与方向后各进行了一次请求。

实时公交页面

第一个请求 http://www.bjbus.com/home/ajax_rtbus_data.php?act=getLineDirOption&selBLine=634 打开后是一个完整的DOM页面。

<!-- act = getLineDirOption -->
<option value="">请选择行车方向</option>
<option value="4715326590914336491">634(保福寺桥西-郭庄子公交场站)</option>
<option value="5385042765424051829">634(郭庄子公交场站-保福寺桥西)</option>

<!-- act = getLineDir -->
<a href="javascript:;" data-uuid="4715326590914336491">634(保福寺桥西-郭庄子公交场站)</a>
<a href="javascript:;" data-uuid="5385042765424051829">634(郭庄子公交场站-保福寺桥西)</a>

第二个请求 http://www.bjbus.com/home/ajax_rtbus_data.php?act=getDirStationOption&selBLine=634&selBDir=5385042765424051829 同样。

<!-- act = getLineDirOption -->
<option value="">请选择上车站</option>
<option value="1">郭庄子公交场站</option>
<option value="2">兴源路</option>
<option value="3">兴源路口北</option>
	...
<option value="36">保福寺桥西</option>

<!-- act = getLineDir -->
<a href="javascript:;" data-seq="1">郭庄子公交场站</a>
<a href="javascript:;" data-seq="2">兴源路</a>
<a href="javascript:;" data-seq="3">兴源路口北</a>
	...
<a href="javascript:;" data-seq="36">保福寺桥西</a>

所以,网站的查询流程是选中某一条公交的线路,然后调用getLineDir方法请求到线路的所有方向并填充到第二个选择框,选择完方向后请求得到这个方向上的所有站点。

选择完上车点后点击查询按钮,我们得到了第三个请求 http://www.bjbus.com/home/ajax_rtbus_data.php?act=busTime&selBLine=634&selBDir=5385042765424051829&selBStop=31 。这是一个json文件。

请求三

其中有三个属性:html、seq、w。刨除掉后两个不明所以的属性,可以看到,html包含了一套万恶之源dom(吐槽一下,既然都是json了为什么还要强行再包装成html...给随后的解析增加了不少工作量.....)

<div class="inquiry_header">
    <div class="left fixed">
        <h3 id="lh">634路</h3>
    </div>
    <div class="inner">
        <h2 id="lm">保福寺桥西-郭庄子公交场站</h2>
        <article>
            <p>知春里 6:30-23:15 分段计价 所属客四分公司</p>
            <p>最近一辆车距离此还有 1 站, 
                <span>698</span> 米,预计到站时间
                <span>2</span> 分钟
            </p>
        </article>
    </div>
</div>
<div id="cc_stop" class="inquiry_main" unselectable="on" onselectstart="return false;">
    <ul class="fixed">
        <li>
            <div id="1">
                <i></i>
                <p class="sicon"></p>
                <span title="保福寺桥西">保福寺桥西</span>
            </div>
        </li>
        <li>
            <div id="2m">
                <i ></i>
            </div>
        </li>
        <li>
            <div id="2">
                <i ></i>
                <p class="sicon"></p>
                <span title="保福寺桥南">保福寺桥南</span>
            </div>
        </li>
        <li>
            <div id="3m">
                <i ></i>
            </div>
        </li>
        <li>
            <div id="3">
                <i></i>
                <p class="sicon"></p>
                <span title="白塔庵北">白塔庵北</span>
            </div>
        </li>
        <li>
            <div id="4m">
                <i class="busc" clstag="2095"></i>
            </div>
        </li>
        <li>
            <div id="4">
                <i class="buss" clstag="-1"></i>
                <p class="sicon"></p>
                <span title="知春里东站">知春里东站</span>
            </div>
        </li>
        <li>
            <div id="5m">
                <i ></i>
            </div>
        </li>
        <li>
            <div id="5">
                <i></i>
                <p class="sicon"></p>
                <span title="知春里" style="font-size: 16px;font-weight:700;">知春里</span>
            </div>
        </li>
        
        	......
        <li>
            <div id="38m">
                <i  class="busc" clstag=""></i>
            </div>
        </li>
        <li>
            <div id="38">
                <i></i>
                <p class="sicon"></p>
                <span title="郭庄子公交场站">郭庄子公交场
                    <br/>...
                </span>
            </div>
        </li>
    </ul>
</div>
<div class="inquiry_footer">
    <section>
        <div class="inner">
            <span class="buss">途中车辆</span>
            <span class="busc">到站车辆</span>
        </div>
    </section>
</div>

从这串DOM可以看到这些信息:

  • 该线路的名称、方向
  • 当前站、运营时间、计价方式、所属公司
  • 最近一辆距当前站的站数、距离、预计到达分钟数
  • 当前方向所有站名
  • 所有在线公交车的粗略位置

好了,这就是接下来要用到的所有数据来源,下面想办法怎么解析出来。

解析DOM

我用的API框架是 Lumen。这是由 php 框架 Laravel 衍生出的一个轻量级框架,接下来将用 php 来解析上面的DOM。

获取某线路的方向

只有两个 a 标签,data-uuid 属性为该方向的 id 值。

<a href="javascript:;" data-uuid="4715326590914336491">634(保福寺桥西-郭庄子公交场站)</a>
<a href="javascript:;" data-uuid="5385042765424051829">634(郭庄子公交场站-保福寺桥西)</a>

SimpleXML 是 PHP 5 中的新特性,无需安装就可以便捷地解析简单的 XML。由于获取方向的 DOM 较为简单,我们采用 SimpleXML 来实现。

const HEAD_URL = 'http://www.bjbus.com/home/ajax_rtbus_data.php';

function _getLineDir($line) {
    $url = self::HEAD_URL.'?act=getLineDir&selBLine='.$line;
    $content = '<response>'.file_get_contents($url).'</response>';
    $xml = simplexml_load_string($content)->a; // 获取所有的a标签

    $res = array();
    $res['count'] = $xml->count();

    foreach ($xml as $item) {
        $res['data'][] = array(
            "id" => (string) $item['data-uuid'],
            "name" => $this->_getBetween($item, "(", ")")
        );
    }
    return $res;
}
/* 
{
    "count": 2,
    "data": [
        {
            "id": "4715326590914336491",
            "name": "保福寺桥西-郭庄子公交场站"
        },
        {
            "id": "5385042765424051829",
            "name": "郭庄子公交场站-保福寺桥西"
        }
    ]
} 
*/

获取该方向上的站点

该 DOM 结构与上节高度相似,所以同样采用 SimpleXML 实现。

function _getDirStation($line, $dir) {
    $url = self::HEAD_URL.'?act=getDirStation&selBLine='.$line.'&selBDir='.$dir;
    $content = '<response>'.file_get_contents($url).'</response>';
    $xml = simplexml_load_string($content)->a;

    $res = array();
    $res['count'] = $xml->count();

    foreach ($xml as $item) {
        $res['data'][] = array(
            "id" => (string) $item['data-seq'],
            "name" => (string) $item
        );
    }

    return $res;
}

获取实时数据

从原始 DOM 可以看到,每个站点及站点之间的 div 中,都含有一个 i 标签。

<li>
    <div id="2">
        <i ></i>
        <p class="sicon"></p>
        <span title="保福寺桥南">保福寺桥南</span>
    </div>
</li>
<li>
    <div id="4m">
        <i class="busc" clstag="2095"></i>
    </div>
</li>
<li>
    <div id="4">
        <i class="buss" clstag="-1"></i>
        <p class="sicon"></p>
        <span title="知春里东站">知春里东站</span>
    </div>
</li>

发现有些站点的 i 标签为空,有些不为空,可以判断这就是公交位置的占位符。当这一区域存在公交车时(如id=4m,4),就向其中填充数据。而 4m 表示“第三站已过,第四站未到”的中间位置。

clstag 属性代表当前车辆距离当前站点的距离,单位为米。该数值仅在车未过站,且车在站间(即 id 为 *m)时才会显示,车辆过站或在某一站停靠时均为 -1。

这部分 DOM 比较复杂,需要用更强劲一点的解析工具。

在前些时候,我试用了 DOMDocument。这是一套比较强大的 DOM 解析器,同样被内置在 php 中。然而试用下来几天发现解析并不稳定,可能是由于这个 DOM 不够规范导致程序报错。后来,我换用了一套更宽容的第三方库 Simple Html Dom

function _getBusTime($line, $dir, $stop_id) {
    $url = self::HEAD_URL.'?act=busTime&selBLine='.$line.'&selBDir='.$dir.'&selBStop='.$stop_id;
    $content = html_entity_decode((string) json_decode(file_get_contents($url))->html); // 从json中抽取html

    $replace = [' ',' ',' ',' '];
    $res = array();

    $html = HtmlDomParser::str_get_html($content); // DOM对象

    $line = $html->find('h3', 0)->plaintext; // h3即线路名称
    $res['name'] = $line;
    $res['dir'] = $html->find('h2', 0)->plaintext; // h2为线路方向

    $desc = str_replace($replace, "", $html->find('p', 1)->plaintext).".";
    $res['desc'] = $desc;

    $buses = $html->find('i'); // 获取每一辆车的位置
    foreach ($buses as $bus) {
        if (isset($bus->class)) {
            $res['buses'][] = array(
                "poi" => $bus->parent()->id,
                "distance" => $bus->clstag
            );
        }
    }

    return $res;
}

/*
{
    "name": "634路",
    "dir": "保福寺桥西-郭庄子公交场站",
    "desc": "最近一辆车距离此还有2站,1.21公里,预计到站时间3分钟.",
    "buses": [
        {
            "poi": "2m",
            "distance": "3210"
        },
        {
            "poi": "5",
            "distance": "-1"
        },
        {
            "poi": "20",
            "distance": "-1"
        },
        {
            "poi": "30",
            "distance": "-1"
        }
    ]
}
*/

进阶功能

仅仅实现这三个功能是不够的,因为我认为官网提供的这套流程实在不够人性化:

  • 只能输入线路名称进行查询,不能根据公交站查询;
  • 必须输入上车位置,有时候只是想看一下车在什么位置;
  • 无法换向。

接下来,将对 DOM 进行进一步解析,逐步解决这几个痛点。

第一辆车的数据

因为在前面的 desc 中,已经拿到了包含第一辆车信息的字符串:

最近一辆车距离此还有0站,1.84公里,预计到站时间3分钟.

所以,要分离出第一辆车的信息,只要做字符串的截取就可以了。

第二辆车的数据

由于字符串中没有描述第二辆车的信息,所以只能从所有在线的公交列表中遍历出第二辆车的数据。

foreach ($buses as $bus) { // 遍历在线的公交列表
    $item_poi = $bus->parent()->id;
    if (isset($bus->class)) {
        $buses_list[] = array(
            "poi" => $bus->parent()->id,
            "distance" => $bus->clstag
        );

        // 第二辆车的数据,需要遍历列表
        if ($stop_id - (int)$item_poi > (int)$first_poi_raw) {
            $second_bus = array(
                "poi" => ($stop_id - (int)$item_poi)."站", // 当前站数减去公交车所在的站数,得到公交车距离当前位置还有几站。
                "distance" => $bus->clstag == "-1" ? "到站" : sprintf("%.2f", ($bus->clstag)/1000)."公里"
            );
        }
    }

}

字符串的替换

官网返回的数据有一些是不够准确的,可以按照如下原则进行逐一处理:

  • 距离当前还有0站,0米:公交车到站了;
  • 距离当前还有0站,大于0米:公交车即将到站;
  • 该方向上无车辆运行/车辆均已过站:可能是没有实时数据,如运通公交。

换向

换向是需求很高的一个功能,可惜我们只能自己去实现。

$dir_opposite = "";
if ($dir_list['count'] == 2) { // 这里验证它是不是只有一个方向,如xx内环
    if ($dir_list['data'][0]['id'] == $dir) {
        $dir_opposite = $dir_list['data'][1]['id'];
    } else {
        $dir_opposite = $dir_list['data'][0]['id'];
    }
}

效果

集成上述功能后,现在 _getBusTime 函数的返回值如下。

具体的 API 地址及文档会随后更新。

{
    "name": "394路",
    "success": true,
    "dir": {
        "id": "5036565648772945865",
        "name": "韩家川南站-北京西站",
        "from": "韩家川南站",
        "to": "北京西站",
        "opposite": "5148641961736494681",
        "list": {
            "count": 2,
            "data": [
                {
                    "id": "5036565648772945865",
                    "name": "韩家川南站-北京西站"
                },
                {
                    "id": "5148641961736494681",
                    "name": "北京西站-韩家川南站"
                }
            ]
        }
    },
    "stop_id": "22",
    "desc": "最近一辆车距离此还有0站,1.84公里,预计到站时间3分钟.",
    "first": {
        "poi": "即将到站",
        "distance": "1.84公里",
        "time": "3分钟"
    },
    "second": {
        "poi": "8站",
        "distance": "12.69公里"
    },
    "buses": [
        {
            "poi": "4m",
            "distance": "19139"
        },
        {
            "poi": "14m",
            "distance": "12686"
        },
        {
            "poi": "22m",
            "distance": "1835"
        },
        {
            "poi": "23m",
            "distance": ""
        }
    ]
}

  • 由于 php 原生的file_get_contents函数不够稳定,后来采用了CURL方法来实现,由于原理相似,文中代码就不再更新了。

评论