-
Notifications
You must be signed in to change notification settings - Fork 109
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 906f878
Showing
19 changed files
with
353 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
!offline_data/README.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
## 北京实时公交 | ||
|
||
该项目是北京实时公交查询接口的Python绑定,接口逆向自其[安卓客户端](http://www.bjjtw.gov.cn/topic/bjssgj/) | ||
|
||
### Quick Start | ||
|
||
* `pip install -r requirements.txt` 安装依赖 | ||
* `python manage.py build_cache` 获取离线数据,建立本地缓存 | ||
|
||
项目自带了一个终端中的查询工具作为例子,运行: `python manage.py query` | ||
|
||
### Roadmap | ||
|
||
- [x] 实现 beijing_bus 模块,提供需要的Python接口 | ||
- [x] 终端中的查询工具 | ||
- [ ] webapp | ||
- [ ] 微信公众号 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
#-*-coding:utf-8-*- | ||
|
||
from .cache import cache | ||
from .line import BusLine | ||
|
||
|
||
class BeijingBus(object): | ||
|
||
@classmethod | ||
def build_cache(cls): | ||
cache.invalidate(hard=True) | ||
cls.get_all_lines() | ||
|
||
@classmethod | ||
def get_all_lines(cls): | ||
return BusLine.get_all_lines() | ||
|
||
@classmethod | ||
def search_lines(cls, keyword): | ||
return list(BusLine.search(keyword)) |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
#-*-coding:utf-8-*- | ||
|
||
import logging | ||
import requests | ||
import xmltodict | ||
|
||
|
||
API_ENDPOINT = 'http://mc.aibang.com/aiguang/bjgj.c' | ||
REALTIME_ENDPOINT = 'http://bjgj.aibang.com:8899/bus.php' | ||
|
||
|
||
def request_api(url, params): | ||
r = requests.get(url, params=params, headers={'cid':1024}) | ||
return xmltodict.parse(r.text) | ||
|
||
|
||
def get_line_update_state(): | ||
logging.debug('Getting all lines') | ||
params = {'m': 'checkUpdate', 'version': '1'} | ||
return request_api(API_ENDPOINT, params) | ||
|
||
|
||
def get_bus_offline_data(line_id): | ||
logging.debug('Fetching line: %s' % line_id) | ||
params = {'m': 'update', 'id': line_id} | ||
return request_api(API_ENDPOINT, params) | ||
|
||
|
||
def get_realtime_data(line_id, station_num): | ||
params = { | ||
'city': '北京', | ||
'id': line_id, | ||
'no': station_num, | ||
'type': 2, | ||
'encrpt': 1, | ||
'versionid': 2 | ||
} | ||
return request_api(REALTIME_ENDPOINT, params) |
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
#-*-coding:utf-8-*- | ||
|
||
""" | ||
为了支持多种离线方案,使用了 dogpile.cache,支持 本地文件(默认), memcached, redis 等 | ||
参见文档:http://dogpilecache.readthedocs.org/en/latest/api.html#module-dogpile.cache.backends.memory | ||
""" | ||
|
||
from dogpile.cache import make_region | ||
|
||
|
||
cache_config_null = { | ||
'backend': 'dogpile.cache.null' | ||
} | ||
|
||
cache_config_file = { | ||
'backend': 'dogpile.cache.dbm', | ||
'arguments': { | ||
'filename': 'offline_data/bus_offline_data.dbm' | ||
} | ||
} | ||
|
||
|
||
cache = make_region().configure(**cache_config_file) |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
#-*-coding:utf-8-*- | ||
|
||
""" | ||
加密算法的实现 (实际上是RC4) | ||
http://zh.wikipedia.org/wiki/RC4 | ||
>>> cipher = Cipher('aibang127') | ||
>>> print cipher.decrypt('N+j17kGHx8Mj') | ||
运通122 | ||
""" | ||
|
||
import copy | ||
import base64 | ||
import hashlib | ||
|
||
|
||
class Cipher(object): | ||
|
||
def __init__(self, key): | ||
k = bytearray(self._md5(key)) | ||
self.s_box = self._get_s_box(k) | ||
|
||
def _md5(self, data): | ||
return hashlib.md5(data).hexdigest() | ||
|
||
def _get_s_box(self, key): | ||
s_box = range(256) | ||
j = 0 | ||
for i in range(256): | ||
j = (j + s_box[i] + key[i % len(key)]) % 256 | ||
s_box[i], s_box[j] = s_box[j], s_box[i] | ||
return s_box | ||
|
||
def calc(self, data): | ||
s_box = copy.copy(self.s_box) | ||
results = range(len(data)) | ||
j = 0 | ||
for i in range(len(data)): | ||
k = (i + 1) % 256 | ||
j = (j + s_box[k]) % 256 | ||
s_box[j], s_box[k] = s_box[k], s_box[j] | ||
n = (s_box[j] + s_box[k]) % 256 | ||
results[i] = data[i] ^ s_box[n] | ||
return results | ||
|
||
def decrypt(self, message, decode=base64.b64decode): | ||
message = decode(message) | ||
data = bytearray(message) | ||
return str(bytearray(self.calc(data))) | ||
|
||
def encrypt(self, message, encode=base64.b64encode): | ||
message = encode(message) | ||
data = bytearray(message) | ||
return str(bytearray(self.calc(data))) |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
#-*-coding:utf-8-*- | ||
|
||
from datetime import datetime | ||
|
||
from . import api | ||
from .cache import cache | ||
from .cipher import Cipher | ||
from .station import BusStation | ||
|
||
|
||
class BusLine(object): | ||
|
||
def __init__(self, **kwargs): | ||
self.stations = [] | ||
for name, value in kwargs.iteritems(): | ||
setattr(self, name, value) | ||
|
||
def __repr__(self): | ||
return '<Line: %s>' % self.name | ||
|
||
@classmethod | ||
@cache.cache_on_arguments() | ||
def get(cls, id): | ||
resp_doc = api.get_bus_offline_data(id) | ||
busline = resp_doc['root']['busline'] | ||
|
||
key = 'aibang%s' % id | ||
cipher = Cipher(key) | ||
d = cipher.decrypt # for convenience | ||
|
||
line = cls(**{ | ||
'id': int(busline['lineid']), | ||
'name': d(busline['linename']), | ||
'short_name': d(busline['shotname']), | ||
'distince': float(busline['distince']), | ||
'ticket_type': busline['ticket'], | ||
'price': float(busline['totalPrice']), | ||
'running_time': busline['time'], | ||
'line_type': busline['type'], | ||
'coords': d(busline['coord']), | ||
'status': busline['status'], | ||
'version': busline['version'] | ||
}) | ||
|
||
for station_data in busline['stations']['station']: | ||
name, no, lon, lat = map(d, station_data.values()) | ||
station = BusStation(name, float(lat), float(lon)) | ||
line.stations.append(station) | ||
|
||
return line | ||
|
||
@classmethod | ||
def gets(cls, ids): | ||
return [cls.get(id) for id in ids] | ||
|
||
@classmethod | ||
@cache.cache_on_arguments() | ||
def get_all_line_ids(cls): | ||
resp_doc = api.get_line_update_state() | ||
root = resp_doc['root'] | ||
line_ids = [line['id'] for line in root['lines']['line']] | ||
return line_ids | ||
|
||
@classmethod | ||
def get_all_lines(cls): | ||
line_ids = cls.get_all_line_ids() | ||
return cls.gets(line_ids) | ||
|
||
@classmethod | ||
def search(cls, name): | ||
for line in cls.get_all_lines(): | ||
if name in line.name: | ||
yield line | ||
|
||
def get_realtime_data(self, station_num): | ||
|
||
def t(ts): | ||
if float(ts) > -1: | ||
return datetime.fromtimestamp(float(ts)) | ||
|
||
data = [] | ||
resp_doc = api.get_realtime_data(self.id, station_num) | ||
for bus_data in resp_doc['root']['data']['bus']: | ||
key = 'aibang%s' % bus_data['gt'] | ||
d = Cipher(key).decrypt | ||
data.append({ | ||
'id': int(bus_data['id']), | ||
'lat': float(d(bus_data['x'])), | ||
'lon': float(d(bus_data['y'])), | ||
'next_station_name': d(bus_data['ns']), | ||
'next_station_num': int(d(bus_data['nsn'])), | ||
'next_station_distance': float(bus_data['nsd']), | ||
'next_station_arriving_time': t(bus_data['nst']), | ||
'station_distance': float(d(bus_data['sd'])), | ||
'station_arriving_time': t(d(bus_data['st'])), | ||
}) | ||
return data |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
#-*-coding:utf-8-*- | ||
|
||
|
||
class BusStation(object): | ||
|
||
def __init__(self, name, lat, lon): | ||
self.name = name | ||
self.lat = lat | ||
self.lon = lon | ||
|
||
def __eq__(self, other): | ||
return self.name == other.name | ||
|
||
def __repr__(self): | ||
return '<Station %s>' % self.name | ||
|
||
def get_num_in_a_line(self, line): | ||
try: | ||
return line.stations.index(self) + 1 | ||
except ValueError: | ||
return 0 |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
#-*-coding:utf-8-*- | ||
|
||
import time | ||
import click | ||
from datetime import datetime | ||
from beijing_bus import BeijingBus | ||
|
||
|
||
@click.group() | ||
def cli(): | ||
pass | ||
|
||
|
||
@click.command(help='build or re-build the cache') | ||
def build_cache(): | ||
import logging | ||
logging.basicConfig(level=logging.DEBUG) | ||
BeijingBus.build_cache() | ||
click.secho('Done!', fg='green') | ||
|
||
|
||
def echo_realtime_data(line, station_num): | ||
station = line.stations[station_num-1] | ||
realtime_data = line.get_realtime_data(station_num) | ||
click.clear() | ||
now = datetime.now().strftime('%H:%M:%S') | ||
title = '实时数据 [%s] 线路:%s (每5秒自动刷新,更新于%s)' % (station.name, line.name, now) | ||
click.secho(title, fg='green', bold=True) | ||
click.echo() | ||
realtime_data = filter(lambda d: d['station_arriving_time'], realtime_data) | ||
realtime_data.sort(key=lambda d: d['station_arriving_time']) | ||
for i, data in enumerate(realtime_data): | ||
click.secho('公交%s:' % (i+1), bold=True, underline=True) | ||
click.echo('距离 %s 还有%s米' % (station.name, data['station_distance'])) | ||
click.echo('预计 %s 到达' % data['station_arriving_time'].strftime('%H:%M')) | ||
click.echo() | ||
|
||
|
||
@click.command() | ||
def query(): | ||
q = click.prompt('请输入线路名', value_proc=str) | ||
lines = BeijingBus.search_lines(q) | ||
for index, line in enumerate(lines): | ||
click.echo() | ||
click.secho('[%s] %s' % (index+1, line.name), bold=True, underline=True) | ||
station_names = [s.name for s in line.stations] | ||
click.echo() | ||
click.echo('站点列表:%s' % ','.join(station_names)) | ||
|
||
click.echo() | ||
q = click.prompt('请从结果中选择线路编号', type=int) | ||
|
||
line = lines[q-1] | ||
click.clear() | ||
click.echo('你选择了 %s,下面请选择站点' % line.name) | ||
click.echo() | ||
for index, station in enumerate(line.stations): | ||
click.echo('[%s] %s' % (index+1, station.name)) | ||
|
||
click.echo() | ||
q = click.prompt('请从结果中选择线路编号', type=int) | ||
|
||
while True: | ||
echo_realtime_data(line, q) | ||
time.sleep(5) | ||
|
||
|
||
cli.add_command(build_cache) | ||
cli.add_command(query) | ||
|
||
|
||
if __name__ == '__main__': | ||
cli() |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
click | ||
requests | ||
xmltodict | ||
dogpile.cache |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
#-*-coding:utf-8-*- | ||
|