2017-11-25

【Python】SEO対策向けの共起語取得ツールを作ってみた Selenium + Janome

SeleniumとJanomeで無理やり共起語取得ツールを作ってみた



自社の記事を検索上位に挙げるために必要な、検索最適化(SEO)。この検索最適化の要素の一つに、「共起語」の発生数というものがあるらしいです。


簡単に言うと、「Python 機械学習」と検索したときに、その関連する言葉が記事にたくさん入っていることが、検索上位の要因となっているという話です。


そこまでSEOに詳しくないので、詳細はこちらの記事にお任せします。

共起(きょうき)は、ある単語がある文章中に出たとき、その文章中に別の限られた単語が頻繁に出現すること。日本語の例では「崩御する」という単語に「天皇が」という言葉が多く用いられることや、単に他動詞には「を」が、「行く」には目的地を示す「に」が頻出することが挙げられる。(共起語の意味とは何?SEO効果があるのか? | アフィリエイト初心者でも稼げるネットビジネスブログ

順位を上げたいページ(ページA)に、そのページで狙っているキーワード(キーワードA)の共起語(キーワードB)を加える。
キーワードBをアンカーテキストにして、キーワードBについて書いてあるページ(ページB)にリンクを張る。
この手法を実行した途端に、ありとあらゆるSEOを施策していたのに3ページ目より上に上げられなかったページAがキーワードAの検索で2位に来たそうです。「共起語SEOをもう一度解説してみる」 

ということで、この共起語を見つけたいのですが、実際検索上位でどのような言葉がいくつ入っているのかがわかれば、逆算して「この単語とこの単語をいくつ出現させれば、検索上位を取れるのか」ということがわかります。そうすると、共起語取得ツールがあれば企業のWeb集客としても、非常に便利そうです。ということで、作ってみました。



アルゴリズム

とはいえ、自然言語処理は全然初心者なので、どう実装しようか迷いましたが、
東ロボ君の検索アルゴリズムをまねしてつくってみることにします。




  • コマンドライン引数に、検索語句を入力する 例「東ロボ 偏差値」
  • Google検索結果の上位10記事を取得する
  • 一つ一つのページに入り、HTMLファイルのbodyの中を取得する
  • 共起語を取得する
  • テキストファイルに保存する
  • 最後のページまでおなじ動作を繰り返す

この流れでやっていきます。


結果

さきに結果からお見せしたいと思います。こちらが「東ロボくん 偏差値」で検索した結果です。コマンドライン上に、検索したキーワードで出てきた記事の共起語が取得されます。


そして、検索語句.txtというファイルが作成されるようになります。


環境

  • Python3.5
  • Windows Subsystem for LinuxでUbuntu環境

ライブラリとか

はじめにpipしておいてください。
  • Janome (Python用形態素分析ライブラリ)
  • Selenium (スクレイピングライブラリ)
  • nltk(自然言語処理のライブラリ)
Janomeは、日本語の形態素分析ライブラリです。インストールが楽なので、利用しています。ドキュメント等は下記参照。


Seleniumはもうこのブログで腐るほど解説しているので、過去の記事を読んでください。


実装

ということで、とりあえず完成したコードがこちら。

import sys
from selenium import webdriver
from selenium.webdriver.common.keys import Keys

from janome.tokenizer import Tokenizer
from janome.analyzer import Analyzer
from janome.tokenfilter import *

import re
import nltk


def filterText(text):
   """
   :param text: str
   :rtype : str
   """
   # アルファベットと半角英数と記号と改行とタブを排除
   text = re.sub(r'[a-zA-Z0-9¥"¥.¥,¥@]+', '', text)
   text = re.sub(r'[!"“#$%&()\*\+\-\.,\/:;<=>?@\[\\\]^_`{|}~]', '', text)
   text = re.sub(r'[\n|\r|\t]', '', text)

   # 日本語以外の文字を排除(韓国語とか中国語とかヘブライ語とか)
   jp_chartype_tokenizer = nltk.RegexpTokenizer(u'([ぁ-んー]+|[ァ-ンー]+|[\u4e00-\u9FFF]+|[ぁ-んァ-ンー\u4e00-\u9FFF]+)')
   text = "".join(jp_chartype_tokenizer.tokenize(text))
   text = text.replace(" ", "")
   text = text.replace(" ", "")
   return text

if __name__ == '__main__':
    browser = webdriver.Chrome(executable_path='/mnt/c/workspace/pydev/chromedriver.exe')#Selenium Chromedriverにパスを通す
    browser.get("https://www.google.co.jp/")
    print("[INFO]Googleにアクセスしました")
    query = ""
    argvs = sys.argv  # コマンドライン引数を格納したリストの取得
    if len(argvs) > 1:
        for argv in argvs[1:]: # 検索語句が複数の場合は、それを全部渡す
            query +=  argv + " "
    else:
        query = argvs[0]
    print("[INFO]「{}」で検索しています....".format(query))
    element = browser.find_element_by_id("lst-ib")
    element.send_keys(query) #クエリを入力
    element.send_keys(Keys.ENTER) #Enterキーを押下する
    print("[INFO]検索に成功しました")
    f = open("{}.csv".format(query), 'w') #共起語レポートの作成

    urlsList = [] #ここに変数を格納しないと覚えておいてくれない

    urls = browser.find_elements_by_css_selector("h3.r a")
    print(len(urls))
    for url in urls:
        urlsList.append(url.get_attribute("href"))

    print("[INFO]{}件の検索結果が見つかりました。".format(len(urlsList)))
    count = 1
    for url in urlsList:
        try:

            browser.get(url)
            print("-----------------------------------------------------------------------")
            print("[INFO]{}ページ目にアクセスしました".format(count))
            title = "[INFO]タイトル:{} :{}位".format(browser.title,count)
            f.write(title) # 引数の文字列をファイルに書き込む

            text = browser.find_element_by_css_selector("body").text
            print("[INFO]HTMLを取得しました。")
            print("---------------------------------------")
            filterdText = filterText(text)

            print(filterdText)
            print("---------------------------------------")
            token_filters = [POSKeepFilter(['名詞','動詞']), TokenCountFilter(att='base_form')]
            a = Analyzer(token_filters=token_filters)
            top_words = list(a.analyze(filterdText))[:20]
            print("[INFO]共起語を取得しています。")
            print("-----------------------------------------------------------------------")
            for name, count in top_words:
                output = "{} | {}".format(name,count)
                print(output)
                f.write(output)
            print("-----------------------------------------------------------------------")
        except Exception as e:
            "[ERROR] 共起語を見つけられませんでした。"
        count += 1
    f.close() # ファイルを閉じる    

では、コードの解説です。

import sys
from selenium import webdriver
from selenium.webdriver.common.keys import Keys

コマンドライン引数の受け取りでsysは使っています。あとはSeleniumをインポートしています。from selenium.webdriver.common.keys import Keys は、seleniumのsend_keysメソッドの引数に、Keys.ENTERという、キーボードでエンターを押すような動作ができるようになるライブラリなので入れています。

import re
import nltk

reは正規表現をPython上で扱うライブラリみたいです。nltkは自然言語処理のライブラリみたいですが、詳しくは分からないので、下の記事を参考にしてください。

def filterText(text):
   """
   :param text: str
   :rtype : str
   """
   # アルファベットと半角英数と記号と改行とタブを排除
   text = re.sub(r'[a-zA-Z0-9¥"¥.¥,¥@]+', '', text)
   text = re.sub(r'[!"“#$%&()\*\+\-\.,\/:;<=>?@\[\\\]^_`{|}~]', '', text)
   text = re.sub(r'[\n|\r|\t]', '', text)

   # 日本語以外の文字を排除(韓国語とか中国語とかヘブライ語とか)
   jp_chartype_tokenizer = nltk.RegexpTokenizer(u'([ぁ-んー]+|[ァ-ンー]+|[\u4e00-\u9FFF]+|[ぁ-んァ-ンー\u4e00-\u9FFF]+)')
   text = "".join(jp_chartype_tokenizer.tokenize(text))
   text = text.replace(" ", "")
   text = text.replace(" ", "")
   return text

これは、後でSeleniumで取得したテキストを、きれいにしてくれるメソッドです。日本語や記号などが入らないようにしてくれます。こちらを参考にしてください。

if __name__ == '__main__':
    browser = webdriver.Chrome(executable_path='/mnt/c/workspace/pydev/chromedriver.exe')#Selenium Chromedriverにパスを通す
    browser.get("https://www.google.co.jp/")
    print("[INFO]Googleにアクセスしました")
    query = ""
    argvs = sys.argv  # コマンドライン引数を格納したリストの取得
    if len(argvs) > 1:
        for argv in argvs[1:]: # 検索語句が複数の場合は、それを全部渡す
            query +=  argv + " "
    else:
        query = argvs[0]
    print("[INFO]「{}」で検索しています....".format(query))
    element = browser.find_element_by_id("lst-ib")
    element.send_keys(query) #クエリを入力
    element.send_keys(Keys.ENTER) #Enterキーを押下する
    print("[INFO]検索に成功しました")

ここからが、実行される処理です。

browser = webdriver.Chrome(executable_path='/mnt/c/workspace/pydev/chromedriver.exe')#Selenium Chromedriverにパスを通す
    browser.get("https://www.google.co.jp/")
    print("[INFO]Googleにアクセスしました")

Seleniumのパスを指定して、その後グーグルにアクセスします。

query = ""
    argvs = sys.argv  # コマンドライン引数を格納したリストの取得
    
        for argv in argvs[1:]: # 検索語句が複数の場合は、それを全部渡す
            query +=  argv + " "
    else:
        query = argvs[0]
    print("[INFO]「{}」で検索しています....".format(query))

コマンドラインから、検索語句を受け取ります。この際、argvはリスト形式になっており、最初に実行ファイルの名前(cooccur.py)が格納されています。

if len(argvs) > 1:

なので、引数の数が1個以上なのであれば、

for argv in argvs[1:]: # 検索語句が複数の場合は、それを全部渡す
            query +=  argv + " "

検索語句をqueryに空白とともに追加していきます。

 print("[INFO]「{}」で検索しています....".format(query))
    element = browser.find_element_by_id("lst-ib")
    element.send_keys(query) #クエリを入力
    element.send_keys(Keys.ENTER) #Enterキーを押下する
    print("[INFO]検索に成功しました")

その後、Seleniumのsend_keysメソッドで、グーグルの検索フォームをIDで取得し、検索語句を入れ、エンターを押します。そうすると検索が始まります。

f = open("{}.csv".format(query), 'w') #共起語レポートの作成

    urlsList = [] #ここに変数を格納しないと覚えておいてくれない

    urls = browser.find_elements_by_css_selector("h3.r a")
    print(len(urls))
    for url in urls:
        urlsList.append(url.get_attribute("href"))

    print("[INFO]{}件の検索結果が見つかりました。".format(len(urlsList)))
    count = 1

共起語検索の結果をレポートにまとめたいので、

f = open("{}.text".format(query), 'w') #共起語レポートの作成

で、テキストファイルを作成します。

urlsList = [] #ここに変数を格納しないと覚えておいてくれない

    urls = browser.find_elements_by_css_selector("h3.r a")
    print(len(urls))
    for url in urls:
        urlsList.append(url.get_attribute("href"))
検索結果の上位10ページのURLを取得し、urlslistに格納していきます。ちなみにこのような処理をしているのは、

urls = browser.find_elements_by_css_selector("h3.r a")

このurlsの値がページ遷移とともに保存されないからです。


ここから、さっそく記事の共起語を取得します。

print("[INFO]{}件の検索結果が見つかりました。".format(len(urlsList)))
    count = 1
    for url in urlsList:
        try:

            browser.get(url)
            print("-----------------------------------------------------------------------")
            print("[INFO]{}ページ目にアクセスしました".format(count))
            title = "[INFO]タイトル:{} :{}位".format(browser.title,count)
            f.write(title) # 引数の文字列をファイルに書き込む

            text = browser.find_element_by_css_selector("body").text
            print("[INFO]HTMLを取得しました。")
            print("---------------------------------------")
            filterdText = filterText(text)

            print(filterdText)
            print("---------------------------------------")
            token_filters = [POSKeepFilter(['名詞','動詞']), TokenCountFilter(att='base_form')]
            a = Analyzer(token_filters=token_filters)
            top_words = list(a.analyze(filterdText))[:20]
            print("[INFO]共起語を取得しています。")
            print("-----------------------------------------------------------------------")
            for name, count in top_words:
                output = "{} | {}".format(name,count)
                print(output)
                f.write(output)
            print("-----------------------------------------------------------------------")
        except Exception as e:
            "[ERROR] 共起語を見つけられませんでした。"
        count += 1
    f.close() # ファイルを閉じる    

取得した記事のURLに一つ一つアクセスしていきます。

text = browser.find_element_by_css_selector("body").text
            print("[INFO]HTMLを取得しました。")
            print("---------------------------------------")
            filterdText = filterText(text)
ページにアクセスして、HTMLのbody要素をスクレイピングします。そして、さきほどのfilterTextメソッドに、そのスクレイピングしたbody要素のテキストをきれいにします。

token_filters = [POSKeepFilter(['名詞','動詞']), TokenCountFilter(att='base_form')]

ここはJanomeのメソッドなのですが、POSKeepFilter の引数には、リスト形式で取得したい品詞を選択できます。また、TokenCountFilter(att='base_form')では、基本形に変換したものを取得することができるようになります。

a = Analyzer(token_filters=token_filters)
            top_words = list(a.analyze(filterdText))[:20]
            print("[INFO]共起語を取得しています。")
            print("-----------------------------------------------------------------------")
            for name, count in top_words:
                output = "{} | {}".format(name,count)
                print(output)
                f.write(output)
            print("-----------------------------------------------------------------------")
        except Exception as e:
            "[ERROR] 共起語を見つけられませんでした。"

正直、こっからはなんとか作ったので、ロジックをうまく説明できるほど理解していないのですが、

top_words = list(a.analyze(filterdText))[:20]
こちらで取得した共起語で、最も多い20個をリスト形式で取得します。

for name, count in top_words:
                output = "{} | {}".format(name,count)
                print(output)
                f.write(output)

で、そのリストを単語と頻度を一つずつテキストファイルに追加していきます。結構な割合で失敗するので、失敗するとExceptionにはいります。

ということで、これをすべてのページで取得したのちに、

  f.close() # フ
でテキストファイルを保存し終了します。


課題


作ってみたものの、まだ課題が残ります。

  • 「する」などの意味を伴わない動詞の取り扱い
  • 内容語だけ取得する方法
  • 原因不明のlist indexエラー

ここを解決しなければ、実用段階までにはいきません。手伝ってくれる方はツイッターからご連絡いただけると幸いです。





注目の投稿

 PythonのTweepyを利用して、Twitter APIを利用している。 その中で、ハマったポイントをメモしておく。 まず、Searchに関して。 Twitter検索は、クライアントアプリ側では、全期間の検索が可能になっている。 一方で、APIを利用する際は、過去1週間しか...