[PYTHON] Résumé de la page Web (prétraitement)

Préface

Cet article est basé sur ❷ de Collecte et classification des informations relatives à l'apprentissage automatique (concept).

Pour classer automatiquement les pages Web collectées par exploration, comme indiqué précédemment dans Classification multi-étiquettes par forêt aléatoire avec scikit-learn, forêt aléatoire et Il est possible de classer dans un environnement sur site avec un algorithme tel qu'un arbre de décision, mais d'un autre côté, comme exemple d'utilisation du Watson Natural Language Classifier de Ruby Il est également possible d'utiliser des services sur le cloud.

Le prétraitement tel que décrit dans Types de prétraitement dans le traitement du langage naturel et sa puissance est très important.

Cependant, si vous souhaitez utiliser un service sur le cloud pour classer automatiquement les pages Web, vous devez effectuer un prétraitement non mentionné dans cet article.

C'est le "Résumé de la page Web".

En règle générale, les services sur le cloud ont une limite sur la quantité de données par enregistrement. Exemple d'utilisation de Watson Natural Language Classifier de Ruby a une limitation de 1024 caractères par article à classer, donc découpez jusqu'à 1024 octets à partir du début de l'article de blog J'ai essayé de le transmettre au service. C'est aussi un «résumé» rudimentaire.

Dans le cas des pages Web, il n'y a pas de limite sur la quantité de données, il est donc nécessaire de concevoir des moyens de réduire la quantité de données transmises aux services sur le cloud sans perdre les informations requises pour la classification.

Je pense que la technique la plus orthodoxe pour cela est la "synthèse de pages Web".

Techniques générales de synthèse

Je n'ai pas trouvé beaucoup de tendances récentes en matière de technologie de résumé, mais il y a environ trois ans, Tendances de la recherche sur la technologie de résumé automatique A été utile.

Aussi, en utilisant LexRank comme implémentation concrète du code je vais résumer le discours de Donald Trump en trois lignes avec l'algorithme de synthèse automatique LexRank ) Et Résumé de la valeur du produit du site EC utilisant l'algorithme de synthèse automatique LexRank sont publiés sur Qiita.

Ces implémentations résument les phrases avec la politique de traiter les phrases comme un ensemble de «phrases», d'attribuer de l'importance aux «phrases» et d'extraire dans l'ordre des «phrases» les plus importantes.

La question ici est de savoir comment diviser une page Web en «phrases».

À ce propos, je n'ai pas pu trouver le code d'implémentation même si je cherchais sur le net, et la seule logique qui pouvait être implémentée était ["Division de texte HTML pour la synthèse de pages Web en extrayant des phrases importantes"](http: / /harp.lib.hiroshima-u.ac.jp/hiroshima-cu/metadata/5532) était le papier [^ 1].

Après tout, je n'ai pas trouvé le code d'implémentation, donc cette fois j'ai décidé d'implémenter le code [^ 2] avec la logique de cet article.

Logique spécifique

La logique spécifique comprend les trois étapes suivantes.

(1) Divisez le tout en unités de texte (phrases candidates) La pause est en dessous

(2) Classer les unités de texte en unités de phrase et unités sans phrase La peine doit remplir au moins trois des conditions suivantes --C1. Le nombre de mots indépendants est de 7 ou plus --C2. Le rapport entre le nombre de mots indépendants et le nombre total de mots est égal ou inférieur à 0,64 --C3. Le rapport entre le nombre de mots attachés et le nombre de mots indépendants est égal ou supérieur à 0,22 [^ 3] --C4 Le rapport entre le nombre de mots auxiliaires et le nombre de mots indépendants est de 0,26 ou plus. --C5 Le rapport entre le nombre de verbes auxiliaires et le nombre de mots indépendants est de 0,06 ou plus.

(3) Combinez et divisez les unités sans phrase en fonction du nombre de mots indépendants

