-
Notifications
You must be signed in to change notification settings - Fork 1
/
SFS.py
executable file
·3163 lines (2782 loc) · 144 KB
/
SFS.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-
"""Hanterar (konsoliderade) författningar i SFS från Regeringskansliet
rättsdatabaser.
"""
from __future__ import with_statement
# system libraries
from pprint import pprint
from tempfile import mktemp
from time import time,sleep
import codecs
import difflib
import htmlentitydefs
import HTMLParser
import logging
import os
import re
import sys
import unicodedata
import shutil
from datetime import date, datetime
import cgi
# python 2.5 required
from collections import defaultdict, OrderedDict
import xml.etree.cElementTree as ET
import xml.etree.ElementTree as PET
# 3rdparty libs
from configobj import ConfigObj
from mechanize import Browser, LinkNotFoundError, urlopen
from BeautifulSoup import BeautifulSoup
try:
from rdflib.Graph import Graph
except ImportError:
from rdflib import Graph
from rdflib import Literal, Namespace, URIRef, RDF, RDFS
# my own libraries
import LegalSource
from LegalRef import LegalRef, ParseError, Link, LinkSubject
import LegalURI
import Util
from DispatchMixin import DispatchMixin
from TextReader import TextReader
import FilebasedTester
from DataObjects import UnicodeStructure, CompoundStructure, \
MapStructure, TemporalStructure, OrdinalStructure, \
PredicateType, DateStructure, serialize, deserialize
__version__ = (1,6)
__author__ = u"Staffan Malmgren <[email protected]>"
__shortdesc__ = u"Författningar i SFS"
__moduledir__ = "sfs"
log = logging.getLogger(__moduledir__)
if not os.path.sep in __file__:
__scriptdir__ = os.getcwd()
else:
__scriptdir__ = os.path.dirname(__file__)
# Objektmodellen för en författning är uppbyggd av massa byggstenar
# (kapitel, paragrafen, stycken m.m.) där de allra flesta är någon
# form av lista. Även stycken är listor, dels då de kan innehålla
# lagrumshänvisningar i den löpande texten, som uttrycks som
# Link-objekt mellan de vanliga unicodetextobjekten, dels då de kan
# innehålla en punkt- eller nummerlista.
#
# Alla klasser ärver från antingen CompoundStructure (som är en list
# med lite extraegenskaper), UnicodeStructure (som är en unicode med
# lite extraegenskaper) eller MapStructure (som är ett dict med lite
# extraegenskaper).
#
# De kan även ärva från TemporalStructure om det är ett objekt som kan
# upphävas eller träda ikraft (exv paragrafer och rubriker, men inte
# enskilda stycken) och/eller OrdinalStructure om det är ett objekt
# som har nån sorts löpnummer, dvs kan sorteras på ett meningsfullt
# sätt (exv kapitel och paragrafer, men inte rubriker).
class Forfattning(CompoundStructure, TemporalStructure):
"""Grundklass för en konsoliderad författningstext. Metadatan
(SFS-numret, ansvarigt departement, 'uppdaterat t.o.m.' m.fl. fält
lagras inte här, utan i en separat Forfattningsinfo-instans"""
pass
# Rubrike är en av de få byggstenarna som faktiskt inte kan innehålla
# något annat (det förekommer "aldrig" en hänvisning i en
# rubriktext). Den ärver alltså från UnicodeStructure, inte
# CompoundStructure.
class Rubrik(UnicodeStructure,TemporalStructure):
"""En rubrik av något slag - kan vara en huvud- eller underrubrik
i löptexten, en kapitelrubrik, eller något annat"""
fragment_label = "R"
def __init__(self, *args, **kwargs):
self.id = kwargs['id'] if 'id' in kwargs else None
super(Rubrik,self).__init__(*args, **kwargs)
class Stycke(CompoundStructure):
fragment_label = "S"
def __init__(self, *args, **kwargs):
self.id = kwargs['id'] if 'id' in kwargs else None
super(Stycke,self).__init__(*args, **kwargs)
class NumreradLista (CompoundStructure): pass
class Strecksatslista (CompoundStructure): pass
class Bokstavslista (CompoundStructure): pass
class Preformatted(UnicodeStructure): pass
class Tabell(CompoundStructure): pass # Varje tabellrad är ett objekt
class Tabellrad(CompoundStructure, TemporalStructure): pass # Varje tabellcell är ett objekt
class Tabellcell(CompoundStructure): pass # ..som kan innehålla text och länkar
class Avdelning(CompoundStructure, OrdinalStructure):
fragment_label = "A"
def __init__(self, *args, **kwargs):
self.id = kwargs['id'] if 'id' in kwargs else None
super(Avdelning,self).__init__(*args, **kwargs)
class UpphavtKapitel(UnicodeStructure, OrdinalStructure):
"""Ett UpphavtKapitel är annorlunda från ett upphävt Kapitel på så
sätt att inget av den egentliga lagtexten finns kvar, bara en
platshållare"""
pass
class Kapitel(CompoundStructure, OrdinalStructure):
fragment_label = "K"
def __init__(self, *args, **kwargs):
self.id = kwargs['id'] if 'id' in kwargs else None
super(Kapitel,self).__init__(*args, **kwargs)
class UpphavdParagraf(UnicodeStructure, OrdinalStructure):
pass
# en paragraf har inget "eget" värde, bara ett nummer och ett eller flera stycken
class Paragraf(CompoundStructure, OrdinalStructure):
fragment_label = "P"
def __init__(self, *args, **kwargs):
self.id = kwargs['id'] if 'id' in kwargs else None
super(Paragraf,self).__init__(*args,**kwargs)
# kan innehålla nästlade numrerade listor
class Listelement(CompoundStructure, OrdinalStructure):
fragment_label = "N"
def __init__(self, *args, **kwargs):
self.id = kwargs['id'] if 'id' in kwargs else None
super(Listelement,self).__init__(*args,**kwargs)
class Overgangsbestammelser(CompoundStructure):
def __init__(self, *args, **kwargs):
self.rubrik = kwargs['rubrik'] if 'rubrik' in kwargs else u'Övergångsbestämmelser'
super(Overgangsbestammelser,self).__init__(*args,**kwargs)
class Overgangsbestammelse(CompoundStructure, OrdinalStructure):
fragment_label = "L"
def __init__(self, *args, **kwargs):
self.id = kwargs['id'] if 'id' in kwargs else None
super(Overgangsbestammelse,self).__init__(*args,**kwargs)
class Bilaga(CompoundStructure):
fragment_label = "B"
def __init__(self, *args, **kwargs):
self.id = kwargs['id'] if 'id' in kwargs else None
super(Bilaga,self).__init__(*args,**kwargs)
class Register(CompoundStructure):
"""Innehåller lite metadata om en grundförfattning och dess
efterföljande ändringsförfattningar"""
def __init__(self, *args, **kwargs):
self.rubrik = kwargs['rubrik'] if 'rubrik' in kwargs else None
super(Register,self).__init__(*args, **kwargs)
class Registerpost(MapStructure):
"""Metadata för en viss (ändrings)författning: SFS-nummer,
omfattning, förarbeten m.m . Vanligt förekommande nycklar och dess värden:
* 'SFS-nummer': en sträng, exv u'1970:488'
* 'Ansvarig myndighet': en sträng, exv u'Justitiedepartementet L3'
* 'Rubrik': en sträng, exv u'Lag (1978:488) om ändring i lagen (1960:729) om upphovsrätt till litterära och konstnärliga verk'
* 'Ikraft': en date, exv datetime.date(1996, 1, 1)
* 'Övergångsbestämmelse': True eller False
* 'Omfattning': en lista av nodeliknande saker i stil med
[u'ändr.',
LinkSubject('23',uri='http://rinfo.lagrummet.se/publ/sfs/1960:729#P23', pred='rinfo:andrar'),
u', ',
LinkSubject('24',uri='http://rinfo.lagrummet.se/publ/sfs/1960:729#P24', pred='rinfo:andrar'),
u' §§; ny ',
LinkSubject('24 a',uri='http://rinfo.lagrummet.se/publ/sfs/1960:729#P24a' pred='rinfo:inforsI'),
' §']
* 'Förarbeten': en lista av nodeliknande saker i stil med
[Link('Prop. 1981/82:152',uri='http://rinfo.lagrummet.se/publ/prop/1981/82:152'),
u', ',
Link('KrU 1977/78:27',uri='http://rinfo.lagrummet.se/extern/bet/KrU/1977/78:27')]
* 'CELEX-nr': en node, exv:
Link('393L0098',uri='http://rinfo.lagrummet.se/extern/celex/393L0098')
"""
pass
class Forfattningsinfo(MapStructure):
pass
class UnicodeSubject(PredicateType,UnicodeStructure): pass
class DateSubject(PredicateType,DateStructure): pass
# module global utility functions
def SFSnrToFilename(sfsnr):
"""converts a SFS id to a filename, sans suffix, eg: '1909:bih. 29
s.1' => '1909/bih._29_s.1'. Returns None if passed an invalid SFS
id."""
if sfsnr.find(":") < 0: return None
return re.sub(r'([A-Z]*)(\d{4}):',r'\2/\1',sfsnr.replace(' ', '_'))
def FilenameToSFSnr(filename):
"""converts a filename, sans suffix, to a sfsnr, eg:
'1909/bih._29_s.1' => '1909:bih. 29 s.1'"""
(dir,file)=filename.split("/")
if file.startswith('RFS'):
return re.sub(r'(\d{4})/([A-Z]*)(\d*)( [AB]|)(-(\d+-\d+|first-version)|)',r'\2\1:\3', filename.replace('_',' '))
else:
return re.sub(r'(\d{4})/(\d*( s[\. ]\d+|))( [AB]|)(-(\d+-\d+|first-version)|)',r'\1:\2', filename.replace('_',' '))
class SFSDownloader(LegalSource.Downloader):
def __init__(self,config):
super(SFSDownloader,self).__init__(config) # sets config, logging, initializes browser
def DownloadAll(self):
log.info(u'Downloading everything')
# add &upph=false to filter out expired things
self.browser.open("http://rkrattsbaser.gov.se/sfsr/adv?sort=asc")
pagecnt = 1
done = False
while not done:
log.info(u'Resultatsida nr #%s' % pagecnt)
soup = BeautifulSoup(self.browser.response().read())
for hit in soup.findAll("div", "search-hit-info-num"):
sfsnr = hit.text.split("SFS-nummer: ", 1)[1].strip()
if not sfsnr.startswith("N"): # Icke-SFS-författningar
# som ändå finns i
# databasen
try:
self._downloadSingle(sfsnr)
except IOError as e:
log.error("Failed to download %s: %s" % (sfsnr, e))
# self.browser.back()
try:
self.browser.find_link(text=u'Nästa')
self.browser.follow_link(text=u'Nästa')
pagecnt += 1
except LinkNotFoundError:
log.info(u'Ingen nästa sida-länk, vi är nog klara')
done = True
self._setLastSFSnr()
def _get_module_dir(self):
return __moduledir__
def _setLastSFSnr(self,last_sfsnr=None):
maxyear = datetime.today().year+1
if not last_sfsnr:
log.info(u'Letar efter senaste SFS-nr i %s/sfst"' % self.download_dir)
last_sfsnr = "1600:1"
for f in Util.listDirs(u"%s/sfst" % self.download_dir, ".html"):
if "RFS" in f or "checksum" in f or "-" in f:
continue
tmp = self._findUppdateradTOM(FilenameToSFSnr(f[len(self.download_dir)+6:-5].replace("\\", "/")), f)
tmpyear = int(tmp.split(":")[0])
if tmpyear > maxyear:
log.warning('%s is probably not correct, ignoring (%s)' % (tmp,f))
continue
if Util.numcmp(tmp, last_sfsnr) > 0:
log.info(u'%s > %s (%s)' % (tmp, last_sfsnr, f))
last_sfsnr = tmp
self.config[__moduledir__]['next_sfsnr'] = last_sfsnr
self.config.write()
def DownloadNew(self):
if not 'next_sfsnr' in self.config[__moduledir__]:
self._setLastSFSnr()
(year,nr) = [int(x) for x in self.config[__moduledir__]['next_sfsnr'].split(":")]
done = False
real_last_sfs_nr = False
while not done:
wanted_sfs_nr = '%s:%s' % (year,nr)
log.info(u'Söker efter SFS nr %s' % wanted_sfs_nr)
base_sfsnr_list = self._checkForSFS(year,nr)
if base_sfsnr_list:
self.download_log.info("%s:%s [%s]" % (year,nr,", ".join(base_sfsnr_list)))
for base_sfsnr in base_sfsnr_list: # usually only a 1-elem list
uppdaterad_tom = self._downloadSingle(base_sfsnr)
print("uppdaterad_tom %s wanted_sfs_nr %s" % (uppdaterad_tom, wanted_sfs_nr))
if base_sfsnr_list[0] == wanted_sfs_nr:
# initial grundförfattning - varken
# "Uppdaterad T.O.M. eller "Upphävd av" ska
# vara satt
pass
elif Util.numcmp(uppdaterad_tom, wanted_sfs_nr) < 0:
log.warning(u" Texten uppdaterad t.o.m. %s, inte %s" % (uppdaterad_tom, wanted_sfs_nr))
if not real_last_sfs_nr:
real_last_sfs_nr = wanted_sfs_nr
nr = nr + 1
else:
log.info('tjuvkikar efter SFS nr %s:%s' % (year,nr+1))
base_sfsnr_list = self._checkForSFS(year,nr+1)
if base_sfsnr_list:
if not real_last_sfs_nr:
real_last_sfs_nr = wanted_sfs_nr
nr = nr + 1 # actual downloading next loop
elif datetime.today().year > year:
log.info(u' Är det dags att byta år?')
base_sfsnr_list = self._checkForSFS(datetime.today().year, 1)
if base_sfsnr_list:
year = datetime.today().year
nr = 1 # actual downloading next loop
else:
log.info(u' Vi är klara')
done = True
else:
log.info(u' Vi är klara')
done = True
if real_last_sfs_nr:
self._setLastSFSnr(real_last_sfs_nr)
else:
self._setLastSFSnr("%s:%s" % (year,nr))
def _checkForSFS(self,year,nr):
"""Givet ett SFS-nummer, returnera en lista med alla
SFS-numret för dess grundförfattningar. Normalt sett har en
ändringsförfattning bara en grundförfattning, men för vissa
(exv 2008:605) finns flera. Om SFS-numret inte finns alls,
returnera en tom lista."""
# Titta först efter grundförfattning
log.info(u' Letar efter grundförfattning')
grundforf = []
url = "http://rkrattsbaser.gov.se/sfsr/adv?bet=%s:%s" % (year, nr)
# FIXME: consider using mechanize
tmpfile = mktemp()
self.browser.retrieve(url,tmpfile)
t = TextReader(tmpfile,encoding="utf-8")
try:
# t.cue(u"<p>Sökningen gav ingen träff!</p>")
t.cue(u"<div>Inga tr\xe4ffar</div>")
except IOError: # hurra!
grundforf.append(u"%s:%s" % (year,nr))
return grundforf
# Sen efter ändringsförfattning
log.info(u' Letar efter ändringsförfattning')
url = "http://rkrattsbaser.gov.se/sfsr/adv?%%C3%%A4bet=%s:%s" % (year, nr)
self.browser.retrieve(url, tmpfile)
# maybe this is better done through mechanize?
t = TextReader(tmpfile,encoding="utf-8")
try:
t.cue(u"<div>Inga tr\xe4ffar</div>")
log.info(u' Hittade ingen ändringsförfattning')
return grundforf
except IOError:
t.seek(0)
try:
t.cuepast(u'<a href="/sfst?bet=')
grundforf.append(t.readto(u'"'))
log.debug(u' Hittade ändringsförfattning (till %s)' % grundforf[-1])
return grundforf
except IOError:
# FIXME: when is this used?
t.seek(0)
page = t.read(sys.maxint)
for m in re.finditer('>(\d+:\d+)</a>',page):
grundforf.append(m.group(1))
log.debug(u' Hittade ändringsförfattning (till %s)' % grundforf[-1])
return grundforf
def _downloadSingle(self, sfsnr):
"""Laddar ner senaste konsoliderade versionen av
grundförfattningen med angivet SFS-nr. Om en tidigare version
finns på disk, arkiveras den. Returnerar det SFS-nummer till
vilket författningen uppdaterats."""
sfsnr = sfsnr.replace("/", ":")
log.info(u' Laddar ner %s' % sfsnr)
# enc_sfsnr = sfsnr.replace(" ", "+")
# Div specialhack för knepiga författningar
if sfsnr == "1723:1016+1": parts = ["1723:1016"]
# elif sfsnr == "1942:740": parts = ["1942:740 A", "1942:740 B"]
else: parts = [sfsnr]
upphavd_genom = uppdaterad_tom = old_uppdaterad_tom = None
for part in parts:
sfst_url = "http://rkrattsbaser.gov.se/sfst?bet=%s" % part.replace(" ","%20")
sfst_file = "%s/sfst/%s.html" % (self.download_dir, SFSnrToFilename(part))
sfst_tempfile = mktemp()
self.browser.retrieve(sfst_url, sfst_tempfile)
# here, we need to check if tempfile is a real text, or if
# it's a search result page like
# http://rkrattsbaser.gov.se/sfst?bet=1926:1. If it's the
# latter, we need to find the right value in the list,
# make note of the post_id parameter in the url, retrieve
# it and keep it around for
if os.path.exists(sfst_file):
old_checksum = self._checksum(sfst_file)
new_checksum = self._checksum(sfst_tempfile)
upphavd_genom = self._findUpphavtsGenom(sfst_tempfile)
uppdaterad_tom = self._findUppdateradTOM(sfsnr, sfst_tempfile)
if (old_checksum != new_checksum):
old_uppdaterad_tom = self._findUppdateradTOM(sfsnr, sfst_file)
uppdaterad_tom = self._findUppdateradTOM(sfsnr, sfst_tempfile)
if uppdaterad_tom != old_uppdaterad_tom:
log.info(u' %s har ändrats (%s -> %s)' % (sfsnr,old_uppdaterad_tom,uppdaterad_tom))
self._archive(sfst_file, sfsnr, old_uppdaterad_tom)
else:
log.info(u' %s har ändrats (gammal checksum %s)' % (sfsnr,old_checksum))
self._archive(sfst_file, sfsnr, old_uppdaterad_tom,old_checksum)
# replace the current file, regardless of wheter
# we've updated it or not
Util.robustRename(sfst_tempfile, sfst_file)
elif upphavd_genom:
log.info(u' %s har upphävts' % (sfsnr))
else:
log.debug(u' %s har inte ändrats (gammal checksum %s)' % (sfsnr,old_checksum))
else:
Util.robustRename(sfst_tempfile, sfst_file)
sfsr_url = "http://rkrattsbaser.gov.se/sfsr?bet=%s" % sfsnr.replace(" ", "%20")
sfsr_file = "%s/sfsr/%s.html" % (self.download_dir, SFSnrToFilename(sfsnr))
if (old_uppdaterad_tom and
old_uppdaterad_tom != uppdaterad_tom):
self._archive(sfsr_file, sfsnr, old_uppdaterad_tom)
Util.ensureDir(sfsr_file)
sfsr_tempfile = mktemp()
self.browser.retrieve(sfsr_url, sfsr_tempfile)
Util.replace_if_different(sfsr_tempfile,sfsr_file)
if upphavd_genom:
log.info(u' %s är upphävd genom %s' % (sfsnr, upphavd_genom))
return upphavd_genom
elif uppdaterad_tom:
log.info(u' %s är uppdaterad tom %s' % (sfsnr, uppdaterad_tom))
return uppdaterad_tom
else:
log.info(u' %s är varken uppdaterad eller upphävd' % (sfsnr))
return None
def _archive(self, filename, sfsnr, uppdaterad_tom, checksum=None):
"""Arkivera undan filen filename, som ska vara en
grundförfattning med angivet sfsnr och vara uppdaterad
t.o.m. det angivna sfsnumret"""
archive_filename = "%s/sfst/%s-%s.html" % (self.download_dir, SFSnrToFilename(sfsnr),
SFSnrToFilename(uppdaterad_tom).replace("/","-"))
if checksum:
archive_filename = archive_filename.replace(".html", "-checksum-%s.html"%checksum)
log.info(u' Arkiverar %s till %s' % (filename, archive_filename))
if not os.path.exists(archive_filename):
os.rename(filename,archive_filename)
def _findUppdateradTOM(self, sfsnr, filename):
# the first few bytes tell whether this is a legacy HTML file
# downloaded from the old TRIPS system or a new one.
with open(filename) as fp:
magic = fp.read(46)
try:
if magic == '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">':
# old one
reader = TextReader(filename,encoding='iso-8859-1')
reader.cue("Ändring införd:<b> t.o.m. SFS")
else:
# new one
reader = TextReader(filename, encoding='utf-8')
reader.cue(u"\xc4ndring inf\xf6rd:</span> t.o.m. SFS")
# from here the logic is identical for new and old files
l = reader.readline()
m = re.search('(\d+:\s?\d+)',l)
if m:
return m.group(1)
else:
# if m is None, the SFS id is using a non-standard
# formatting (eg 1996/613-first-version) -- interpret
# it as if it didn't exist
return sfsnr
except IOError:
return sfsnr # the base SFS nr
def _findUpphavtsGenom(self, filename):
with open(filename) as fp:
magic = fp.read(46)
try:
if magic == '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">':
reader = TextReader(filename,encoding='iso-8859-1')
reader.cue("upphävts genom:<b> SFS")
l = reader.readline()
m = re.search('(\d+:\s?\d+)',l)
if m:
return m.group(1)
else:
return None
else:
# the new SFST files DO NOT CONTAIN THIS INFORMATION! The SFSR files sort of do.
return None
except IOError:
return None
def _checksum(self,filename):
"""MD5-checksumman för den angivna filen"""
import hashlib
c = hashlib.md5()
# fixme: Use SFSParser._extractSFST so that we only compare
# the plaintext part of the downloaded file
#f = open(filename)
#data = f.read()
#f.close()
#c.update(data)
#return c.hexdigest()
p = SFSParser()
plaintext = p._extractSFST([filename])
# for some insane reason, hashlib:s update method can't seem
# to handle ordinary unicode strings
c.update(plaintext.encode('iso-8859-1', errors='xmlcharrefreplace'))
return c.hexdigest()
class UpphavdForfattning(Exception):
"""Slängs när en upphävd författning parseas"""
pass
class IckeSFS(Exception):
"""Slängs när en författning som inte är en egentlig SFS-författning parseas"""
pass
DCT = Namespace(Util.ns['dct'])
XSD = Namespace(Util.ns['xsd'])
RINFO = Namespace(Util.ns['rinfo'])
RINFOEX = Namespace(Util.ns['rinfoex'])
class SFSParser(LegalSource.Parser):
re_SimpleSfsId = re.compile(r'(\d{4}:\d+)\s*$')
re_SearchSfsId = re.compile(r'\((\d{4}:\d+)\)').search
re_ChangeNote = re.compile(ur'(Lag|Förordning) \(\d{4}:\d+\)\.?$')
re_ChapterId = re.compile(r'^(\d+( \w|)) [Kk][Aa][Pp]\.').match
re_DivisionId = re.compile(r'^AVD. ([IVX]*)').match
re_SectionId = re.compile(r'^(\d+ ?\w?) §[ \.]') # used for both match+sub
re_SectionIdOld = re.compile(r'^§ (\d+ ?\w?).') # as used in eg 1810:0926
re_DottedNumber = re.compile(r'^(\d+ ?\w?)\. ')
re_Bullet = re.compile(ur'^(\-\-?|\x96) ')
re_NumberRightPara = re.compile(r'^(\d+)\) ').match
re_Bokstavslista = re.compile(r'^(\w)\) ')
re_ElementId = re.compile(r'^(\d+) mom\.') # used for both match+sub
re_ChapterRevoked = re.compile(r'^(\d+( \w|)) [Kk]ap. (upphävd|har upphävts) genom (förordning|lag) \([\d\:\. s]+\)\.?$').match
re_SectionRevoked = re.compile(r'^(\d+ ?\w?) §[ \.]([Hh]ar upphävts|[Nn]y beteckning (\d+ ?\w?) §) genom ([Ff]örordning|[Ll]ag) \([\d\:\. s]+\)\.$').match
re_RevokeDate = re.compile(ur'/(?:Rubriken u|U)pphör att gälla U:(\d+)-(\d+)-(\d+)/')
re_RevokeAuthorization = re.compile(ur'/Upphör att gälla U:(den dag regeringen bestämmer)/')
re_EntryIntoForceDate = re.compile(ur'/(?:Rubriken t|T)räder i kraft I:(\d+)-(\d+)-(\d+)/')
re_EntryIntoForceAuthorization = re.compile(ur'/Träder i kraft I:(den dag regeringen bestämmer)/')
re_dehyphenate = re.compile(r'\b- (?!(och|eller))',re.UNICODE).sub
re_definitions = re.compile(r'^I (lagen|förordningen|balken|denna lag|denna förordning|denna balk|denna paragraf|detta kapitel) (avses med|betyder|används följande)').match
re_brottsdef = re.compile(ur'\b(döms|dömes)(?: han)?(?:,[\w§ ]+,)? för ([\w ]{3,50}) till (böter|fängelse)', re.UNICODE).search
re_brottsdef_alt = re.compile(ur'[Ff]ör ([\w ]{3,50}) (döms|dömas) till (böter|fängelse)', re.UNICODE).search
re_parantesdef = re.compile(ur'\(([\w ]{3,50})\)\.', re.UNICODE).search
re_loptextdef = re.compile(ur'^Med ([\w ]{3,50}) (?:avses|förstås) i denna (förordning|lag|balk)', re.UNICODE).search
# use this custom matcher to ensure any strings you intend to convert
# are legal roman numerals (simpler than having from_roman throwing
# an exception)
re_roman_numeral_matcher = re.compile('^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$').match
swedish_ordinal_list = (u'första', u'andra', u'tredje', u'fjärde',
u'femte', u'sjätte', u'sjunde', u'åttonde',
u'nionde', u'tionde', u'elfte', u'tolfte')
swedish_ordinal_dict = dict(zip(swedish_ordinal_list, range(1,len(swedish_ordinal_list)+1)))
roman_numeral_map = (('M', 1000),
('CM', 900),
('D', 500),
('CD', 400),
('C', 100),
('XC', 90),
('L', 50),
('XL', 40),
('X', 10),
('IX', 9),
('V', 5),
('IV', 4),
('I', 1))
keep_expired = False
def __init__(self):
self.trace = {'rubrik': logging.getLogger('sfs.trace.rubrik'),
'paragraf': logging.getLogger('sfs.trace.paragraf'),
'numlist': logging.getLogger('sfs.trace.numlist'),
'tabell': logging.getLogger('sfs.trace.tabell')}
self.trace['rubrik'].debug(u'Rubriktracern är igång')
self.trace['paragraf'].debug(u'Paragraftracern är igång')
self.trace['numlist'].debug(u'Numlisttracern är igång')
self.trace['tabell'].debug(u'Tabelltracern är igång')
self.lagrum_parser = LegalRef(LegalRef.LAGRUM,
LegalRef.EGLAGSTIFTNING)
self.forarbete_parser = LegalRef(LegalRef.FORARBETEN)
self.current_section = u'0'
self.current_headline_level = 0 # 0 = unknown, 1 = normal, 2 = sub
LegalSource.Parser.__init__(self)
def Parse(self,basefile,files):
self.id = basefile
# find out when data was last fetched (use the oldest file)
timestamp = sys.maxint
for filelist in files.values():
for file in filelist:
if os.path.getmtime(file) < timestamp:
timestamp = os.path.getmtime(file)
registry = self._parseSFSR(files['sfsr'])
try:
plaintext = self._extractSFST(files['sfst'])
# FIXME: Maybe Parser classes should be directly told what the
# current basedir is, rather than having to do it the ugly way
# (c.f. RegPubParser.Parse, which does something similar to
# the below)
plaintextfile = files['sfst'][0].replace(".html", ".txt").replace("downloaded/sfst", "intermediate")
Util.ensureDir(plaintextfile)
tmpfile = mktemp()
f = codecs.open(tmpfile, "w",'iso-8859-1', errors="xmlcharrefreplace")
# f = codecs.open(tmpfile, "w",'utf-8', errors="xmlcharrefreplace")
f.write(plaintext+"\r\n")
f.close()
Util.replace_if_different(tmpfile,plaintextfile)
patchfile = 'patches/sfs/%s.patch' % basefile
descfile = 'patches/sfs/%s.desc' % basefile
patchdesc = None
if os.path.exists(patchfile):
# Prep the files to have unix lineendings
plaintextfile_u = mktemp()
shutil.copy2(plaintextfile,plaintextfile_u)
patchfile_u = mktemp()
shutil.copy2(patchfile,patchfile_u)
cmd = 'dos2unix %s' % plaintextfile_u
Util.runcmd(cmd)
cmd = 'dos2unix %s' % patchfile_u
Util.runcmd(cmd)
patchedfile = mktemp()
# we don't want to sweep the fact that we're patching under the carpet
log.warning(u'%s: Applying patch %s' % (basefile, patchfile))
cmd = 'patch -s %s %s -o %s' % (plaintextfile_u, patchfile_u, patchedfile)
log.debug(u'%s: running %s' % (basefile,cmd))
(ret, stdout, stderr) = Util.runcmd(cmd)
if ret == 0: # successful patch
# patch from cygwin always seem to produce unix lineendings
assert os.path.exists(descfile), "No description of patch %s found" % patchfile
patchdesc = codecs.open(descfile,encoding='utf-8').read().strip()
cmd = 'unix2dos %s' % patchedfile
log.debug(u'%s: running %s' % (basefile,cmd))
(ret, stdout, stderr) = Util.runcmd(cmd)
if ret == 0:
plaintextfile = patchedfile
else:
log.warning(u"%s: Failed lineending conversion: %s" % (basefile,stderr))
else:
log.warning(u"%s: Could not apply patch %s: %s" % (basefile, patchfile, stdout.strip()))
(meta, body) = self._parseSFST(plaintextfile, registry, patchdesc)
except IOError:
log.warning("%s: Fulltext saknas" % self.id)
# extractSFST misslyckades, då det fanns någon post i
# SFST-databasen (det händer alltför ofta att bara
# SFSR-databasen är uppdaterad). Fejka ihop en meta
# (Forfattningsinfo) och en body (Forfattning) utifrån
# SFSR-datat
meta = Forfattningsinfo()
meta['Rubrik'] = registry.rubrik
meta[u'Utgivare'] = LinkSubject(u'Regeringskansliet',
uri=self.find_authority_rec("Regeringskansliet"),
predicate=self.labels[u'Utgivare'])
# dateval = "1970-01-01"
# meta[u'Utfärdad'] = DateSubject(datetime.strptime(dateval, '%Y-%m-%d'),
# predicate=self.labels[u'Utfärdad'])
fldmap = {u'SFS-nummer' :u'SFS nr',
u'Ansvarig myndighet':u'Departement/ myndighet'}
for k,v in registry[0].items():
if k in fldmap:
meta[fldmap[k]] = v
docuri = self.lagrum_parser.parse(meta[u'SFS nr'])[0].uri
meta[u'xml:base'] = docuri
body = Forfattning()
kwargs = {'id':u'S1'}
s = Stycke([u'(Lagtext saknas)'], **kwargs)
body.append(s)
# Lägg till information om konsolideringsunderlag och
# förarbeten från SFSR-datat
meta[u'Konsolideringsunderlag'] = []
meta[u'Förarbeten'] = []
for rp in registry:
uri = self.lagrum_parser.parse(rp['SFS-nummer'])[0].uri
meta[u'Konsolideringsunderlag'].append(uri)
if u'Förarbeten' in rp:
for node in rp[u'Förarbeten']:
if isinstance(node,Link):
meta[u'Förarbeten'].append(node.uri)
# Plocka in lite extra metadata
meta[u'Senast hämtad'] = DateSubject(datetime.fromtimestamp(timestamp),
predicate="rinfoex:senastHamtad")
# hitta eventuella etablerade förkortningar
g = Graph()
if sys.platform == "win32":
g.load("file:///"+__scriptdir__+"/etc/sfs-extra.n3", format="n3")
else:
g.load(__scriptdir__+"/etc/sfs-extra.n3", format="n3")
for obj in g.objects(URIRef(meta[u'xml:base']), DCT['alternate']):
meta[u'Förkortning'] = unicode(obj)
# Plocka ut övergångsbestämmelserna och stoppa in varje
# övergångsbestämmelse på rätt plats i registerdatat.
obs = None
for p in body:
if isinstance(p, Overgangsbestammelser):
obs = p
break
if obs:
for ob in obs:
found = False
# Det skulle vara vackrare om Register-objektet hade
# nycklar eller index eller något, så vi kunde slippa
# att hitta rätt registerpost genom nedanstående
# iteration:
for rp in registry:
if rp[u'SFS-nummer'] == ob.sfsnr:
if u'Övergångsbestämmelse' in rp and rp[u'Övergångsbestämmelse'] != None:
log.warning(u'%s: Det finns flera Övergångsbestämmelse-objekt för SFS-nummer [%s] - endast det första behålls' % (self.id, ob.sfsnr))
else:
rp[u'Övergångsbestämmelse'] = ob
found = True
break
if not found:
log.warning(u'%s: Övergångsbestämmelse för [%s] saknar motsvarande registerpost' % (self.id, ob.sfsnr))
kwargs = {'id':u'L'+ob.sfsnr,
'uri':u'http://rinfo.lagrummet.se/publ/sfs/'+ob.sfsnr}
rp = Registerpost(**kwargs)
rp[u'SFS-nummer'] = ob.sfsnr
rp[u'Övergångsbestämmelse'] = ob
xhtml = self.generate_xhtml(meta,body,registry,__moduledir__,globals())
return xhtml
# metadatafält (kan förekomma i både SFST-header och SFSR-datat)
# som bara har ett enda värde
labels = {u'SFS-nummer': RINFO['fsNummer'],
u'SFS nr': RINFO['fsNummer'],
u'Ansvarig myndighet': DCT['creator'],
u'Departement': DCT['creator'],
u'Departement/ myndighet': DCT['creator'],
u'Utgivare': DCT['publisher'],
u'Rubrik': DCT['title'],
u'Utfärdad': RINFO['utfardandedatum'],
u'Ikraft': RINFO['ikrafttradandedatum'],
u'Observera': RDFS.comment, # FIXME: hitta bättre predikat
u'Övrigt': RDFS.comment, # FIXME: hitta bättre predikat
u'Tidsbegränsad': RINFOEX['tidsbegransad'],
u'Omtryck': RINFOEX['omtryck'], # subtype av RINFO['fsNummer']
u'Ändring införd': RINFO['konsolideringsunderlag'],
u'Författningen har upphävts genom':
RINFOEX['upphavdAv'], # ska vara owl:inverseOf
# rinfo:upphaver
u'Upphävd': RINFOEX['upphavandedatum']
}
# metadatafält som kan ha flera värden (kommer representeras som
# en lista av unicodeobjekt och LinkSubject-objekt)
multilabels = {u'Förarbeten': RINFO['forarbete'],
u'CELEX-nr': RINFO['forarbete'],
u'Omfattning': RINFO['andrar'], # också RINFO['ersatter'], RINFO['upphaver'], RINFO['inforsI']
}
def _parseSFSR(self,files):
"""Parsear ut det SFSR-registret som innehåller alla ändringar
i lagtexten från HTML-filer"""
all_attribs = []
r = Register()
for f in files:
soup = Util.loadSoup(f)
if soup.content and soup.content[0] == u'DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN"':
r.rubrik = Util.elementText(soup.body('table')[2]('tr')[1]('td')[0])
changes = []
for table in soup.body('table')[3:-2]:
d = OrderedDict()
for row in table('tr'):
key = Util.elementText(row('td')[0])
if key.endswith(":"): key= key[:-1] # trim ending ":"
if key == '': continue
val = Util.elementText(row('td')[1])
d[key] = val
changes.append[d]
else:
soup = Util.loadSoup(f, encoding="utf-8")
content = soup.find('div', 'search-results-content')
innerboxes = content.findAll('div', 'result-inner-box')
r.rubrik = innerboxes[1].text.strip()
d = OrderedDict()
d['SFS-nummer'] = innerboxes[0].text.split(u"\xb7")[1].strip()
for innerbox in innerboxes[2:]:
key, val = innerbox.text.split(":", 1)
d[key] = val
changes = [d]
for c in content.findAll('div', 'result-inner-sub-box-container'):
d = OrderedDict()
d[u'SFS-nummer'] = c.find('div', 'result-inner-sub-box-header').text.split("SFS ")[1]
for row in c.findAll('div', 'result-inner-sub-box'):
key, val = row.text.split(":", 1)
d[key] = val
changes.append(d)
for rowdict in changes:
kwargs = {'id': 'undefined',
'uri': u'http://rinfo.lagrummet.se/publ/sfs/undefined'}
p = Registerpost(**kwargs)
for key, val in rowdict.items():
val = Util.normalizeSpace(val.replace(u'\xa0',' ')) # no nbsp's, please
if val != "":
if key == u'SFS-nummer':
if val.startswith('N'):
raise IckeSFS()
if len(r) == 0:
firstnode = self.lagrum_parser.parse(val)[0]
if hasattr(firstnode,'uri'):
docuri = firstnode.uri
else:
log.warning(u'Kunde inte tolka [%s] som ett SFS-nummer' % val)
p[key] = UnicodeSubject(val,predicate=self.labels[key])
# FIXME: Eftersom det här sen går in i ett
# id-fält, id-värden måste vara NCNames,
# och NCNames inte får innehålla kolon
# måste vi hitta på någon annan
# delimiterare, typ bindestreck eller punkt
# http://www.w3.org/TR/REC-xml-names/#NT-NCName
# http://www.w3.org/TR/REC-xml/#NT-Name
#
# (börjar med 'L' eftersom NCNames måste
# börja med ett Letter)
p.id = u'L' + val
# p.uri = u'http://rinfo.lagrummet.se/publ/sfs/' + val
firstnode = self.lagrum_parser.parse(val)[0]
if hasattr(firstnode,'uri'):
p.uri = firstnode.uri
else:
log.warning(u'Kunde inte tolka [%s] som ett SFS-nummer' % val)
elif key == u'Ansvarig myndighet' or key == u'Departement':
try:
authrec = self.find_authority_rec(val)
p[key] = LinkSubject(val, uri=unicode(authrec[0]),
predicate=self.labels[key])
except Exception, e:
p[key] = val
elif key == u'Rubrik':
p[key] = UnicodeSubject(val,predicate=self.labels[key])
elif key == u'Observera':
if not self.keep_expired:
if u'Författningen är upphävd/skall upphävas: ' in val:
if datetime.strptime(val[41:51], '%Y-%m-%d') < datetime.today():
raise UpphavdForfattning()
p[key] = UnicodeSubject(val,predicate=self.labels[key])
elif key == u'Upphävd':
if datetime.strptime(val[:10], u'%Y-%m-%d') < datetime.today():
raise UpphavdForfattning()
# p[key] = DateSubject(datetime.strptime(val[:10], '%Y-%m-%d'), predicate=self.labels[key])
p[u'Observera'] = UnicodeSubject(u'Författningen är upphävd/skall upphävas: ' + val, predicate=self.labels[u'Observera'])
elif key == u'Ikraft':
try:
date = datetime.strptime(val[:10], '%Y-%m-%d')
p[key] = DateSubject(date, predicate=self.labels[key])
except ValueError as e: # eg. val is "den dag regeringen bestammer" or otherwise not a date
p[key] = val # or just ignore?
#if val.find(u'\xf6verg.best.') != -1):
# p[u'Har övergångsbestämmelse'] = UnicodeSubject(val,predicate
elif key == u'Omfattning':
p[key] = []
for changecat in val.split(u'; '):
if (changecat.startswith(u'ändr.') or
changecat.startswith(u'ändr ') or
changecat.startswith(u'ändring ')):
pred = RINFO['ersatter']
elif (changecat.startswith(u'upph.') or
changecat.startswith(u'utgår')):
pred = RINFO['upphaver']
elif (changecat.startswith(u'ny') or
changecat.startswith(u'ikrafttr.') or
changecat.startswith(u'ikrafftr.') or
changecat.startswith(u'ikraftr.') or
changecat.startswith(u'ikraftträd.') or
changecat.startswith(u'tillägg')):
pred = RINFO['inforsI']
elif (changecat.startswith(u'nuvarande') or
changecat == 'begr. giltighet' or
changecat == 'Omtryck' or
changecat == 'omtryck' or
changecat == 'forts.giltighet' or
changecat == 'forts. giltighet' or
changecat == 'forts. giltighet av vissa best.'):
# FIXME: Is there something smart
# we could do with these?
pred = None
else:
log.warning(u"%s: Okänd omfattningstyp ['%s']" % (self.id, changecat))
pred = None
# print self.lagrum_parser.parse(changecat,docuri,pred)
p[key].extend(self.lagrum_parser.parse(changecat,docuri,pred))
p[key].append(u';')
p[key] = p[key][:-1] # chop of trailing ';'
elif key == u'F\xf6rarbeten':
p[key] = self.forarbete_parser.parse(val,docuri,RINFO['forarbete'])
elif key == u'CELEX-nr':
p[key] = self.forarbete_parser.parse(val,docuri,RINFO['forarbete'])
elif key == u'Tidsbegränsad':
p[key] = DateSubject(datetime.strptime(val[:10], '%Y-%m-%d'), predicate=self.labels[key])
if p[key] < datetime.today():
if not self.keep_expired:
raise UpphavdForfattning()
else:
log.warning(u'%s: Obekant nyckel [\'%s\']' % (self.id, key))
if p:
r.append(p)
# else:
# print "discarding empty post"
return r
def _extractSFST(self, files = [], keepHead=True):
"""Plockar fram plaintextversionen av den konsoliderade
lagtexten från (idag) nedladdade HTML-filer"""
if not files:
return ""
with open(files[0]) as fp:
magic = fp.read(46)
if magic == '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">':
t = TextReader(files[0], encoding="iso-8859-1")
if keepHead:
t.cuepast(u'<pre>')
else:
t.cuepast(u'<hr>')
txt = t.readto(u'</pre>')
re_entities = re.compile("&(\w+?);")
txt = re_entities.sub(self._descapeEntity,txt)
if not '\r\n' in txt:
txt = txt.replace('\n','\r\n')
re_tags = re.compile("</?\w{1,3}>")
txt = re_tags.sub(u'',txt)
return txt + self._extractSFST(files[1:],keepHead=False)
else:
# files[0] might very well contain unneeded xml char refs
# (ie ö instead of o-umlaut). TextReader doesn't
# handle these, so we need to deescape them before passing
# them to TextReader
h = HTMLParser.HTMLParser()
with codecs.open(files[0], encoding="utf-8") as fp:
escaped = fp.read()
unescaped = h.unescape(escaped)
t = TextReader(ustring=unescaped)
if keepHead:
t.cuepast(u'<div class="search-results-content">')
# the tagsoup in header needs to be parsed and
# made to look similar to what the old files used
# for headers.
soup = BeautifulSoup(t.readto(u'<div class="result-box-text body-text">'))
divs = soup.findAll("div", "result-inner-box")
hdict = {'SFS nr': divs[0].text.split(u"\xb7")[1].strip(),
'Rubrik': divs[1].text.strip()}
for div in divs[2:]:
if ":" in div.text:
k, v = div.text.split(":", 1)
hdict[k.strip()] = v.strip()
header = u"\n"