Appelez l'API Hatena Blog depuis Python et enregistrez vos articles de blog individuellement sur votre PC

Aperçu

En repensant au blog Hatena que j'ai écrit en 2019, j'ai décidé de créer un WordCloud pour mon article de blog [^ 1]. Après quelques recherches, j'ai trouvé Article appelant l'API du blog Hatena en JavaScript. J'ai donc essayé de faire la même chose avec Python. Cet article résume ce que j'ai appris en bougeant mes mains.

[^ 1]: mettre à jour le lien après la publication

Le script utilisé dans cet article effectue les opérations suivantes:

Il y a deux sujets à discuter:

  1. Préparation avant d'appeler l'API de Hatena Blog
  2. Comment analyser la réponse API (XML) du blog Hatena

Environnement d'exploitation

$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.4
BuildVersion:	18E226
$ python -V
Python 3.7.3  #Je crée un environnement virtuel avec le module venv
$ pip list | grep requests
requests    2.22.0

Préparation: jusqu'à ce que vous appeliez l'API de Hatena Blog

La préparation comprend deux étapes.

Tout d'abord, à propos de l'API de Hatena Blog

Blog Hatena AtomPub | http://developer.hatena.ne.jp/ja/documents/blog/apis/atom

Une API utilisant le protocole de publication Atom (AtomPub) est ouverte au public.

Blog Hatena Pour utiliser AtomPub, le client doit effectuer l'authentification OAuth, l'authentification WSSE ou l'authentification de base.

Cette fois, je l'ai implémenté avec ** l'authentification de base **.

La ** réponse est renvoyée au format XML **, comme décrit dans «Obtenir une liste des entrées de blog» dans le document ci-dessus.

Préparation 1. Savoir analyser XML avec Python

J'ai utilisé le module standard API XML ElementTree (https://docs.python.org/ja/3/library/xml.etree.elementtree.html).

Le module xml.etree.ElementTree implémente une API simple et efficace pour analyser et créer des données XML.

Vérifions le fonctionnement de XML avec un exemple simple. La même opération a été effectuée sur la réponse du blog Hatena Atom Pub.

practice.py


import xml.etree.ElementTree as ET

sample_xml_as_string = """<?xml version="1.0"?>
<data>
    <member name="Kumiko">
        <grade>2</grade>
        <instrument>euphonium</instrument>
    </member>
    <member name="Kanade">
        <grade>1</grade>
        <instrument>euphonium</instrument>
    </member>
    <member name="Mizore">
        <grade>3</grade>
        <instrument>oboe</instrument>
    </member>
</data>"""

root = ET.fromstring(sample_xml_as_string)