Par souci de simplicité cette fois, l'unité sans instruction se joint uniquement et n'implémente pas le fractionnement.

De plus, le code ci-dessous est implémenté pour extraire uniquement la partie corps du HTML, car il est supposé que le contenu de l'élément title est utilisé pour le nom de fichier du fichier HTML téléchargé. En général, c'est une bonne idée d'utiliser également le contenu de l'élément title dans le résumé.

Diviser le HTML en unités de texte et convertir en texte brut

html2plaintext_part_1.py


import codecs
import re

class Article:

    #Essayez les codes de caractères dans cet ordre
    encodings = [
        "utf-8",
        "cp932",
        "euc-jp",
        "iso-2022-jp",
        "latin_1"
    ]

    #Expression normale d'extraction d'élément de niveau bloc
    block_level_tags = re.compile("(?i)</?(" + "|".join([
        "address", "blockquote", "center", "dir", "div", "dl",
        "fieldset", "form", "h[1-6]", "hr", "isindex", "menu",
        "noframes", "noscript", "ol", "pre", "p", "table", "ul",
        "dd", "dt", "frameset", "li", "tbody", "td", "tfoot",
        "th", "thead", "tr"
        ]) + ")(>|[^a-z].*?>)")

    def __init__(self,path):
        print(path)
        self.path = path
        self.contents = self.get_contents()

    def get_contents(self):
        for encoding in self.encodings:
            try:
                lines = ' '.join([line.rstrip('\r\n') for line in codecs.open(self.path, 'r', encoding)])
                parts = re.split("(?i)<(?:body|frame).*?>", lines, 1)
                if len(parts) == 2:
                    head, body = parts
                else:
                    print('Cannot split ' + self.path)
                    body = lines
                body = re.sub(r"(?i)<(script|style|select).*?>.*?</\1\s*>"," ", body)
                body = re.sub(self.block_level_tags, ' _BLOCK_LEVEL_TAG_ ', body)
                body = re.sub(r"(?i)<a\s.+?>",' _ANCHOR_LEFT_TAG_ ', body)
                body = re.sub("(?i)</a>",' _ANCHOR_RIGHT_TAG_ ', body)
                body = re.sub("(?i)<[/a-z].*?>", " ", body)
                blocks = []
                for block in body.split("_BLOCK_LEVEL_TAG_"):
                    units = []
                    for unit in block.split("。"):
                        unit = re.sub("_ANCHOR_LEFT_TAG_ +_ANCHOR_RIGHT_TAG_", " ", unit) #Exclure les liens vers des images
                        if not re.match(r"^ *$", unit):
                            for fragment in re.split("((?:_ANCHOR_LEFT_TAG_ .+?_ANCHOR_LEFT_TAG_ ){2,})", unit):
                                fragment = re.sub("_ANCHOR_(LEFT|RIGHT)_TAG_", ' ', fragment)
                                if not re.match(r"^ *$", fragment):
                                    if TextUnit(fragment).is_sentence():
                                        #Les unités de relevé se terminent par "."
                                        if len(units) > 0 and units[-1] == '―':
                                            units.append('。\n')
                                        units.append(fragment)
                                        units.append(' 。\n')
                                    else:
                                        #Les unités sans phrase se terminent par "-".
                                        # (Contrainte)Contrairement à l'article, les unités sans phrase sont uniquement combinées et non divisées.
                                        units.append(fragment)
                                        units.append('―')
                    if len(units) > 0 and units[-1] == '―':
                       units.append('。\n')
                    blocks += units
                return re.sub(" +", " ", "".join(blocks))
            except UnicodeDecodeError:
                continue
        print('Cannot detect encoding of ' + self.path)
        return None

Distinguer les unités de relevé et les unités non-relevé

html2plaintext_part_2.py


from janome.tokenizer import Tokenizer
from collections import defaultdict
import mojimoji
#import re

