Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,18 @@ La première étape est de télécharger les archives LEGI depuis

La deuxième étape est la conversion des archives en base SQLite :

python -m legi.tar2sqlite legi.sqlite ./tarballs
python -m legi.tar2sqlite legi.sqlite ./tarballs [--raw]

Cette opération peut prendre de quelques minutes à plusieurs heures selon votre
machine et le nombre d'archives. Les deux caractéristiques importantes de votre
machine sont: le disque dur (un SSD est beaucoup plus rapide), et le processeur
(notamment sa fréquence, le nombre de cœurs importe peu car le travail n'est pas
parallèle).

La taille du fichier SQLite créé est environ 3,3Go (en février 2017).
La taille du fichier SQLite créé est environ 3,7Go (en décembre 2018).

L'option `--raw` désactive le nettoyage des données, ajoutez-la si vous avez
besoin des données LEGI brutes.

`tar2sqlite` permet aussi de maintenir votre base de données à jour, il saute
automatiquement les archives qu'il a déjà traité. En général la DILA publie une
Expand All @@ -63,10 +66,10 @@ exemple avec [cron][cron] :

## Fonctionnalités

### Normalisation des titres
### Normalisation des titres et numéros

Le module `normalize` corrige les titres de textes qui ne sont pas parfaitement
"standards". Les données originales sont sauvegardées dans une table dédiée.
Le module `normalize` corrige les titres de textes et les numéros d'articles qui
ne sont pas parfaitement « standards ».

### Factorisation des textes

Expand All @@ -82,7 +85,7 @@ Le module `html` permet de nettoyer les contenus des textes. Il supprime :
- les éléments inutiles, par exemple un `<span>` sans attributs
- les éléments vides, sauf `<td>` et `<th>`

En février 2018 il détecte 78 millions de caractères inutiles dans LEGI.
En décembre 2018 il détecte 85 millions de caractères inutiles dans LEGI.

Cette fonctionnalité n'est pas activée par défaut car elle est « destructrice »
et récente. Vous pouvez nettoyer tout l'HTML d'une base en exécutant la commande
Expand Down
152 changes: 152 additions & 0 deletions legi/articles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# encoding: utf8

"""
Parsing of article numbers and titles.
"""

from __future__ import division, print_function, unicode_literals

from argparse import ArgumentParser
import re

from .roman import ROMAN_PATTERN as roman_num
from .utils import connect_db, show_match


article_num_extra = (
r"(?:un|duo|ter|quater|quin|sex|sept|octo|novo)?(?:dec|vic|tric|quadrag|quinquag|sexag|septuag|octog|nonag)i[eè]s|"
r"semel|bis|ter|quar?ter|(?:quinqu(?:edec)?|sext?|sept|oct|non)i[eè]s|quinto"
) # "quinquedecies" et "sexties" ne sont pas corrects mais existent dans LEGI

article_num = (
r"(?:"
r"(?:LO\.? ?|[RD]\*{1,2}|[LDRA]\.? ?)?(?:[0-9]+|1er)|"
r"[0-9]+[a-z]?|"
r"\b[A-Z][0-9]*'?|"
r"\b%(roman_num)s\(?[a-e]?\)?|"
r"\b(?!AOC|ART\.)[A-Z]{1,4}" # e.g. LEGIARTI000025170742, LEGIARTI000029180938
r")(?:"
r"[-./*](?: ?[0-9]+[A-Z]?| ?[A-Z]{1,4}[0-9]*| ?[a-z])|"
r" - [0-9]+|"
r" (?!ART\.)[A-Z0-9]{1,3}|"
r" [a-z][A-Z]?(?![\w'])|"
r" ?\((?:%(roman_num)s|[A-Za-z0-9]{1,2})\)|"
r" 1er|"
r"[- ](?:%(article_num_extra)s)(?:-[0-9]+)?"
r")*(?!\w)"
) % globals()

article_type = (
r"[Aa]dditif |[Aa]nnexes?(?: [:-]|,)? (?:(?:à l')?article |art\. |unique )?|"
r"[Aa]ppendices? |[Bb]arèmes? |[Dd]otations? |[Dd][ée]cision |[Ll]istes? |"
r"[Rr]ègle |[Tt]able(?:aux?)? |[Éé]tats? |[Ii]nstruction "
)

article_subtype = (
r"doc|table(?:au)?|option|état|liste|appendice|"
r"(?:à|de) l'art(?:\.|icle)|"
r"(?:au %(roman_num)s )?art(?:\.|icle)?|"
r"(?:[Ss]ous-)?[Pp]art(?:ie)?"
) % dict(roman_num=roman_num)

article_titre = (
r"(?:(?:%(article_type)s)(?:technique )?)?"
r"(?:[nN]° ?)?"
r"\(?(?:unique|liminaire|(?:suite |-)?%(article_num)s|suite)\)?"
r"(?: (?:aux articles|art) %(article_num)s(?:(?:,|,? et| à) %(article_num)s)+)?"
r"(?: (?:[Ss]ous-)?[Pp]arties %(article_num)s(?:(?:,|,? et| à) %(article_num)s)+)?"
r"(?:,? ?\(?(?:%(article_subtype)s) %(article_num)s\)?)*"
r"(?: de l'annexe(?: %(article_num)s)?| du statut annexe)?"
r"(?: \([^)]+\))*"
r"(?:,? (?:introduction|suite|nouveau|ancien|[Aa]nnexe|[Pp]réambule)$)?"
) % globals()

