├── .gitignore ├── .gitmodules ├── LICENSE ├── Readme.md ├── chai.py ├── indianrail.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | env 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "grequests"] 2 | path = grequests 3 | url = https://github.com/kennethreitz/grequests 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Chai 2 | Copyright (C) 2015 Vikrant Varma 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Chai 2 | 3 | A command line tool to help book tickets on the Indian Railways. 4 | 5 | ## Installation 6 | 7 | Use `virtualenv` to install packages locally rather than globally. 8 | 9 | ``` 10 | $ pip install -r requirements.txt 11 | $ git submodule init && git submodule update 12 | ``` 13 | 14 | ## Usage 15 | 16 | ``` 17 | chai.py [-h] [-v] -t TRAIN_NO -s SRC -d DST -D DAY -m MONTH 18 | [-c {1A,2A,3A,SL,CC}] [-q {GN,CK}] 19 | {avail,optimize} ... 20 | 21 | positional arguments: 22 | {avail,optimize} sub-command help 23 | avail find availability between two stations 24 | optimize calculate the best possible route to take between two 25 | stations 26 | 27 | optional arguments: 28 | -h, --help show this help message and exit 29 | -v, --verbose turn on verbose mode 30 | -t TRAIN_NO, --train_no TRAIN_NO 31 | train number 32 | -s SRC, --src SRC source station code 33 | -d DST, --dst DST destination station code 34 | -D DAY, --day DAY day of travel (dd) 35 | -m MONTH, --month MONTH 36 | month of travel (mm) 37 | -c {1A,2A,3A,SL,CC}, --class {1A,2A,3A,SL,CC} 38 | class of travel 39 | -q {GN,CK}, --quota {GN,CK} 40 | class code 41 | ``` 42 | 43 | ## Example 44 | 45 | ``` 46 | $ python chai.py -t 12802 -s NDLS -d KGP -D 30 -m 4 avail 47 | RAC3/RAC 3 48 | 49 | $ python chai.py -t 12802 -s NDLS -d KGP -D 30 -m 4 optimize 50 | Fetching stations on route... done. 51 | Using up to 100 concurrent connections. 52 | Fetching availability... 100% 53 | Optimum plan is: 54 | NDLS --> CNB ( 1 stations ) : AVAILABLE 33 55 | CNB --> KGP ( 19 stations ) : AVAILABLE 3 56 | ``` 57 | 58 | ## License 59 | 60 | [GPLv3](https://github.com/amrav/chai/blob/master/LICENSE) 61 | -------------------------------------------------------------------------------- /chai.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import re 5 | import indianrail as ir 6 | import sys 7 | import networkx as nx 8 | 9 | def main(): 10 | p = argparse.ArgumentParser(description="A command line tool to help book tickets on the Indian Railways.") 11 | p.add_argument("-v", "--verbose", help="turn on verbose mode", 12 | action='store_false', dest='verbose', default=False) 13 | sp = p.add_subparsers(help="sub-command help") 14 | 15 | p.add_argument("-t", "--train_no", help="train number", required=True, dest='train_no') 16 | p.add_argument("-s", "--src", help="source station code", required=True, dest='src') 17 | p.add_argument("-d", "--dst", help="destination station code", required=True, dest='dst') 18 | p.add_argument("-D", "--day", help="day of travel (dd)", required=True, dest='day') 19 | p.add_argument("-m", "--month", help="month of travel (mm)", required=True, dest='month') 20 | p.add_argument("-c", "--class", help="class of travel", 21 | choices=['1A', '2A', '3A', 'SL', 'CC'], default='3A', dest='class_') 22 | p.add_argument("-q", "--quota", help="class code", 23 | choices=['GN', 'CK'], default='GN', dest='quota') 24 | 25 | def _optimize(args): 26 | optimize(args.train_no, args.src, args.dst, args.day, args.month, args.class_, args.quota) 27 | 28 | def _get_avail(args): 29 | print ir.get_avail(args.train_no, args.src, args.dst, args.day, args.month, args.class_, args.quota) 30 | 31 | p_availability = sp.add_parser('avail', help="find availability between two stations") 32 | p_optimize = sp.add_parser('optimize', help="calculate the best possible route to take between two stations") 33 | 34 | p_optimize.set_defaults(func=_optimize) 35 | p_availability.set_defaults(func=_get_avail) 36 | 37 | args = p.parse_args() 38 | 39 | args.func(args) 40 | 41 | def cost_tuple(v1, v2, avail, indices): 42 | #if v1 is ahead of v2 43 | if (indices[v1] >= indices[v2]): 44 | return (0, 0, 10 * (indices[v1] - indices[v2])) 45 | elif re.match("AVAILABLE", avail[v1][v2]): 46 | return (0, 0, 1) 47 | elif re.match("RAC", avail[v1][v2]): 48 | return (0, 1, 0) 49 | wl = re.findall('/WL(\d+)', avail[v1][v2]) 50 | if len(wl) == 1: 51 | return (int(wl[0]), indices[v2] - indices[v1], 0) 52 | else: 53 | return (float("inf"), 0, 0) 54 | 55 | def numerical_cost(cost_tuple): 56 | LARGE_BASE = 100 57 | return (LARGE_BASE*LARGE_BASE) * cost_tuple[0] + LARGE_BASE * cost_tuple[1] + cost_tuple[2] 58 | 59 | def shortest_path(src, dst, names, cost): 60 | G = nx.DiGraph() 61 | G.add_nodes_from(names) 62 | 63 | indices = {} 64 | for i in range(len(names)): 65 | indices[names[i]] = i 66 | 67 | # Add edges from src to stations before it 68 | for i in range(indices[src]): 69 | G.add_edge(src, names[i], 70 | weight=cost(src, names[i])) 71 | # Add edges from stations after dst to dst 72 | for i in range(indices[dst] + 1, len(names)): 73 | G.add_edge(names[i], dst, weight=cost(names[i], dst)) 74 | # Add reverse edges from every station after src back 75 | for i in range(indices[src] + 1, len(names)): 76 | for j in range(indices[src] + 1, i): 77 | G.add_edge(names[i], names[j], weight=cost(names[i], names[j])) 78 | # Add edges from every station before dst to 79 | # every station after src 80 | for i in range(len(names)): 81 | for j in range(i + 1, len(names)): 82 | if i >= indices[dst] or j <= indices[src]: 83 | continue 84 | G.add_edge(names[i], names[j], 85 | weight=cost(names[i], names[j])) 86 | nx.write_multiline_adjlist(G, "test.adjlist") 87 | return nx.shortest_path(G, src, dst, weight='weight') 88 | 89 | def optimize(train_no, src, dst, day, month, class_, quota): 90 | sys.stdout.write("Fetching stations on route... ") 91 | sys.stdout.flush() 92 | stations = ir.get_stations(train_no) 93 | print "done." 94 | 95 | if (src not in stations['names'] or dst not in stations['names']): 96 | print "%s not in route of train %s. Aborting." \ 97 | %(src if src not in stations['names'] else dst, train_no) 98 | sys.exit(1) 99 | 100 | indices = {} 101 | for i in range(len(stations['names'])): 102 | indices[stations['names'][i]] = i 103 | 104 | avail = ir.get_all_avail(train_no, day, month, class_, quota, stations) 105 | 106 | def cost(src, dst): 107 | return numerical_cost(cost_tuple(src, dst, avail, indices)) 108 | 109 | print_plan(shortest_path(src, dst, stations['names'], cost), avail, indices) 110 | 111 | def print_plan(shortest_path, avail, indices): 112 | src = shortest_path[0] 113 | dst = shortest_path[-1] 114 | print "Best plan is: " 115 | for i in range(len(shortest_path) - 1): 116 | print shortest_path[i], " --> ", shortest_path[i + 1], 117 | print "(", indices[shortest_path[i + 1]] - indices[shortest_path[i]], " stations )", 118 | if (indices[shortest_path[i+1]] > indices[shortest_path[i]]): 119 | print ":", avail[shortest_path[i]][shortest_path[i+1]] 120 | else: 121 | if indices[shortest_path[i+1]] < indices[src]: 122 | print ":", "Get on at %s" %shortest_path[i] 123 | elif shortest_path[i + 1] == dst: 124 | print ":", "Get off at %s" %shortest_path[i+1] 125 | else: 126 | print ":", "Switch at %s" %shortest_path[i+1] 127 | 128 | if __name__ == '__main__': 129 | main() 130 | -------------------------------------------------------------------------------- /indianrail.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import re 3 | from bs4 import BeautifulSoup as bs 4 | import sys 5 | import copy 6 | import os 7 | import inspect 8 | 9 | # hack to include grequests from http://stackoverflow.com/a/6098238/1448759 10 | cmd_subfolder = os.path.realpath(os.path.abspath(os.path.join(os.path.split(inspect.getfile( inspect.currentframe() ))[0],"grequests"))) 11 | if cmd_subfolder not in sys.path: 12 | sys.path.insert(0, cmd_subfolder) 13 | import grequests 14 | 15 | session = grequests.Session() 16 | session.mount('http://', requests.adapters.HTTPAdapter(max_retries=10)) 17 | 18 | def scrape_avail(html): 19 | soup = bs(html) 20 | avail_str = '' 21 | avail_str = soup.select('tr.heading_table_top')[1].find_next_siblings('tr')[0].find_all('td')[2].text.strip() 22 | return avail_str 23 | 24 | def scrape_stations_list(html): 25 | soup = bs(html) 26 | stations = [] 27 | offsets = [] 28 | for row in soup.select("tr.heading_table_top")[1].find_next_siblings('tr'): 29 | stations.append(row.select('td')[1].text.strip()) 30 | offsets.append(int(row.select('td')[8].text.strip()) - 1) 31 | return {'names': stations, 'offsets': offsets} 32 | 33 | def is_avail(avail_str): 34 | return (re.match(r"AVAILABLE|RAC", avail_str) != None) 35 | 36 | 37 | AVAIL_URI = 'http://www.indianrail.gov.in/cgi_bin/inet_accavl_cgi.cgi' 38 | SCHEDULE_URI = 'http://www.indianrail.gov.in/cgi_bin/inet_trnnum_cgi.cgi' 39 | 40 | # IRCTC specific header and param names 41 | params = { 42 | 'lccp_trnno' : '12860', 43 | 'lccp_day' : '4', 44 | 'lccp_month': '2', 45 | 'lccp_srccode': 'BSP', 46 | 'lccp_dstncode': 'R', 47 | 'lccp_class1': '3A', # or SL or 2A or 1A 48 | 'lccp_quota': 'CK', # or GN 49 | 'submit': 'Please+Wait...', 50 | 'lccp_classopt': 'ZZ', 51 | 'lccp_class2': 'ZZ', 52 | 'lccp_class3': 'ZZ', 53 | 'lccp_class4': 'ZZ', 54 | 'lccp_class5': 'ZZ', 55 | 'lccp_class6': 'ZZ', 56 | 'lccp_class7': 'ZZ', 57 | 'lccp_trnname': '22811', 58 | 'getIt': 'Please+Wait...', 59 | } 60 | 61 | headers = { 62 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:26.0) Gecko/20100101 Firefox/26.0', 63 | 'Host': 'www.indianrail.gov.in', 64 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 65 | 'Accept-Language': 'en-US,en;q=0.5', 66 | 'Accept-Encoding': 'gzip, deflate', 67 | 'Referer': 'http://www.indianrail.gov.in/seat_Avail.html' 68 | } 69 | 70 | def get_avail(train_no, src, dst, day, month, class_, quota, offset = 0): 71 | 72 | day = int(day) 73 | month = int(month) 74 | 75 | (day, month) = correct_date(day, month, offset) 76 | 77 | params['lccp_trnno'] = train_no 78 | params['lccp_srccode'] = src 79 | params['lccp_dstncode'] = dst 80 | params['lccp_class1'] = class_ 81 | params['lccp_quota'] = quota 82 | params['lccp_day'] = day 83 | params['lccp_month'] = month 84 | headers['Referer'] = 'http://www.indianrail.gov.in/seat_Avail.html' 85 | headers['Content-Type'] = 'application/x-www-form-urlencoded1; charset=UTF-8;' 86 | r = requests.post(AVAIL_URI, data=params, headers=headers) 87 | try: 88 | return scrape_avail(r.text) 89 | except IndexError: 90 | print "Error: Couldn't get availability. Aborting." 91 | sys.exit(1) 92 | 93 | def correct_date(day, month, offset): 94 | days_in_month = 30 95 | if (month == 1 or 96 | month == 3 or 97 | month == 5 or 98 | month == 7 or 99 | month == 8 or 100 | month == 10 or 101 | month == 12): 102 | days_in_month = 31 103 | elif (month == 2): 104 | days_in_month = 28 105 | if (day + offset > days_in_month): 106 | month += 1 107 | if (month > 12): 108 | month -= 12 109 | day += offset 110 | if (day > days_in_month): 111 | day -= days_in_month 112 | return (day, month) 113 | 114 | def get_stations(train_no): 115 | params['lccp_trnname'] = train_no 116 | headers['Referer'] = 'http://www.indianrail.gov.in/train_Schedule.html' 117 | r = requests.post(SCHEDULE_URI, data=params, 118 | headers=headers) 119 | try: 120 | return scrape_stations_list(r.text) 121 | except IndexError: 122 | print "Error: Couldn't get stations list. Aborting." 123 | sys.exit(1) 124 | 125 | def print_progress(p, prompt='', text=''): 126 | sys.stdout.write("\r" + " " * (len(prompt) + len(text) + len(str(p))) + 127 | "\r%s%d%%%s" %(prompt, p, text)) 128 | sys.stdout.flush() 129 | 130 | def get_all_avail(train_no, day, month, class_, quota, stations=None, concurrency=100): 131 | if (stations == None): 132 | sys.stdout.write("Getting stations...") 133 | sys.stdout.flush() 134 | stations = irctc.get_stations(train_no) 135 | print " done." 136 | names = stations['names'] 137 | rs = [] 138 | 139 | # hack because Python has weak closures 140 | response_counter = [0] 141 | response_tot = (len(names) * (len(names) - 1)) / 2 142 | 143 | def on_response(day, month, src, dst, avail): 144 | def _on_response(response, response_counter=response_counter, *args, **kwargs): 145 | response_counter[0] += 1 146 | print_progress(response_counter[0] * 100 / response_tot, 147 | prompt="Fetching availability... ") 148 | if (src not in avail): 149 | avail[src] = {} 150 | try: 151 | avail[src][dst] = scrape_avail(response.text) 152 | except IndexError: 153 | print "\nWarning: Couldn't detect availability for %s/%s from %s to %s" %(day, month, src, dst) 154 | avail[src][dst] = "UNAVAILABLE" 155 | return _on_response 156 | 157 | failedRequests = [] 158 | def exception_handler(request, exception, failedRequests=failedRequests): 159 | failedRequests.append(request) 160 | 161 | avail = {} 162 | print "Using up to", concurrency, "concurrent connections." 163 | for i in range(len(names) - 1): 164 | for j in range(i + 1, len(names)): 165 | (c_day, c_month) = correct_date(int(day), int(month), stations['offsets'][i]) 166 | params['lccp_trnno'] = train_no 167 | params['lccp_srccode'] = names[i] 168 | params['lccp_dstncode'] = names[j] 169 | params['lccp_class1'] = class_ 170 | params['lccp_quota'] = quota 171 | params['lccp_day'] = c_day 172 | params['lccp_month'] = c_month 173 | headers['Referer'] = 'http://www.indianrail.gov.in/seat_Avail.html' 174 | headers['Content-Type'] = 'application/x-www-form-urlencoded1; charset=UTF-8;' 175 | rs.append( 176 | grequests.post( 177 | AVAIL_URI, 178 | data=copy.copy(params), 179 | headers=copy.copy(headers), 180 | hooks=dict(response=on_response(day=c_day, 181 | month=c_month, 182 | src=names[i], 183 | dst=names[j], 184 | avail=avail)), 185 | timeout=10 186 | )) 187 | grequests.map(rs, size=concurrency, exception_handler=exception_handler) 188 | 189 | while len(failedRequests) != 0: 190 | print "\nWarning: Retrying %d requests." % len(failedRequests) 191 | requests = copy.copy(failedRequests) 192 | del failedRequests[:] 193 | grequests.map(requests, size=concurrency, exception_handler=exception_handler) 194 | 195 | print 196 | return avail 197 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.3.2 2 | certifi==14.05.14 3 | decorator==3.4.0 4 | gevent==1.0.1 5 | greenlet==0.4.4 6 | networkx==1.9 7 | requests==2.4.1 8 | wsgiref==0.1.2 9 | --------------------------------------------------------------------------------