class TextUnit:

    tokenizer = Tokenizer("user_dic.csv", udic_type="simpledic", udic_enc="utf8")

    def __init__(self,fragment):
        self.fragment   = fragment
        self.categories = defaultdict(int)
        for token in self.tokenizer.tokenize(self.preprocess(self.fragment)):
            self.categories[self.categorize(token.part_of_speech)] += 1

    def categorize(self,part_of_speech):
        if re.match("^nom,(Général|代nom|固有nom|Changer de connexion|[^,]+tige)", part_of_speech):
            return 'Indépendance'
        if re.match("^verbe", part_of_speech) and not re.match("Sa étrange", part_of_speech):
            return 'Indépendance'
        if re.match("^adjectif,Indépendance", part_of_speech):
            return 'Indépendance'
        if re.match("^Particule", part_of_speech):
            return 'Particule'
        if re.match("^Verbe auxiliaire", part_of_speech):
            return 'Verbe auxiliaire'
        return 'Autre'

    def is_sentence(self):
        if self.categories['Indépendance'] == 0:
            return False
        match = 0
        if self.categories['Indépendance'] >= 7:
            match += 1
        if 100 * self.categories['Indépendance'] / sum(self.categories.values()) <= 64:
            match += 1
        if 100 * (self.categories['Particule'] + self.categories['Verbe auxiliaire']) / self.categories['Indépendance'] >= 22:
            #Interprété comme "mot attaché = verbe auxiliaire ⋃ verbe auxiliaire" selon l'article(Différent de la définition habituelle)
            match += 1
        if 100 * self.categories['Particule'] / self.categories['Indépendance'] >= 26:
            match += 1
        if 100 * self.categories['Verbe auxiliaire'] / self.categories['Indépendance'] >= 6:
            match += 1
        return match >= 3

    def preprocess(self, text):
        text = re.sub("&[^;]+;",  " ", text)
        text = mojimoji.han_to_zen(text, digit=False)
        text = re.sub('(\t | )+', " ", text)
        return text

Convertir un fichier HTML en fichier texte brut

html2plaintext_part_3.py


if __name__ == '__main__':
    import glob
    import os

    path_pattern = ''/home/samba/example/links/bookmarks.crawled/**/*.html'
    # The converted plaintext is put as '/home/samba/example/links/bookmarks.plaintext/**/*.txt'
    for path in glob.glob(path_pattern, recursive=True):
        article = Article(path)
        plaintext_path = re.sub("(?i)html?$", "txt", path.replace('.crawled', '.plaintext'))
        plaintext_dir  = re.sub("/[^/]+$", "", plaintext_path)
        if not os.path.exists(plaintext_dir):
            os.makedirs(plaintext_dir)
        with open(plaintext_path, 'w') as f:
            f.write(article.contents)

Je ne suis pas familier avec Python, donc je pense que c'est un code maladroit, mais je vous serais reconnaissant si vous pouviez signaler des améliorations.

Résumé en texte brut

Le texte brut généré de cette manière doit être résumé en utilisant quelque chose comme LexRank.

Dans cet exemple, janome a été utilisé pour l'analyse morphologique par souci de simplicité, mais comme "HTML-> texte brut" et "texte brut-> résumé" ont des étapes séparées, "texte brut-> résumé" est complètement différent. Vous pouvez également utiliser l'outil d'analyse morphologique de.

[^ 1]: La recherche originale était de 2002 à 2004, et une logique plus efficace peut avoir été proposée récemment.

Recommended Posts

Résumé de la page Web (prétraitement)
Réalisation Flask-Python
Page de résumé de l'article Flask
Résumé de l'article sur la programmation Web Python
Grattage WEB avec BeautifulSoup4 (page en couches)
[Note personnelle] Scraping de pages Web en python3
Surveillez les mises à jour des pages Web avec LINE BOT
Grattage WEB avec BeautifulSoup4 (page du numéro de série)