Github gist の
形態素解析を
今回は、
[TOC]
前提
作成した
OS
cat /etc/redhat-release CentOS Linux release 7.4.1708 (Core)
python の
Version python3.6 -V Python 3.6.4
Django の
Version python3.6 -m pip list --format=columns | grep Django Django 1.10.8
その
他使用している ライブラリ wagtail 1.13.1 pandas 0.22.0 searchconsole 後述します。 puput 0.9.2.1
参考
【Python】はてな
キーワードAPIを 使って 特徴語を 抽出する - 歩いたら 休め
ブログ記事本文をはてな キーワード API の INPUTに して、 戻りで キーワードを 取得しています。
ほぼこの 実装を そのまま 使用させて 頂きました。 Quickstart: Run a Search Console App in Python | Search Console API | Google Developers
Google Search Console API をPython 経由で 実行する 方法が 記載されています。
何故 Google Search Console API と、 はてな キーワード API を 併用するのか ?
それぞれ、
Google Search Console API で
取得できる データの 特徴 人間が
Web 検索時に 使用した キーワード
【SEO上級編】検索キーワードの 種類と コンテンツマーケティングの 関係 | 本気ファクトリー株式会社 に 記載が ありますが、 トランザクショナルクエリ
、ナビゲーショナルクエリ
、インフォメーショナルクエリ
に分類可能な キーワードが 取得できます。 検索キーワードの
パフォーマンス
対象の検索キーワード で、 サイトが Google の 検索結果の 何番目に 表示されたのか 等の、 パフォーマンス情報が 入手できます。
はてな
キーワード API で 取得できる データの 特徴 - 人間が
付与、 作成した キーワード
人間が、意図的に 作成した キーワードを 取得できます。 タグのような イメージを 持ちました。
おそらく、インフォメーショナルクエリ に 属する 言葉が 取得できるのかと 思います。
- 人間が
2つの
API の 積集合が 持つ 特徴
実装して記載していないので、 予想に なりますが、 以下の 特徴を 持つ キーワードが 取得できるかと 思います。 - 検索キーワードと
して、 使われる 傾向の 強い 名詞 - 掲載順位、
クリック、 表示回数を 加味する ことで、 記事の 特徴を 表す 名詞 - タグ、
カテゴリと して 付与しても 違和感の ない 名詞
- 検索キーワードと
処理概要
以下の
- 記事を
INPUT に はてな キーワード API を 実行、 戻り値を 保持する。 - Google Search Console API を
実行し、 戻り値を 保持する。 1.
、2.
のデータを 元に、 キーワードの 積集合を 作成、 スコア等を 条件に 積集合を 絞り込む。
- テーブル定義に
ついて
はてなキーワード API を 戻りを 保持する テーブルと、 Google Search Console の 戻りを 保持する テーブルを 作成しました。
記事テーブルとの関係は 下図のようになっています。
以下、
1. 記事を INPUT に はてな キーワード API を 実行、 戻り値を 保持する。
下記の、
【
collect_hatena_keywords.py
from __future__ import print_function, unicode_literals import xmlrpc.client import six try: import HTMLParser except ImportError: from html.parser import HTMLParser from django.core.management.base import BaseCommand from puput.models import EntryPage from markdown import markdown from bs4 import BeautifulSoup from logging import getLogger from home.models import EntryHatenaKeyword logger = getLogger(__name__) class Command(BaseCommand): def handle(self, **options): logger.info(__name__ , " start") # --------------------------- # データを全件削除する # ---------------------- EntryHatenaKeyword.objects.all().delete() for blog_post in EntryPage.objects.all(): import time time.sleep(1) # blog contents html = markdown(blog_post.body) text = ''.join(BeautifulSoup(html, "html5lib").findAll(text=True)) if six.PY2: html_parser = HTMLParser.HTMLParser() else: if six.PY34: import html html_parser = html else: html_parser = HTMLParser() unescaped_text = html_parser.unescape(text) server = xmlrpc.client.ServerProxy("http://d.hatena.ne.jp/xmlrpc") res = server.hatena.setKeywordLink({"body": unescaped_text, 'mode': 'lite'}) word_list = res.get("wordlist") for word in word_list: word["word"] = word["word"].strip().capitalize() EntryHatenaKeyword.objects.create(entry=blog_post, **word) logger.info(__name__, " end")
説明
データは
DELETE ALL、 INSERT ALL
記事件数が少ないので、 全件削除、 全件登録しています。 多い 場合、 差分のみ 実行するなどの 考慮が 必要かと 思います。
リクエストの連続実行防止の ため、 1秒の インターバルを 設定しています。 登録項目に
ついて
項目と値の 意味に ついて 説明します。 項目名 説明 entry キーワード抽出対象となった記事ID word キーワード score スコア refcount キーワードの参照回数(はてなブログ等でのリンクの数) cname キーワードの分類名 markdown を
html 変換して、 本文を 抽出する
Blog の記事本文は、 markdown で 記載しています。
markdown から本文を 抽出する ため、 一度 html に 変換し、 BeautifulSoup で 本文抽出を 行なっています。 # blog contents html = markdown(blog_post.body) text = ''.join(BeautifulSoup(html, "html5lib").findAll(text=True)) if six.PY2: html_parser = HTMLParser.HTMLParser() else: if six.PY34: import html html_parser = html else: html_parser = HTMLParser() unescaped_text = html_parser.unescape(text)
2. Google Search Console API を 実行し、 戻り値を 保持する。
以下、
collect_search_console.py
from __future__ import print_function, unicode_literals from logging import getLogger import pandas as pd import searchconsole from django.core.management.base import BaseCommand from home.models import EntryGoogleSearchConsole from puput.models import EntryPage logger = getLogger(__name__) def split_data_frame_list(df, target_column, separator): ''' df = dataframe to split, target_column = the column containing the values to split separator = the symbol used to perform the split returns: a dataframe with each entry for the target column separated, with each element moved into a new row. The values in the other columns are duplicated across the newly divided rows. ''' def split_list_to_rows(row, row_accumulator, target_column, separator): split_row = row[target_column].split(separator) for s in split_row: new_row = row.to_dict() new_row["splited_" + target_column] = s row_accumulator.append(new_row) new_rows = [] df.apply(split_list_to_rows, axis=1, args=(new_rows, target_column, separator)) new_df = pd.DataFrame(new_rows) return new_df class Command(BaseCommand): def handle(self, **options): logger.info(__name__, " start") # -------------------------------------- # データ全件削除 # ------------------------------- EntryGoogleSearchConsole.objects.all().delete() # -------------------------------------- # SearchConsole API 実行 # ------------------------------- from django.conf import settings account = searchconsole.authenticate(service_account=settings.BASE_DIR + '/client_secrets.json') web_property = account['https://your.domain.com/'] report = web_property.query.range('today', days=-90).dimension('query', 'page').limit(50000).get() df = report.to_dataframe() df["slug"] = df["page"] # SearchConsole の URL から、blogのid を抽出。 # ここは、不要であれば、除去してください。 df["slug"] = df["slug"].str.replace("https://your.domain.com/posts/", "") df["slug"] = df["slug"].str.replace("https://your.domain.com/", "") df["slug"] = df["slug"].str.replace("/", "") df = df[df["slug"] != ""] df = split_data_frame_list(df, 'query', ' ') df["splited_query"] = df["splited_query"].str.strip().str.capitalize() # Dataframe の行数分繰り返し for index, row in df.iterrows(): entry = EntryPage.objects.filter(gist_id=row["slug"]).first() if not entry: continue dict = row.to_dict() del dict["slug"] EntryGoogleSearchConsole.objects.create(entry=entry, **dict) logger.info(__name__, " end")
説明
searchconsole に
ついて
Google Search Console のAPI を 実行する ライブラリが ないか 調べた ところ、 joshcarty/google-searchconsole: A wrapper for the Google Search Console API. が 見つかりました。
to_datafrome
で、pandas の dataframe に API の 戻り 値を 変換できます。
本線にマージされていませんが、 branch に ある version は、 サービスアカウントを 使った API 接続が できるようになっていますので、 それを 使用しています。 dataframe の
検索クエリを スペースで 区切って 複数行 dataframe に 変換する
検索クエリをスペースで 区切って、 単語化し、 且つ 分割した 数分行を 複製したかったため、 split_data_frame_list
という メソッドを 作成しました。
実装は以下の gist から 拝借させて 頂きました。
Efficiently split Pandas Dataframe cells containing lists into multiple rows, duplicating the other column’s values.URL から、
blog 記事の キーとなる ID を 抽出
df["slug"] = df["slug"].str.replace("https://your.domain.com/posts/", "")
あたりの記述で、 blog 記事 ID 抽出の ため、 文字列置換を 実施しています。 データの
登録方法は、 DELETE ALL、 INSERT ALL
こちらは、API 一撃で 取得できる データですので、 負荷は あまり 気に ならないかと 思います。
取得データが膨大な 場合は、 少しずつ データを 取得して 登録した ほうが いいかもしれません。 登録項目に
ついて
項目と値の 意味に ついて 説明します。 項目名 説明 entry キーワード抽出対象となった記事ID clicks クリックされた回数 impressions 表示された回数 page 表示対象、クリック対象となったURL position 掲載順位 query 検索キーワード splited_query スペースで分割した検索キーワードの一部
3. 1.
、2.
の データを 元に、 キーワードの 積集合を 作成、 スコア等を 条件に 積集合を 絞り込む。
1.
、2.
で
以下の
classify_entry.py
from __future__ import print_function, unicode_literals from django.core.management.base import BaseCommand from puput.models import EntryPage from home.models import EntryGoogleSearchConsole from home.models import EntryHatenaKeyword from django.db import connection from logging import getLogger logger = getLogger(__name__) class Command(BaseCommand): def handle(self, **options): logger.info(__name__, " start") cursor = connection.cursor() cursor.execute(""" select p_ep.page_ptr_id, h_ehk.word from puput_entrypage as p_ep inner join home_entryhatenakeyword as h_ehk on p_ep.page_ptr_id = h_ehk.entry_id where h_ehk.cname in ('elec','web') and h_ehk.score >= 25 and h_ehk.word in (select splited_query from home_entrygooglesearchconsole) order by p_ep.page_ptr_id """) rows = cursor.fetchall() import collections entry_tag_relations = collections.defaultdict(list) for k, v in rows: entry_tag_relations[k].append(v) for k, v in entry_tag_relations.items(): entry = EntryPage.objects.get(id=k) entry.tags.clear() entry.tags.add(*v) entry.save() logger.info(__name__, " end")
説明
抽出条件に
ついて
Commad に記載している SQL が 抽出条件に なります。
はてなキーワードの cname が、 elec
又は、web
で、スコア 25 以上の データ
はてなキーワードの word が、 Google Search Console の 検索クエリにも 含まれる データを 抽出しています。
Google Search Console のデータ量が 少なく、 多くの タグを 付与したいためこの 条件に していますが、 データ量が 多い 場合は、 表示件数、 掲載順位等も 条件に 含めてた ほうが よいかと 思います。 データ取得結果の
Taple を 辞書に 変換する
SQL の取得結果の Taple は BlogID と word の 2要素取得できます。
この値を key value と する 辞書の ほうが 後続処理が 行いやすかったので、 以下の 処理を 実施しています。 import collections entry_tag_relations = collections.defaultdict(list) for k, v in rows: entry_tag_relations[k].append(v)
まとめ
Google Search Console API と、
API だよりとなり、
ただ、
文書自体の
後は、
以上です。
コメント