replaced most overview parsing stuff with functions
[pywienerlinien] / parseHtml.py
1 from BeautifulSoup import BeautifulSoup, NavigableString
2 import urllib2
3 from datetime import time, datetime
4 from textwrap import wrap
5 import settings
6
7 class ParserError(Exception):
8      def __init__(self, value='', code=0):
9          self.value = value
10          self.code = code
11
12      def __str__(self):
13          return repr(self.value)
14
15 class Parser:
16     _overview = None
17     _details = None
18     STATE_ERROR = -1
19     STATE_START = 0
20     STATE_SEARCH = 1
21     STATE_RESULT = 2
22     _current_state = 0
23
24     def __init__(self, html):
25         self.soup = BeautifulSoup(html)
26
27     def __iter__(self):
28         for detail in self.details():
29             yield detail
30         raise IndexError()
31
32     def _parse_details(self):
33         if self._current_state < 0:
34             raise ParserError('Unable to parse details while in error state')
35
36         trips = map(lambda x: map(lambda x: {
37                                              # TODO kick out wrap
38                         'time': map(lambda x: (time(*map(int, x.split(':')))), wrap(x.find('td', {'class': 'col_time'}).text, 5)), # black magic appears
39                         'station': map(lambda x: x[2:].strip(),
40                                        filter(lambda x: type(x) == NavigableString, x.find('td', {'class': 'col_station'}).contents)), # filter non NaviStrings
41                         'info': map(lambda x: x.strip(),
42                                     filter(lambda x: type(x) == NavigableString, x.find('td', {'class': 'col_info'}).contents)),
43                     }, x.find('tbody').findAll('tr')),
44                     self.soup.findAll('div', {'class': 'data_table tourdetail'})) # all routes
45         return trips
46
47     @property
48     def details(self):
49         """returns list of trip details
50         [ [ { 'time': [datetime.time, datetime.time] if time else [],
51               'station': [u'start', u'end'] if station else [],
52               'info': [u'start station' if station else u'details for walking', u'end station' if station else u'walking duration']
53             }, ... # next trip step 
54           ], ... # next trip possibility
55         ]
56         """
57         if not self._details:
58             self._details = self._parse_details()
59
60         return self._details
61
62     def _parse_overview(self):
63         def get_tdtext(x, cl):
64             return x.find('td', {'class': cl}).text
65
66         def get_change(x):
67             y = get_tdtext(x, 'col_change')
68             if y:
69                 return int(y)
70             else:
71                 return 0
72
73         def get_price(x):
74             y = get_tdtext(x, 'col_price')
75             if y.find(','):
76                 return float(y.replace(',', '.'))
77             else:
78                 return 0.0
79
80         def get_date(x):
81             y = get_tdtext(x, 'col_date')
82             if y:
83                 return datetime.strptime(y, '%d.%m.%Y').date()
84             else:
85                 return None
86
87         # get overview table
88         table = self.soup.find('table', {'id': 'tbl_fahrten'})
89
90         # check if there is an overview table
91         if table and table.findAll('tr'):
92             # get rows
93             rows = table.findAll('tr')[1:] # cut off headline
94             overview = map(lambda x: {
95                                'date': get_date(x),
96                                'time': map(lambda x: time(*map(int, x.strip().split(':'))) if x else None, # extract times or set to None if empty
97                                            x.find('td', {'class': 'col_time'}).text.split('-')) if x.find('td', {'class': 'col_time'}) else [],
98                                'duration': time(*map(int, x.find('td', {'class': 'col_duration'}).text.split(':'))), # grab duration
99                                'change': get_change(x),
100                                'price': get_price(x),
101                            },
102                            rows)
103         else:
104             self._current_state = self.STATE_ERROR
105             raise ParserError('Unable to parse details while in error state')
106
107         return overview
108
109     @property
110     def overview(self):
111         """dict containing
112         date: datetime
113         time: [time, time]
114         duration: time
115         change: int
116         price: float
117         """
118         if not self._overview:
119             try:
120                 self._overview = self._parse_overview()
121             except AttributeError:
122                 f = open('DEBUG', 'w')
123                 f.write(str(self.soup))
124                 f.close()
125
126         return self._overview
127
128     def _check_request_state(self):
129         raise NotImplementedError()
130
131     @property
132     def request_state(self):
133         return self._current_state
134
135
136 class iParser:
137     _stations = {}
138     _lines = []
139
140     def __init__(self):
141         pass
142
143     def get_stations(self, letter):
144         if not self._stations.has_key(letter):
145             bs = BeautifulSoup(urllib2.urlopen(settings.stations % letter).read())
146             self._stations[letter] = map(lambda x: x['value'], bs.find('select', {'id': 'letter'}).findAll('option'))
147
148         return self._stations[letter]
149
150     def get_lines(self):
151         if not self._lines:
152             bs = BeautifulSoup(urllib2.urlopen(settings.line_overview).read())
153             # get tables
154             lines = bs.findAll('table', {'class': 'linie'})
155             # cut line parameter out of href
156             self._lines = map(lambda x: map(lambda x: x['href'][x['href'].find('=') + 1:], x.findAll('a')), lines)
157
158         return self._lines