Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
wong2 committed Dec 5, 2014
0 parents commit 906f878
Show file tree
Hide file tree
Showing 19 changed files with 353 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!offline_data/README.md
17 changes: 17 additions & 0 deletions README.md
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
- [ ] 微信公众号
20 changes: 20 additions & 0 deletions beijing_bus/__init__.py
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 added beijing_bus/__init__.pyc
Binary file not shown.
38 changes: 38 additions & 0 deletions beijing_bus/api.py
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 added beijing_bus/api.pyc
Binary file not shown.
Binary file added beijing_bus/bus.pyc
Binary file not shown.
23 changes: 23 additions & 0 deletions beijing_bus/cache.py
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 added beijing_bus/cache.pyc
Binary file not shown.
57 changes: 57 additions & 0 deletions beijing_bus/cipher.py
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 added beijing_bus/cipher.pyc
Binary file not shown.
97 changes: 97 additions & 0 deletions beijing_bus/line.py
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 added beijing_bus/line.pyc
Binary file not shown.
21 changes: 21 additions & 0 deletions beijing_bus/station.py
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 added beijing_bus/station.pyc
Binary file not shown.
73 changes: 73 additions & 0 deletions manage.py
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 added offline_data/README.txt
Empty file.
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
click
requests
xmltodict
dogpile.cache
2 changes: 2 additions & 0 deletions webapp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#-*-coding:utf-8-*-

0 comments on commit 906f878

Please sign in to comment.