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