Spécifiez l'option -i [^ 2] lors de l'exécution du script avec l'interpréteur Python. Cette spécification fait apparaître le mode interactif après l'exécution du script (l'intention est d'éviter d'avoir à saisir des chaînes XML à partir du mode interactif).

$ python -i practice.py
>>> root
<Element 'data' at 0x108ac4f48>
>>> root.tag  # <data>Représente une balise
'data'
>>> root.attrib  # <data>Attributs sur les balises(attribute)ne pas
{}
>>> for child in root:  # <data>Niché dans les balises<member>Vous pouvez retirer l'étiquette
...     print(child.tag, child.attrib)
...
member {'name': 'Kumiko'}  #membre est un attribut appelé nom(attribute)avoir
member {'name': 'Kanade'}
member {'name': 'Mizore'}

En plus de l'instruction for, la structure imbriquée des balises XML peut également être gérée par les méthodes find et findall [^ 3].

--find: "Trouver le premier élément enfant avec une balise spécifique" --findall: "Rechercher uniquement les éléments enfants directs de l'élément courant par balise"

>>> #A continué
>>> someone = root.find('member')
>>> print(someone.tag, someone.attrib)
member {'name': 'Kumiko'}  #Premier élément enfant(member)Il est devenu
>>> members = root.findall('member')
>>> for member in members:
...     print(member.tag, member.attrib)
...
member {'name': 'Kumiko'}  #Tous les éléments enfants(member)A été retiré
member {'name': 'Kanade'}
member {'name': 'Mizore'}
>>> for member in members:
...     instrument = member.find('instrument')
...     print(instrument.text)  #Partie de texte prise en sandwich entre les balises
...
euphonium
euphonium
oboe

Cette fois, j'ai analysé la réponse du blog Atom Pub dans find et findall.

Préparation 2. Appelez l'API en utilisant vos informations d'identification

Les informations pour appeler le blog Hatena AtomPub avec l'authentification de base se trouvent dans Paramètres du blog Hatena> Paramètres avancés> AtomPub.

hatenablog_atompub_config.png

L'authentification de base utilisait l'argument ʻauth de la méthode getderequests` [^ 4].

blog_entries_url = "https://blog.hatena.ne.jp/nikkie-ftnext/nikkie-ftnext.hatenablog.com/atom/entry"
user_pass_tuple = ("nikkie-ftnext", "the_api_key")
r = requests.get(blog_entries_url, auth=user_pass_tuple)
root = ET.fromstring(r.text)

Passez la chaîne au format XML r.text à la méthode ʻElementTree.fromstring`.

Analyse de la réponse API du blog Hatena

Nous avons procédé à l'analyse de XML en comparant la réponse réelle avec "Obtenir une liste d'entrées de blog" à http://developer.hatena.ne.jp/ja/documents/blog/apis/atom.

In [20]: for child in root:  #root est le même que dans l'exemple ci-dessus
    ...:     print(child.tag, child.attrib)  
    ...:                                                                        
{http://www.w3.org/2005/Atom}link {'rel': 'first', 'href': 'https://blog.hatena.ne.jp/nikkie-ftnext/nikkie-ftnext.hatenablog.com/atom/entry'} 
{http://www.w3.org/2005/Atom}link {'rel': 'next', 'href': 'https://blog.hatena.ne.jp/nikkie-ftnext/nikkie-ftnext.hatenablog.com/atom/entry?page=1572227492'} #Page suivante de la liste d'articles
{http://www.w3.org/2005/Atom}title {} 
{http://www.w3.org/2005/Atom}subtitle {} 
{http://www.w3.org/2005/Atom}link {'rel': 'alternate', 'href': 'https://nikkie-ftnext.hatenablog.com/'} 
{http://www.w3.org/2005/Atom}updated {} 
{http://www.w3.org/2005/Atom}author {} 
{http://www.w3.org/2005/Atom}generator {'uri': 'https://blog.hatena.ne.jp/', 'version': '3977fa1b6c9f31b5eab4610099c62851'} 
{http://www.w3.org/2005/Atom}id {} 
{http://www.w3.org/2005/Atom}entry {} #Articles individuels
{http://www.w3.org/2005/Atom}entry {} 
{http://www.w3.org/2005/Atom}entry {} 
{http://www.w3.org/2005/Atom}entry {} 
{http://www.w3.org/2005/Atom}entry {} 
{http://www.w3.org/2005/Atom}entry {} 
{http://www.w3.org/2005/Atom}entry {} 
{http://www.w3.org/2005/Atom}entry {} 
{http://www.w3.org/2005/Atom}entry {} 
{http://www.w3.org/2005/Atom}entry {} 

Nous examinerons également les articles individuels (les balises sont des éléments d'entrée).

>>> for item in child:  #l'enfant contient un article (le reste du pour)
...     print(item.tag, item.attrib)
...
{http://www.w3.org/2005/Atom}id {}
{http://www.w3.org/2005/Atom}link {'rel': 'edit', 'href': 'https://blog.hatena.ne.jp/nikkie-ftnext/nikkie-ftnext.hatenablog.com/atom/entry/26006613457877510'}
{http://www.w3.org/2005/Atom}link {'rel': 'alternate', 'type': 'text/html', 'href': 'https://nikkie-ftnext.hatenablog.com/entry/rejectpy2019-plan-step0'}
{http://www.w3.org/2005/Atom}author {}
{http://www.w3.org/2005/Atom}title {}  #Titre (cible de WordCloud)
{http://www.w3.org/2005/Atom}updated {}
{http://www.w3.org/2005/Atom}published {}  #Remarque: également stocké dans le brouillon
{http://www.w3.org/2007/app}edited {}
{http://www.w3.org/2005/Atom}summary {'type': 'text'}
{http://www.w3.org/2005/Atom}content {'type': 'text/x-markdown'}  #Texte
{http://www.hatena.ne.jp/info/xmlns#}formatted-content {'type': 'text/html'}
{http://www.w3.org/2005/Atom}category {'term': 'Rapport du conférencier'}
{http://www.w3.org/2007/app}control {}  #Contient des informations indiquant si l'élément enfant est brouillon ou non

Je veux créer WordCloud à partir d'articles publiés, alors vérifiez le brouillon de l'élément enfant de {http://www.w3.org/2007/app} control (si oui, il s'agit d'un brouillon non publié, donc il n'est pas applicable) ..

Le texte utilisé pour WordCloud a été utilisé en reliant le titre et le texte.

Le code entier

Publié dans le référentiel suivant: https://github.com/ftnext/hatenablog-atompub-python

main.py


import argparse
from datetime import datetime, timedelta, timezone
import os
from pathlib import Path
import xml.etree.ElementTree as ET

import requests


def load_credentials(username):
    """Renvoie les informations d'identification requises pour l'accès à l'API Hatena au format tapple"""
    auth_token = os.getenv("HATENA_BLOG_ATOMPUB_KEY")
    message = "Variable d'environnement`HATENA_BLOG_ATOMPUB_KEY`Veuillez définir la clé API d'AtomPub sur"
    assert auth_token, message
    return (username, auth_token)


def retrieve_hatena_blog_entries(blog_entries_uri, user_pass_tuple):
    """Accédez à l'API Hatena Blog et retournez le code XML représentant la liste d'articles sous forme de chaîne de caractères"""
    r = requests.get(blog_entries_uri, auth=user_pass_tuple)
    return r.text


def select_elements_of_tag(xml_root, tag):
    """Analyse le XML de retour et retourne tous les éléments enfants avec la balise spécifiée"""
    return xml_root.findall(tag)


def return_next_entry_list_uri(links):
    """Renvoie le point de terminaison de la liste d'articles de blog suivante"""
    for link in links:
        if link.attrib["rel"] == "next":
            return link.attrib["href"]


def is_draft(entry):
    """Déterminer si un article de blog est un brouillon"""
    draft_status = (
        entry.find("{http://www.w3.org/2007/app}control")
        .find("{http://www.w3.org/2007/app}draft")
        .text
    )
    return draft_status == "yes"


def return_published_date(entry):
    """Renvoie la date de publication du billet de blog

C'était une spécification qui était retournée même dans le cas d'un projet
    """
    publish_date_str = entry.find(
        "{http://www.w3.org/2005/Atom}published"
    ).text
    return datetime.fromisoformat(publish_date_str)


def is_in_period(datetime_, start, end):
    """Déterminer si la date et l'heure spécifiées sont incluses dans la période du début à la fin"""
    return start <= datetime_ < end


def return_id(entry):
    """Renvoie la partie ID contenue dans l'URI du blog"""
    link = entry.find("{http://www.w3.org/2005/Atom}link")
    uri = link.attrib["href"]
    return uri.split("/")[-1]


def return_contents(entry):
    """Connectez et renvoyez le titre et le texte du blog"""
    title = entry.find("{http://www.w3.org/2005/Atom}title").text
    content = entry.find("{http://www.w3.org/2005/Atom}content").text
    return f"{title}。\n\n{content}"


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("hatena_id")
    parser.add_argument("blog_domain")
    parser.add_argument("target_year", type=int)
    parser.add_argument("--output", type=Path)
    args = parser.parse_args()

    hatena_id = args.hatena_id
    blog_domain = args.blog_domain
    target_year = args.target_year
    output_path = args.output if args.output else Path("output")

    user_pass_tuple = load_credentials(hatena_id)

    blog_entries_uri = (
        f"https://blog.hatena.ne.jp/{hatena_id}/{blog_domain}/atom/entry"
    )

    jst_tz = timezone(timedelta(seconds=9 * 60 * 60))
    date_range_start = datetime(target_year, 1, 1, tzinfo=jst_tz)
    date_range_end = datetime(target_year + 1, 1, 1, tzinfo=jst_tz)

    oldest_published_date = datetime.now(jst_tz)
    target_entries = []

    while date_range_start <= oldest_published_date:
        entries_xml = retrieve_hatena_blog_entries(
            blog_entries_uri, user_pass_tuple
        )
        root = ET.fromstring(entries_xml)

        links = select_elements_of_tag(
            root, "{http://www.w3.org/2005/Atom}link"
        )
        blog_entries_uri = return_next_entry_list_uri(links)

        entries = select_elements_of_tag(
            root, "{http://www.w3.org/2005/Atom}entry"
        )
        for entry in entries:
            if is_draft(entry):
                continue
            oldest_published_date = return_published_date(entry)
            if is_in_period(
                oldest_published_date, date_range_start, date_range_end
            ):
                target_entries.append(entry)
        print(f"{oldest_published_date}Obtenez des articles jusqu'à (tous{len(target_entries)}Cas)")

    output_path.mkdir(parents=True, exist_ok=True)

    for entry in target_entries:
        id_ = return_id(entry)
        file_path = output_path / f"{id_}.txt"
        contents = return_contents(entry)
        with open(file_path, "w") as fout:
            fout.write(contents)

Exemple d'exécution

$ python main.py nikkie-ftnext nikkie-ftnext.hatenablog.com 2019 --output output/2019
2019-10-30 11:25:23+09:Obtenez des articles jusqu'à 00 (9 au total)
2019-06-13 10:18:36+09:Obtenez des articles jusqu'à 00 (18 au total)
2019-03-30 13:52:19+09:Obtenez des articles jusqu'à 00 (27 au total)
2018-12-23 10:24:06+09:Obtenez les articles jusqu'à 00 (32 au total)
# -> output/32 fichiers texte seront créés sous 2019 (les contenus sont des blogs que j'ai écrits)

De cette façon, j'ai pu obtenir l'article de blog que j'ai écrit sur le blog Hatena AtomPub!

Envoyer

En répétant la même chose, je voudrais étudier et intégrer les éléments suivants:

――Il semble que vous ne pouvez obtenir votre propre blog que sur Atom Pub ――Si vous souhaitez cibler les blogs d'autres personnes, vous devez probablement devenir membre du blog -Ou grattage après avoir vérifié le fichier robots.txt? --Analyse de XML avec espace de noms - https://docs.python.org/ja/3/library/xml.etree.elementtree.html#parsing-xml-with-namespaces --Vous n'avez pas eu à spécifier quelque chose comme {http://www.w3.org/2005/Atom} link (je pourrais l'écrire dans DRY comme dans le document) --Il est également possible d'essayer beautifulsoup4 pour l'analyse XML.

c'est tout.

Recommended Posts

Appelez l'API Hatena Blog depuis Python et enregistrez vos articles de blog individuellement sur votre PC
Obtenez des visites d'articles et des likes avec l'API Qiita + Python
[Python] Récupérez le texte de la loi à partir de l'API e-GOV law
Appelez votre propre module python à partir du package ROS
Obtenez votre fréquence cardiaque à partir de l'API fitbit en Python!
Comment générer le nombre de vues, de likes et de stocks d'articles publiés sur Qiita au format CSV (créé avec "Python + Qiita API v2")
Appelez l'API avec python3.
Installez mecab sur le serveur partagé Sakura et appelez-le depuis python
Comment obtenir des abonnés et des abonnés de Python à l'aide de l'API Mastodon
La route pour installer Python et Flask sur un PC hors ligne
Utilisez Firefox avec Selenium depuis python et enregistrez la capture d'écran
L'histoire de Python et l'histoire de NaN
Existence du point de vue de Python
Utilisez l'API Flickr de Python
Appeler C / C ++ depuis Python sur Mac
[python] Envoyez l'image capturée de la caméra Web au serveur et enregistrez-la
Notes d'apprentissage depuis le début de Python 1
Je suis tombé sur l'API Hatena Keyword
Notes d'apprentissage depuis le début de Python 2
Enregistrez automatiquement les images de vos personnages préférés à partir de la recherche d'images Google avec Python
Jouons avec Python Receive et enregistrez / affichez le texte du formulaire de saisie
[Langage C] Faites attention à la combinaison de l'API de mise en mémoire tampon et de l'appel système sans mise en mémoire tampon
Deep Learning from scratch La théorie et la mise en œuvre de l'apprentissage profond appris avec Python Chapitre 3
Installez l'API Python du simulateur de conduite automatique LGSVL et exécutez un exemple de programme
[Python] Totale automatiquement le nombre total d'articles publiés par Qiita à l'aide de l'API
Récupérer le contenu de git diff depuis python
Résumé des différences entre PHP et Python
La réponse de "1/2" est différente entre python2 et 3
Spécification de la plage des tableaux ruby et python
Comparez la vitesse d'ajout et de carte Python
Lier PHP et Python à partir de zéro sur Laravel
Au moment de la mise à jour de python avec ubuntu
Prise en compte des forces et faiblesses de Python
Appeler Polly à partir du kit SDK AWS pour Python
Essayez d'accéder à l'API YQL directement depuis Python 3
[Python3] Prenez une capture d'écran d'une page Web sur le serveur et recadrez-la davantage
Construisez un serveur API pour vérifier le fonctionnement de l'implémentation frontale avec python3 et Flask
Extraire des images et des tableaux de pdf avec python pour réduire la charge de reporting
J'ai essayé d'automatiser la mise à jour de l'article du blog Livedoor avec Python et sélénium.
J'ai comparé la vitesse de la référence du python dans la liste et la référence de l'inclusion du dictionnaire faite à partir de la liste dans.
Soyons avertis de la météo dans votre région préférée de Yahoo Weather sur LINE!
[Python] Enregistrez le résultat du scraping Web de la page produit Mercari sur Google Colab dans une feuille de calcul Google et affichez également l'image du produit.