├── capture.db ├── stcp.pcapng ├── stcp_2.pcapng ├── .gitattributes ├── README.md ├── .gitignore ├── stcp.py └── capture.py /capture.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laxnring/STCP-API/HEAD/capture.db -------------------------------------------------------------------------------- /stcp.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laxnring/STCP-API/HEAD/stcp.pcapng -------------------------------------------------------------------------------- /stcp_2.pcapng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laxnring/STCP-API/HEAD/stcp_2.pcapng -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stcp_api 2 | 3 | An unofficial API for the public bus system in Porto, Portugal, STCP (Serviço de Transportes Coletivos do Porto). 4 | 5 | Since STCP does not provide its users with a freely accessible API to check the departure times in real time, I've decided to sniff the HTTP requests made by the SMSBus app, available for Windows. This app, developed by STCP themselves, communicates with a real-time server using an uid. This can easily be identified due to the lack of encrypted communication between client and server. Using this uid, one can simulate the app and send an HTTP request and retrieve the departure times, as well as a collection of all stops and lines in the whole service network. It provides the user with real-time data about the time it will take for a bus of a certain line to arrive at said stop. 6 | 7 | The api consists of the following functions, which can be used to access all real-time data about the times of the buses: 8 | - getLines() - Returns all the lines of the bus service. 9 | - getStops(line) - Returns the stops for a particular line. 10 | - getTimes(stop) - Returns the time until the next departures from a particular stop. 11 | 12 | The file capture.py contains an application of the api to check whether a bus has passed a certain stop, adding it to a sqlite database (capture.db). It currently cycles over all stops of the whole network and checks whether a bus has passed the stop. Using that I'm planning on building a program that reliably predicts when a given bus will arrive at a certain stop. 13 | 14 | Required Python libraries: 15 | - Requests 16 | - SQLite3 17 | - Multiprocessing 18 | 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /stcp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os, sys 4 | import requests 5 | 6 | link = "www.stcp.pt/pt/widget/post.php?" 7 | uid = "d72242190a22274321cacf9eadc7ec5f" 8 | submete = "Mostrar" 9 | 10 | # Retorna apenas os números das linhas 11 | def getLinhas(): 12 | request_url = "http://www.stcp.pt/pt/itinerarium/callservice.php?action=lineslist&service=1" 13 | r = requests.get(request_url) 14 | response = r.content.decode() 15 | linhas = [] 16 | num_linhas = int(response.split('"recordsReturned": ')[1][:2]) 17 | for i in range(0, num_linhas-1): 18 | linha = response.split('"code": ')[i+1] 19 | linhas.append(linha[:4].strip('"').strip(",")) 20 | linhas.remove('1"') 21 | return linhas 22 | 23 | linhas = getLinhas() 24 | 25 | def getParagens(linha, ldir): 26 | request_url = "http://www.stcp.pt/pt/itinerarium/callservice.php?action=linestops&lcode="+ linha + "&ldir=" + str(ldir) 27 | r = requests.get(request_url) 28 | response = r.content.decode() 29 | 30 | num_paragens = int(response.split('"recordsReturned": ')[1][:2]) 31 | 32 | paragens = [] 33 | for i in range(0, num_paragens-1): 34 | 35 | paragem = response.split('"code": ')[i+1] 36 | np = response.split('"name": ')[i+1].split('"')[1] 37 | paragem = paragem[:5].strip('"').strip(",") 38 | paragem_duo = [paragem, np] 39 | paragens.append(paragem_duo) 40 | return paragens 41 | 42 | #print(paragens) 43 | # Obter pagina relativa a uma paragem 44 | def getTempos(paragem_duo): 45 | paragem = paragem_duo[0] 46 | np = paragem_duo[1] 47 | request = "http://" + link + "uid=" + uid + "&" + "np=" + np + "&" + "paragem=" + paragem + "&" + "submete=" + submete 48 | 49 | r = requests.get(request) 50 | 51 | response = r.content.decode().rstrip().split() 52 | response = ' '.join(response) 53 | 54 | passagens = response.split('"floatLeft Linha') 55 | autocarros = [] 56 | numero_passagens = int((len(passagens) - 4) / 3) 57 | for i in range (0, numero_passagens): 58 | linha = passagens[3*(i+2)-2].split('class="linha_')[1][:3] 59 | destino = passagens[3*(i+2)-1].split('"> ')[0].split("")[0][6:] 60 | tempo = passagens[3*(i+2)][3:15] 61 | if "a passar" in tempo: 62 | tempo = 0 63 | else: 64 | tempo = int(tempo.split("-")[1].split("min")[0][1:3]) 65 | 66 | linha_trio = [linha, destino, tempo] 67 | autocarros.append(linha_trio) 68 | 69 | # If response:
Nao ha autocarros previstos para a paragem indicada nos proximos 60 minutos.
70 | if '
Nao ha autocarros previstos para a paragem indicada nos proximos 60 minutos.
' in response: 71 | response = False 72 | 73 | return autocarros 74 | 75 | 76 | #print(getTempos(paragens[10])) 77 | -------------------------------------------------------------------------------- /capture.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # # CAPTURAR DADOS DOS STCP PARA PODER VER ONDE É QUE OS HORÁRIOS FALHAM 4 | import os, sys 5 | import importlib 6 | stcp = importlib.import_module("stcp") 7 | import time 8 | import sqlite3 9 | import datetime 10 | from multiprocessing import Process 11 | 12 | def printf(string, file): 13 | f = open("log.txt", "a") 14 | f.write(string) 15 | 16 | def create_connection(db_file): 17 | """ create a database connection to the SQLite database 18 | specified by the db_file 19 | :param db_file: database file 20 | :return: Connection object or None 21 | """ 22 | try: 23 | conn = sqlite3.connect(db_file, isolation_level=None) 24 | return conn 25 | except: 26 | print("ERROR CONNECTING TO DATABASE") 27 | 28 | return None 29 | 30 | def insert_into_db(conn, linha, paragem, time): 31 | c = conn.cursor() 32 | c.execute(""" 33 | INSERT INTO bus VALUES(NULL, '{0}', '{1}', '{2}'); 34 | """.format(linha, paragem, time)) 35 | conn.commit() 36 | # Shift right to display in MB 37 | def size(file_location): 38 | return os.path.getsize(file_location) >> 20 39 | 40 | conn = create_connection("capture.db") 41 | size_of_db = size("./capture.db") 42 | 43 | ### 44 | # TEMPO DE EXECUÇÃO DE UM CICLO: 4m:10s 45 | ### 46 | linhas = stcp.getLinhas() 47 | paragens = [] 48 | for linha in linhas: 49 | for ldir in range(0, 1): 50 | stops = stcp.getParagens(linha, ldir) 51 | #print(linha) 52 | for stop in stops: 53 | if stop not in paragens: 54 | paragens.append(stop) 55 | 56 | def autocarro_passou(stops1): 57 | tempos = [] 58 | tempos_old = [] 59 | while (size_of_db < 500): 60 | i = 0 61 | for stop in stops1: 62 | tempo = None 63 | while tempo is None: 64 | try: 65 | tempo = stcp.getTempos(stop) 66 | except: 67 | pass 68 | 69 | tempos.append(tempo) 70 | #print(tempo) 71 | #print(stop) 72 | if tempos_old != []: 73 | tempo_old = tempos_old[i] 74 | linhas = [] 75 | for j in range(0, len(tempo)): 76 | linhas.append(tempo[j][0]) 77 | for j in range(0, len(tempo_old)): 78 | flag = 1 79 | if tempo_old[j][2] >= 7: 80 | continue 81 | for k in range(0, len(tempo)): 82 | if tempo_old[j][0] == tempo[k][0] and tempo_old[j][2] >= tempo[k][2]-2: 83 | flag = 0 84 | if flag != 0: 85 | insert_into_db(conn, tempo_old[j][0], stop[0], str(datetime.datetime.now().replace(microsecond=0))) 86 | 87 | i = i + 1 88 | tempos_old = tempos 89 | tempos = [] 90 | 91 | if __name__ == '__main__': 92 | # Não usar multiprocessamento, porque é impossível todos os processos escreverem para a db 93 | #array1 = paragens[0:int(len(paragens)/4)] 94 | #array2 = paragens[int(len(paragens)/4):int(len(paragens)/2)] 95 | #array3 = paragens[int(len(paragens)/2):int(len(paragens)/4+len(paragens)/2)] 96 | #array4 = paragens[int(len(paragens)/4+len(paragens)/2):len(paragens)] 97 | #p1 = Process(target=autocarro_passou, args=(array1, )) 98 | #p2 = Process(target=autocarro_passou, args=(array2,)) 99 | #p3 = Process(target=autocarro_passou, args=(array3,)) 100 | #p4 = Process(target=autocarro_passou, args=(array4,)) 101 | #p1.start() 102 | #p2.start() 103 | #p3.start() 104 | #p4.start() 105 | #p1.join() 106 | #p2.join() 107 | #p3.join() 108 | #p4.join() 109 | autocarro_passou(paragens) 110 | --------------------------------------------------------------------------------