Added error checking - previously a wrong password did not lead to an error.
[toast/webscraper/bob.git] / bob_download.py
1 #!/usr/bin/python3
2 """
3 usage: bob_download.py [-h] user_name password dest_dir
4
5 Downloads invoices from BOB.
6
7 positional arguments:
8   username    login user name is phone number (e.g. '4369911111111')
9   password    login password
10   destdir     directory where the invoices should be saved to
11
12 optional arguments:
13   -h, --help  show this help message and exit
14 """
15 import os
16 import re
17 import time
18 import argparse
19 import warnings
20 from urllib.parse import urljoin
21 import requests # pip install requests
22 from bs4 import BeautifulSoup # pip install beautifulsoup4
23 from requests.packages.urllib3.exceptions import SubjectAltNameWarning
24
25 # SubjectAltNameWarning's should go off 0 times per host
26 warnings.simplefilter('ignore', SubjectAltNameWarning)
27
28 def main(username, password, destdir, csv_format):
29     session = requests.Session()
30     session.headers.update({
31         'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', # otherwise site with content '<HTML></HTML>' is returned
32         })
33
34     # load login page
35     response = session.get('https://rechnung.bob.at/')
36     assert response.ok
37     html = BeautifulSoup(response.text, 'html.parser')
38
39     # fill out login form (name='asmpform') with username=<phone number> and password
40     form = html.find(attrs={'name': 'asmpform'})
41     fields = {e['name']: e.get('value', '') for e in form.find_all('input', {'name': True}) if e['name'] != 'submit'}
42     assert 'loginMsisdn' in fields # user name
43     fields['loginMsisdn'] = username # e.g. '4369911111111'
44     assert 'kkw' in fields # password
45     fields['kkw'] = password
46
47     # load overview page
48     response = session.post(form['action'], data=fields)
49     assert response.ok
50     assert 'invalid KKW response' not in response.text
51     html = BeautifulSoup(response.text, 'html.parser')
52     assert html.find('a', title="ausloggen") is not None
53
54     # reload overview page rechnung.bob.at - that makes the URLs in the page much prettier
55     # previously:
56     # https://rechnung.bob.at/bill/pdf/;BOBOBPSESSIONID=B7DB9938A3B9541E3D0EB6CD728F54C0.obpBobCustomer4Rechnungskopie_1509_523260091_1.pdf?bsn=61
57     # same after reload:
58     # '/bill/pdf/Rechnungskopie_1509_523260091_1.pdf?bsn=61'
59     response = session.get(response.url)
60     assert response.ok
61     html = BeautifulSoup(response.text, 'html.parser')
62
63     # Download PDFs
64     # Links look like '/bill/pdf/Rechnungskopie_1509_523260091_1.pdf?bsn=61'
65     regexp = re.compile(r'\/(Rechnungskopie_.*)\?')
66     links = html.findAll('a', href=regexp)
67     for link in links:
68         url = link['href']
69         filename = regexp.search(url).group(1)
70         assert filename.startswith('Rechnungskopie_')
71         filepath = os.path.join(destdir, filename)
72         if not os.path.exists(filepath):
73             response = session.get(urljoin(response.url, url))
74             assert response.ok
75             with open(filepath, 'wb') as file:
76                 file.write(response.content)
77
78     # Download CSVs
79     # Links look like '/bill.set.obp?bsn=61'
80     regexp = re.compile(r'\/bill.set.obp\?')
81     links = html.findAll('a', href=regexp)
82     for link in links:
83         url = link['href']
84         response = session.get(urljoin(response.url, url))
85         assert response.ok
86         assert 'OBP.utils.reloadAfterDelay("/bill.ctn.cdr.set.obp",5);' in response.text
87         time.sleep(5) # OBP.utils.reloadAfterDelay("/bill.ctn.cdr.set.obp",5);
88         response = session.get(urljoin(response.url, 'bill.ctn.cdr.set.obp'))
89         assert 'OBP.utils.reloadAfterDelay("/bill.ctn.cdr.set.obp",5);' not in response.text
90         html = BeautifulSoup(response.text, 'html.parser')
91         assert html.find('a', id='link_csv_download') is not None
92         response = session.get('https://rechnung.bob.at/download.table.obp?fmt={}&table=obp.calls.table'.format(csv_format))
93         assert response.ok
94         filename = response.headers['Content-Disposition'].split('=')[1] # e.g. 'EVN_1509_523260091_1_069911934859.txt'
95         assert filename.startswith('EVN_')
96         filepath = os.path.join(destdir, filename)
97         if not os.path.exists(filepath):
98             with open(filepath, 'wb') as file:
99                 file.write(response.content)
100
101
102 if __name__ == '__main__':
103     parser = argparse.ArgumentParser(description='Downloads invoices from BOB.')
104     parser.add_argument('--csv-format', choices=['CSV-DE', 'CSV', 'TAB'], default='TAB', help='Which CSV variant to use (CSV-DE is semicolon, CSV is comma and TAB is tab separated)')
105     parser.add_argument('username', help="login user name is phone number (e.g. '4369911111111')")
106     parser.add_argument('password', help="login password")
107     parser.add_argument('destdir', help="directory where the invoices should be saved to")
108     args = parser.parse_args()
109     main(args.username, args.password, args.destdir, args.csv_format)