article_num_multi_1 = (
r"(?:Annexes?,? (?:%(article_num)s )?)?(?:- )?(?:art(?:\.|icle) )?(%(article_num)s)"
r"(?:(,? à (?!l'art)|,? et |, ?)(?:(?:art(?:\.|icle) )?((?:[Aa]nnexe )?%(article_num)s)|([Aa]nnexe)$))+"
r"(?P<incomplete>,$|, art?$)?"
) % globals()

article_num_multi_2 = (
r"Annexes? \((%(article_num)s)(?:(,? à |,? et |, ?)(%(article_num)s))+\)"
) % globals()

article_num_multi_sub = r"^([0-9]+)(?:(?: \(|, )(\1-[0-9]+(?:(?:,| et) \1-[0-9]+)+)\)?)$"
# Exemples:
# - "13, 13-1, 13-2, 13-3, 13-4" (LEGIARTI000006864199)
# - "15 (15-1 et 15-2)" (LEGIARTI000006864203)

article_num_multi = (
r"(?:%(article_num_multi_1)s|%(article_num_multi_2)s|%(article_num_multi_sub)s)"
) % globals()


article_num_re = re.compile(article_num)
article_num_extra_re = re.compile(r"\b(?:%s)\b" % article_num_extra, re.I)
article_titre_re = re.compile(article_titre)


def article_num_to_title(num):
"""Turn a raw article number into an article title suitable for display.

>>> article_num_to_title('1er')
'Article 1er'
>>> article_num_to_title('B')
'Article B'
>>> article_num_to_title('unique')
'Article unique'
>>> article_num_to_title('Annexe')
'Annexe'
"""
if article_num_re.match(num) or num[0].islower():
return 'Article ' + num
return num


def legifrance_url_article(id, cid):
return 'https://www.legifrance.gouv.fr/affichCodeArticle.do?idArticle=%s&cidTexte=%s' % (id, cid)


def test_article_num_parsing(db, limit):
i = 0
q = db.all("""
SELECT id, cid, num
FROM articles
WHERE num IS NOT NULL
AND num <> ''
""")
for article_id, cid, num in q:
if '(' in num or ')' in num:
num = num.replace('(', '').replace(')', '')
m = article_titre_re.match(num)
if not m:
if article_num_re.search(num):
print(repr(num), ' ', legifrance_url_article(article_id, cid))
i += 1
if i > limit:
break
elif len(m.group(0)) != len(num):
matched = m.group(0)
if matched.isdigit():
n = int(matched)
if num == '%i - %i' % (n, n + 1):
# exemple: "25 - 26", LEGIARTI000006364359
continue
print(repr(show_match(m)), ' ', legifrance_url_article(article_id, cid))
i += 1
if i > limit:
break


if __name__ == '__main__':
p = ArgumentParser()
p.add_argument('db')
p.add_argument('-l', '--limit', type=int, default=float('inf'))
args = p.parse_args()

db = connect_db(args.db)
try:
with db:
test_article_num_parsing(db, args.limit)
except KeyboardInterrupt:
pass
8 changes: 4 additions & 4 deletions legi/factorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from lxml import etree

from .normalize import main as normalize
from .normalize import normalize_text_titles
from .utils import connect_db


Expand Down Expand Up @@ -117,6 +117,8 @@ def factorize_by(db, key):


def main(db):
print("> Factorisation des textes...")

connect_by_nature_num(db)

db.run("""
Expand Down Expand Up @@ -250,9 +252,7 @@ def main(db):
UPDATE textes_versions SET texte_id = NULL WHERE texte_id IS NOT NULL;
""")
if db.one("SELECT id FROM textes_versions WHERE titrefull_s IS NULL LIMIT 1"):
print("> Normalisation des titres...")
normalize(db)
print("> Factorisation des textes...")
normalize_text_titles(db)
main(db)
except KeyboardInterrupt:
pass
19 changes: 19 additions & 0 deletions legi/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,25 @@ def clean_html(html, cleaner=HTMLCleaner()):
return cleaner.close()[6:-7]


first_paragraph_re = re.compile(r"^(?:<p(?: [^>]+)?>(.+?)</p>|(.+?)<br/><br/>)(.*)")


def split_first_paragraph(html):
"""Extract the content of the first paragraph from an HTML snippet.

Returns a two-tuple `(first_paragraph, rest)`.

>>> split_first_paragraph('<br/><p align="center">Foobar</p>')
('Foobar', '')
>>> split_first_paragraph('First line<br/>Second line<br/><br/><p>Lorem <b>ipsum</b></p>')
('First line\nSecond line', '<p>Lorem <b>ipsum</b></p>')
"""
m = first_paragraph_re.match(clean_html(html))
if m:
return (m.group(1) or m.group(2)).replace('<br/>', '\n').strip(), m.group(3)
return '', ''


strip_re = re.compile(r"<.+?>|[ \t\n\r\f\v]+", re.S)


Expand Down
Loading