Commit efb6f235 authored by Bernard Stephan's avatar Bernard Stephan
Browse files

Amélioration de la détection des effets de caractères.

parent d4521af9
......@@ -191,7 +191,15 @@ Les éléments de la liste *blocks* ont donc la structure suivante :
- **h_min** et **h_max** : Les hauteurs minimum et maximum des lignes du
bloc (calculées à partir des valeurs *height* de chaque ligne du bloc,
voir ci-dessous).
- **nb_cars** et **nb_words** : Le nombre de caractères et le nombre de
- **right**, **left**, **top**, **down** : La distance au bloc le plus
proche situé respectivement à droite, à gauche, au-dessus et en-dessous
du présent bloc. Cette distance est calculée à l'aide des xMin, xMax, ...
Elle est négative s'il n'y a pas de bloc à la droite (à la gauche, ...)
du bloc.
- **right_block**, **left_block**, ... : Désignent le bloc le plus proche
à la droite, (à gauche, ....) du bloc. Il s'agit du bloc retenu comme
étant la plus proche lors du calcul des distances right, left, ...
- **nb_cars** et **nb_words** : Le nombre de caractères et le nombre de
mots du bloc (calculés).
- **class :** contient le résultat du processus de classification de blocs.
La valeur par défaut de cet attribut est BL_UNDEF.
......@@ -215,7 +223,33 @@ Les éléments de la liste *blocks* ont donc la structure suivante :
à ```yMax - yMin```, coordonnées du mot
renvoyées par *pdftotext*.
Lors de la concaténation du contenu des balises word effectuée pour renseigner
la valeur de lines['text'], un traitement particulier est effectué pour détecter
et traiter deux cas :
1. Les "L ignes" ayant un premier caractère mis en exergue, qui se retrouve
séparé du reste du mot. Dans le cas où la première
ligne d'un bloc commence par une balise *word* d'une seule lettre majuscule,
on teste si cette lettre est dans le dictionnaire puis si le mot suivant
l'est aussi. Dans le cas contraire on teste si la concaténation des deux mots
est dans le dictionnaire. Si c'est le cas, alors on garde le mot concaténé.
À noter que pour les aulx, 'A' et 'il' sont dans le dictionnaire. Néanmoins,
l'absence de sens grammatical d'une phrase commençant par "A il " nous
permet de le traiter comme un cas particulier.
1. Les "T I T R E S" ou lignes mal justifiées dans lesquelles les espacements
de caractères sont tels que *pdftotext* insère une espace entre chaque
caractère. Ces lignes sont détectées en calculant la moyenne du nombre de
lettres par mot. En-dessous d'un seuil (actuellement 3), la proportion de
lettres et d'espaces est calculée et doit être supérieure à un seuil
(actuellement deux-tiers).
Pour les lignes qui correspondent, le traitement repose sur la recherche de
deux valeurs d'espacement horizontal, pour tenter de distinguer
un espacement entre deux lettres d'un espacement entre deux mots. À l'aide
de ces valeurs, la phrase est recomposée en collant les lettres. Ensuite,
un comptage des mots présents dans le dictionnaire est effectué sur la ligne
ainsi recomposée et est comparé au même comptage effectué sur le ligne
renvoyée par *pdftotext*. Le texte ayant la plus forte proportion de mots
trouvés dans le dictionnaire est conservé.
### 2.2 pdftohtml
Ensuite, le programme *pdf2blocks* lance la commande *pdftohtml* :
......@@ -268,7 +302,7 @@ La liste **blocks** contient le résultat de la commande *pdftotext*, dont le
but est de restituer, à partir du document pdf, un texte lisible de haut en bas
dans une console texte.
Toutefois, l'ordonnancement des blocs par *pdftotext* s'est révélé peu
adapté à la lectures des Bulletins de Santé du Végétal. C'est pourquoi
adapté à la lecture des Bulletins de Santé du Végétal. C'est pourquoi
un ré-ordonnancement des blocs spécifique est effectué par *pdf2blocks*.
Les chapitres suivants décrivent les traitements, effectués séquentiellement,
......@@ -334,6 +368,9 @@ renvoyées par *pdftohtml*, qui ne correspondent pas exactement aux lignes
renvoyées par *pdftotext*. Par ailleurs, l'ordre des lignes dans une même
page est parfois différent entre les deux outils.
Néanmoins, lorsqu'une ligne est reconnue à l'identique, elle est marquée et
n'est plus utilisée dans l'algorithme.
À noter que cet algorithme n'est pas du tout optimisé. Il a été écrit pour
tester le principe, qui semble satisfaisant, mais il nécessite d'être
réécrit pour que son exécution soit plus rapide.
......@@ -764,6 +801,20 @@ classes de blocs :
| BL_PARAGRAPH | \<p\> |
| BL_MISC | \<p\>\<small\> |
Dans l'écriture du texte en lui-même, un certain nombre de traitements sont
effectués :
- suppression des césures, en se basant sur le dictionnaire (on teste si un mot
est présent dans le dictionnaire avant de supprimer une césure - les césures
entre blocs subsistent cependant),
- remplacement des apostrophes obliques (*quotes* fermantes) par des apostrophes
droites ('),
- remplacement des *doubles-quotes* et des guillemets à la française par des
doubles apostrophes droites ("), ce traitement ainsi que le précédent
perturbant les outils d'analyse lexicale,
- suppression des caractères "bizarres" (utilisés notamment pour les puces),
- suppression des espaces multiples.
##### Remarques
- BL_PARAGRAPH et BL_MISC sont tous deux écrits sous la balise \<p\>.
......
ascochytose
aspergillus
bactériose
BSV
cladosporiose
drosophila
érinose
flavescence
meligethes
metcalfa
pégomyie
poinsettia
rhizomanie
stemphyliose
......@@ -92,6 +92,37 @@ ALIGN_JUSTIFIED = 3
ALIGN_CLASSES = ['centered', 'left', 'right', 'justified', 'undefined']
#### THRESHOLDS
# Thresholds for S T Y L I S H E D titles
## To detect "stylished" titles, we count the number of characters per word
## in a line. If this number is less than STYLE_CHAR_PER_WORD_THRESHOLD,
## the "(maybe) stylished title" procedure is done.
STYLE_CHAR_PER_WORD_THRESHOLD = 3
## Lines having few characters per word are often numerical values in tables.
## So another condition to start the "maybe stylished" procedure is to have
## less than STYLE_CHAR_PROPORTION_THRESHOLD proportion of
## non-(alphabetic or space) characters in the line. The value 0.35 has
## quite good results and stands for about "one third"...
STYLE_CHAR_PROPORTION_THRESHOLD = 0.35
## This one is not a threshold but a list of particular cases for "T itles"
## having a bigger first letter. This is done particularly for "A il",
## because "A" and "il" are in a french dictionary, so they won't be
## concatenated if found at the beginig of a block. But "A il" doesn't have
## sense in french so it should be concatenated. This is why it's in this
## table. In the other hand, "CEREALES" is not found in the dictionary because
## it lacks two accents.
## Finally, "P OMMES" has nothing to do here because "ommes" is not in the
## dictionary and "pommes" is.
## Hope this make the use of this table understandable.
STYLE_CAPITAL_EXCEPTIONS = [
['A', r'ils?'],
['C', r'ereales?'],
['B', r'le'],
]
# These are thresholds for considering a block is shurely in a class
## "Normal" (default) paragrph
......@@ -128,29 +159,30 @@ NB_SUCCESSION_FOR_SAME = 0
#### Regex
INDICES_EXPOSANTS_USUELS = [
re.compile(r'^(er|ère|ere)$'), # 1er, 1ère, …
re.compile(r'^nde?$'), # 2nd
re.compile(r'^(e|i?[eè]me)$'), # 3ème, 4ieme, …
re.compile(r'^°$'),
r'^(er|ère|ere)$', # 1er, 1ère, …
r'^nde?$', # 2nd
r'^(e|i?[eè]me)$', # 3ème, 4ieme, …
r'^°$',
]
NUMBERING_REGEX = r'[0-9,\.\-\+\(\)%°±~ ]'
NUMBERING_REGEX = re.compile(r'[0-9,\.\-\+\(\)%°±~ ]')
PUNCTUATION_REGEX = r'[\.\;\,\?\!]'
PUNCTUATION_REGEX = re.compile(r'[\.\;\,\?\!]')
CAPTION_REGEX = ['cr[ée]dit photo', 'photo ?:']
CAPTION_REGEX = [ r'cr[ée]dit photo', r'photo ?:' ]
LINK_REGEX = ['https?://', 'clique[rz]', '\.gouv\.fr']
LINK_REGEX = [r'https?://', r'clique[rz]', r'\.gouv\.fr' ]
CREDITS_REGEX = ['R[ée]daction ?:', 'R[ée]dacteurs? ?:', 'R[ée]dactrices? ?:',
'cr[ée]dits photos', # Au pluriel ce coup-ci
'Animation du réseau', 'Animateurs? ?:', 'Animatrices? ?:',
'Directrices? de publication', 'Directeurs? de publication',
'[ée]dit[ée] sous la responsabilit[ée]', 'action (co-)?pilot[ée]e par',
'cr[eé]dits issus de la redevance pour pollutions diffuses',
]
CREDITS_REGEX = [
r'R[ée]daction ?:', r'R[ée]dacteurs? ?:', r'R[ée]dactrices? ?:',
r'cr[ée]dits photos', # Au pluriel ici, singulier dans CAPTION
r'Animation du réseau', r'Animateurs? ?:', r'Animatrices? ?:',
r'Directrices? de publication', r'Directeurs? de publication',
r'[ée]dit[ée] sous la responsabilit[ée]', r'action (co-)?pilot[ée]e par',
r'cr[eé]dits issus de la redevance pour pollutions diffuses',
]
COPYRIGHT_REGEX = ['Reproduction int[ée]grale', 'Reproduction partielle']
COPYRIGHT_REGEX = [ r'Reproduction int[ée]grale', 'Reproduction partielle' ]
CONTACT_REGEX = [r"([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)"]
......@@ -158,11 +190,11 @@ CONTACT_REGEX = [r"([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)"]
# Will be replaced with ' which is recognized by tree tagger
# Can be None.
APOSTROPHE_REGEX = re.compile(r"[’]")
APOSTROPHE_REGEX = r"[’]"
# Will be replaced with " which is recognized by tree tagger
# Can be None
DOUBLEQUOTE_REGEX = re.compile(r'[“”«»]')
DOUBLEQUOTE_REGEX = r'[“”«»]'
# https://gist.github.com/Alex-Just/e86110836f3f93fe7932290526529cd1
# Those characters will be substituted with spaces, and multiple spaces will
......
......@@ -45,6 +45,24 @@ def is_equal_wo_accents(ch1, ch2):
return unaccent_fr(ch1.strip().lower()) == unaccent_fr(ch2.strip().lower())
# +--------------------------------------------------------------+
# | check_dict |
# +--------------------------------------------------------------+
# Checks if it finds the word in DICT dictionary. If not, searches into
# DICT suggestions if it finds the same word without accent.
# Returns True if finds something.
def check_dict(word_to_test):
if len(word_to_test) == 0:
return False
if DICT.check(word_to_test):
return True
for sug in DICT.suggest(word_to_test):
if is_equal_wo_accents(sug, word_to_test):
return True
return False
# +--------------------------------------------------------------+
# | get_pdftotext |
# +--------------------------------------------------------------+
......@@ -106,41 +124,58 @@ def get_pdftotext(filename):
}
last_word = ''
nb_one_letter = 0
hspaces = []
last_x = -1
nb_cars = 0
nb_words = 0
no_space_pls = False
for word in line:
if (word.tag.endswith('word')):
hword = float(word.get('yMax')) - float(word.get('yMin'))
li['words'].append({'height': hword, 'text': word.text})
if len(word.text) == 1:
nb_one_letter += 1
# Some statistics to detect stlylished lines
if last_x <= 0:
last_x = float(word.get('xMax'))
else:
hspaces.append(float(word.get('xMin')) - last_x)
last_x = float(word.get('xMax'))
l = len(re.sub(r'\W', '', word.text))
if l > 0:
nb_words +=1
nb_cars += len(word.text)
if no_space_pls:
# no_space_pls indicates that the previous
# word was the first of the block and is
# a single capital letter. So it may be
# a "T itle" effect on the 1st letter.
particular_case = False
for couple in STYLE_CAPITAL_EXCEPTIONS:
particular_case = re.match(couple[0], li['text'], flags = re.IGNORECASE) \
and re.match(couple[1], word.text, flags = re.IGNORECASE)
if particular_case:
break
word_to_test = ("%s%s" % (li['text'], word.text)).split(" ")[-1]
#print(">>> %s" % word_to_test)
if DICT.check(li['text'].split(" ")[-1]) \
if particular_case:
li['text'] = "%s%s" % (li['text'], word.text)
elif DICT.check(li['text'].split(" ")[-1]) \
and DICT.check(word.text):
li['text'] = "%s %s" % (li['text'], word.text)
elif DICT.check(word_to_test):
elif check_dict(word_to_test) or \
check_dict(re.sub('\W','',word_to_test)):
# Quelques explications sur le test qui précède :
# Il peut sembler redondant mais ne l'est
# pas pour les apostrophes.
li['text'] = "%s%s" % (li['text'], word.text)
else:
# We give a try accentless because this
# case is often in titles, which may be
# accentless.
found = False
for sug in DICT.suggest(word_to_test):
found = is_equal_wo_accents(sug, word_to_test)
if found: break
if found:
li['text'] = "%s%s" % (li['text'], word.text)
else:
li['text'] = "%s %s" % (li['text'], word.text)
li['text'] = "%s %s" % (li['text'], word.text)
no_space_pls = False
elif last_word.isdecimal() and is_ind_exp(word.text):
li['text'] = "%s%s" % (li['text'], word.text)
elif first_line and len(li['text']) == 0 and \
re.match(r'^[A-ZÉÈÂÊÄËÏÎÔÖÙÜÛ]$', word.text.strip()):
# "T itle" case :
no_space_pls = True
li['text'] = "%s %s" % (li['text'], word.text)
else:
......@@ -148,9 +183,85 @@ def get_pdftotext(filename):
li['text'] = li['text'].strip()
last_word = word.text
# --- End "for word in line"
if len(li['text']) > 3 and w < h: # Probably vertical text
li['height'] = w
bl['flags'] |= FLAG_VERTICAL
### Is it a "S T Y L I S H" or badly justified line ?
# If yes, we'll redo it a different way
# If there are too much special characters or digits,
# the number of short words is certainly normal.
nb_special_car = len(li['text'])
nb_special_car -= len(re.sub(r"[\W\d]", '', re.sub(r'\s','A',li['text'])))
if nb_words > 1 and (float(nb_cars) / float(nb_words)) < STYLE_CHAR_PER_WORD_THRESHOLD \
and float(nb_special_car) / float(len(li['text'])) < STYLE_CHAR_PROPORTION_THRESHOLD:
hspaces.sort()
# Let's search if there are 2 kinds of spaces
# (space between letters and space between words)
left = 0
right = len(hspaces) - 1
diff_space = 0.25
while right > (left + 1):
dl = hspaces[left+1] - hspaces[left]
dr = hspaces[right] - hspaces[right-1]
if dl < dr:
left += 1
diff_space = max(diff_space, dl)
else:
right -= 1
diff_space = max(diff_space, dr)
if (hspaces[right] - hspaces[left]) <= diff_space: # 1 space size
car_space = hspaces[-1] + 0.1
else :
car_space = (hspaces[right] + hspaces[left]) / 2.0
last_x = -1
ltxt = wo = ''
words_list = []
hword = -1.0
for word in line:
if (word.tag.endswith('word')):
hword = max(hword, float(word.get('yMax')) - float(word.get('yMin')))
if last_x <= 0:
last_x = float(word.get('xMax'))
hs = 0
wo = word.text.strip()
else:
hs = float(word.get('xMin')) - last_x
last_x = float(word.get('xMax'))
if hs > car_space:
ltxt = ("%s %s" % (ltxt, wo)).strip()
words_list.append({'height': hword, 'text': wo.strip()})
hword = -1
wo = word.text.strip()
else:
wo = "%s%s" % (wo, word.text.strip())
ltxt = ("%s %s" % (ltxt, wo)).strip()
if len(wo.strip()) > 0:
words_list.append({'height': hword, 'text': wo.strip()})
nb_ok_new = 0
ls = re.sub(r' +', ' ', re.sub(r"[^'’\w]", ' ', ltxt)).strip().split(' ')
for wo in ls:
if len(re.sub('\d','',wo)) > 0: # Don't check numbers
if check_dict(wo): nb_ok_new += 1
prop_ok_new = float(nb_ok_new) / float(len(ls))
nb_ok_old = 0
ls = re.sub(r' +', ' ', re.sub(r"[^'’\w]", ' ', li['text'])).strip().split(' ')
for wo in ls:
if len(re.sub('\d','',wo)) > 0: # Don't check numbers
if check_dict(wo): nb_ok_old += 1
prop_ok_old = float(nb_ok_old) / float(len(ls))
if prop_ok_new > prop_ok_old :
li['text'] = ltxt
li['words'] = words_list
bl['lines'].append(li)
first_line = False
blocks.append(bl)
......@@ -557,62 +668,6 @@ def compute_lrud(page_blocks):
b['top_block'] = ob
# +--------------------------------------------------------------+
# | table_detection |
# +--------------------------------------------------------------+
# Tries to find tables of a page.
# Uses comute_lrud()'s left/right, ...
# Will try to detect succession of blocks in the same line having
# different blocks on 'top' or 'down', making it different to shitty-justified
# text (having same top or down). But cannot detect multi-column cells.
def table_detection(page_blocks):
# &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
# &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
# &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
# T O D O
# &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
# &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
# &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
# &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
for b in page_blocks:
b['left'] = b['right'] = b['top'] = b['down'] = -1
for ob in page_blocks: # ob : other block
if ob is not b:
if ob['y_min'] <= b['y_max'] and ob['y_max'] >= b['y_min']: # Vertically aligned
if ob['x_min'] >= b['x_max']: # ob on right
dist = ob['x_min'] - b['x_max']
if b['right'] < 0:
b['right'] = dist
b['right_block'] = ob
elif b['right'] > dist:
b['right'] = dist
b['right_block'] = ob
if ob['x_max'] < b['x_min']: # ob on left
dist = b['x_min'] - ob['x_max']
if b['left'] < 0:
b['left'] = dist
b['left_block'] = ob
elif b['left'] > dist:
b['left'] = dist
b['left_block'] = ob
if ob['x_min'] <= b['x_max'] and ob['x_max'] >= b['x_min']: # Horizontally aligned
if ob['y_min'] >= b['y_max']: # ob under b
dist = ob['y_min'] - b['y_max']
if b['down'] < 0:
b['down'] = dist
b['down_block'] = ob
elif b['down'] > dist:
b['down'] = dist
b['down_block'] = ob
if ob['y_max'] < b['y_min']: # ob on top of b
dist = ob['y_max'] - b['y_min']
if b['top'] < 0:
b['top'] = dist
b['top_block'] = ob
elif b['top'] > dist:
b['top'] = dist
b['top_block'] = ob
# +--------------------------------------------------------------+
# | expand_blocks |
# +--------------------------------------------------------------+
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment