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:
$ 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
La préparation comprend deux étapes.
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.
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
.
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.
L'authentification de base utilisait l'argument ʻauth de la méthode
getde
requests` [^ 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`.
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.
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)
$ 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!
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