update copyright years
[toast/webscraper/bob.git] / bob_download.py
1 #!/usr/bin/python3
2 # (c) 2015-2017 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=773A62BEC4AE1FBB917B3D82F69CE3A4.obpBobCustomer4Rechnung_1606_518139490_1.pdf?bsn=108
60     # same after reload:
61     # '/bill/pdf/Rechnung_1606_518139490_1.pdf?bsn=107'
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/Rechnung_1606_518139490_1.pdf?bsn=107'
68     regexp = re.compile(r'\/(Rechnung_.*)\?')
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('Rechnung_')
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.ctn.cdr.obp?bsn=116'
83     regexp = re.compile(r'\/bill.ctn.cdr.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         time.sleep(5)
94         response = session.get(response.url)
95         html = BeautifulSoup(response.text, 'html.parser')
96         assert html.find('a', id='link_csv_download') is not None
97         response = session.get('https://rechnung.bob.at/obp/download.obp?fmt={}&table=obp.calls.table'.format(csv_format))
98         assert response.ok
99         filename = response.headers['Content-Disposition'].split('=')[1] # e.g. 'EVN_1509_523260091_1_069911934859.txt'
100         assert filename.startswith('EVN_')
101         filepath = os.path.join(destdir, filename)
102         if not os.path.exists(filepath):
103             with open(filepath, 'wb') as file:
104                 file.write(response.content)
105
106
107 if __name__ == '__main__':
108     parser = argparse.ArgumentParser(description='Downloads invoices from BOB.')
109     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)')
110     parser.add_argument('username', help="login user name is phone number (e.g. '4369911111111')")
111     parser.add_argument('password', help="login password")
112     parser.add_argument('destdir', help="directory where the invoices should be saved to")
113     args = parser.parse_args()
114     main(args.username, args.password, args.destdir, args.csv_format)