PeaceJet

証券会社で証券外務員をやりながら、マーケティングやデータ分析・UI/UX改善などを行っています。

【Python3】出会い系サイトをテキスト解析して見える化してみました。

こんにちは、PeaceJetです。

はじめに

以前から、興味があったデータ分析に挑戦しようと思います。
最近の流行といいましょうか、コンピューターによるデータ分析が盛り上がっているように感じますから、テンションを上げて行きたいと思います。

目的

  • 後学のため

目標

  • データを可視化し、そのデータから見られる傾向によって仮説を立てて検証する。

今回、この目標の決着点として、対象となるデータを「テキストマイニングすることで出現頻度を計測してWordCloudで可視化をする」こととしました。
※WordCloudとはAndreas Muellerさんが開発した熱盛なテキストが大きく表示されるプログラムです。
github.com

イメージとしては以下のようなものになります。

f:id:PeaceJet:20170907040001p:plain
出典:Andreas Mueller

データ収集

とにもかくにも、データがないと話になりません。
イトクローリングを行いHTMLファイルをダウンロードしながら、スクレイピングを行いデータを集めることにしました。

どのデータを使用するのか?

FX? 株?

馴染み深いデータではあるのですが・・・もう少し角度を変えたいという思いがあり、さらにデータを探すこととしました。

Twitter

次に思い浮かんだのは、Twitterでした。
しかし、タイムラインの分析では自分のフォローしている属性によって偏りが生じてしまいます。
特に、どんな言葉が頻繁に使用されているのかが容易に予想できてしまうことが問題だなとおもったので辞めました。

馬?

次に、競馬サイトとかも良いなと思ったのですが、こちらは言葉というより数字が主体ですので諦めました。

いろいろと探してみた結果・・・。

出会い系サイトはどうだろうと思うに至りました。

早速、Google先生に質問を投げかけることに。

「出会い系」
「出会い 掲示板」

上記のような検索ワードで、検索しました。

広告のほか、様々なサイトが運営されていることがわかります。

多くの候補がある

候補はたくさんありました。
探していくうちに、世界では単純に男性×女性が出会いを求めているわけではないという事実に気が付きました。

同性同士も出会いを求めている

つまり、男性から男性や女性から女性も出会いを求めているという事実です。
データとしては、あまり取られたことのないものではないかと思い興味を持ちました。
そこで、今回は「男性から男性」というフィールドを扱ったサイトを対象としました。

好みによってカテゴリが別れる

しかし、ここへ来てカテゴリが別れることに気が付きます。
男性だからといって男性の姿をした男性との出会いを欲している人もいる一方で、女性の姿をした男性との出会いを求めている人もいるということです。

注意事項

※これより先は、下記のことを十分に念頭においてから、読み進めてください。

免責事項

  1. ここからは、刺激の強い内容になります。
  2. 対象のサイトは伏せさせていただきます。
  3. 同性同士の話題を取り扱っています。
  4. 著者は、あくまでもデータによる可視化を目的としています。
  5. この記事で発生したいかなる損失について、筆者は責任を負いません。
  6. 筆者の判断により、記事を削除または修正する可能性があります。
  7. ソースコードは動作チェックをしておりますが、記事に誤りが含まれる可能性があります。

スクレイピングをする際には注意事項があります。
qiita.com

参照してされると良いかと思います。

データ収集の方法

今回は、GoogleAppsScriptでサイトをスクレイピングして、定期的にクローリングすることとしました。
なぜ、データベースを使わなかったかといいますと、常時、起動しておけるコンピューターがなかったことが背景にあります。
Raspberry Piを使って、Crontabで定期実行しながらデータを蓄積しようとも考えましたが、GoogleAppsScriptとGoogleスプレッドシートの方が早そうだったので、そちらにしました。

クローラーの動作

  1. 12時間毎にデータを収集し、時系列として欠損のないようにデータを収集する。
  2. 一度のクローリングで、5ページまでのHTMLファイルをダウンロードする。
  3. ダウンロードしたHTMLファイルをスクレイピングしてデータを抜き出す。
  4. データをGoogleスプレッドシートに格納する。

GoogleAppsScriptのプログラム

実際に使用しているソースコードではなく、以下のようなメソッドを使用して取得しているというふうに捉えて頂ければと思います。

//URL
function getSource() {
  
  //シート名はランダムに設定する。
  //var rand = 's9mxeb9ccf3s';
  //var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(rand);
  
  for (var p = 1; p <=5; p++) {
    
    var url = "WEBページのURL" + p;
    var response = UrlFetchApp.fetch(url);
    var split = response.getContentText().split('<hr>');
    
    for ( var i = 1; i < split.length; i++ ) {
      
      sheet.appendRow([split[i]]);
      
    }
    
    Utilities.sleep(3000);
    
  }
  
}

解説

  1. UrlFetchApp#fetchメソッドにて、WEBページのソースを取得します。
  2. hrから次のhrタグまでが一つのブロックになっているので、splitメソッドを使用して要素を配列に格納します。
  3. 最後にSheet#appendRowメソッドでシートの1列目・最終行に追加していきます。

※サーバーへの負荷を考えて、3秒位に一回WEBページを取得するようにしています。1秒以上の間隔を設けるのが常識のようです。

スクレイピング

取得したテキストデータとHTMLソースを切り分けます。
何度か工程を踏んでテキスト部分を抽出します。

ここでは、一部紹介いたします。

< br />や< br >の表記の揺れを統一する。

function breakOptimization () {

  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var LastRow = sheet.getLastRow();
  
  //<br>を最適化する
  for (var i = 0; i < LastRow; i++) {
    //一列目のデータを編集する。
    var getString = sheet.getRange(i + 1, 1).getValue();
    var setString = getString.replace(/<br \/>|<br>/g,"<br>");//g(Global)を指定することで、全ての<br>をreplaceする。
    sheet.getRange(i + 1, 1).setValue(setString);
  }
  
}

抽出したテキスト

抽出したテキストの一部を以下に掲載します。
※リンク先はモザイク加工がされておりません。
※全画面で表示されるので、誰も見ていないところで開けるのが懸命です。
ポップアップウインドウで別の画像を表示する方法を知っている方がいらっしゃいましたら教えてください。



MeCabを使用して単語の出現頻度を計測する

ここからは取得したデータに、どのような単語が出現するのか、出現頻度を計測してみたいと思います。

文章を一行にする

データはフィールド(セル)に入っているのですが、それらを一度全て繋げて一行にするという処理を行います。
プログラムはPython3で書きます。

#!/usr/bin/python
#-*- coding: utf-8 -*-

import re
import MeCab
import random

from collections import Counter

def optimizing_data():
    
    myArray = []
    
    '''
       データベースにあるデータをtext.txtというファイルに出力してあると仮定します。
    '''
    file = open("text.txt", "r", encoding="utf-8")
    
    for line in file:
        '''
            性別・年齢・地域・スペース・改行を削除する。
        '''
        pattern = re.compile( r'\W*:' )
        reg = pattern.search( line )
        
        if line in { "\n" }:
            continue    
        elif reg:
            continue
        else:
            xA = line.rstrip('\r\n\r\n')
            yA = xA.rstrip('\r\n')
            myArray.append(yA.rstrip('"'))
    
    file.close

    return "".join(myArray)

def output_textfile(txt):
    OutputFile = open('words.txt', 'w') # 解析結果を書き出すファイルを開く
    OutputFile.writelines(txt)
    OutputFile.close
    
#ここから、形態素解析 MeCab
#tagger = MeCab.Tagger('mecabrc')
#mecab_result = tagger.parse("".join(myArray))

#OutputFile = open('mecab_result.txt', 'w') # 解析結果を書き出すファイルを開く
#OutputFile.write(mecab_result) # 読み込んで解析して書き出し
#OutputFile.close
def extract_word(text):

    tagger = MeCab.Tagger('-Ochasen')
    node = tagger.parseToNode(text)
    word_list = []
    
    while node:
       #if node.feature.split(",")[0] in ("名詞", "動詞", "形容詞"):
       if node.feature.split(",")[0] in ("名詞"):
           word_list.append(node.surface)
       node = node.next
    return word_list

def make_histogram(word_list):
    return Counter(word_list)

def print_map_to_csv(m):
    myArray = []
    for key, value in m.items():
        myArray.append((str(key)  + "\n") * value)
        #myArray.append(str(key) + "\t" + str(value) + "\n")
    return myArray

def output_file(arr):
    OutputFile = open('mecab.txt', 'w') # 解析結果を書き出すファイルを開く
    OutputFile.writelines(arr)
    '''
    with OutputFile as f:
        for v in arr:
            f.writelines(v)# 読み込んで解析して書き出し
    '''
    OutputFile.close

def shuffle(text):
    return random.shuffle(text)   
    
if __name__ == "__main__":
    
    targetText = optimizing_data()
    word_list = extract_word(targetText)
    histogram = make_histogram(word_list)
    arr = print_map_to_csv(histogram)
    #shuf = shuffle(arr)
    output_file(arr)
    
    '''output_textfile(targetText)
    with open("lyrics.txt", "r") as f:
            text = f.readline()
    '''

以下が、出力されたテキストデータになります。

内容
内容
内容
内容
内容
内容
内容
内容
内容
内容
内容
内容
内容
内容
内容
内容
内容
内容
内容
内容
変態
変態
変態
変態
変態
変態
変態
変態
変態
変態
・・・・・

頻出する単語(名詞)をカウントし、そのカウントした数だけ繰り返してテキストデータに出力しています。
この後、テキストファイルに書かれた書かれた単語群をシャッフルします。

WordCloudの前処理

# -*- coding: utf-8 -*-

import random

def shuffle():

    myArray = []
   
    file = open("mecab.txt", "r", encoding="utf-8")

    for line in file:
        myArray.append(line)
    file.close

    random.shuffle(myArray) # ここで、シャッフルします。

    return myArray

def output_file(text):

    out = open("shuffled.txt", "w")
    out.writelines(text)
    out.close

if __name__ == '__main__':

    #print(shuffle())
    output_file(shuffle())
    print("finished")

これを行って、出てきたデータが以下のものになります。


趣旨
お礼
射精


童顔
アナル

新宿
ノンオペ
ハメ

むっちり


Sex

以下、57178行分のテキストが抽出されました。
しかしながら、このままだとWordCloudで上手く表示されませんでした。

github.com
なぜ、うまく行かなかったのかは上記に書かれていました。
解決策は、この単語群をシャッフルしてからでないといけないようです。

#cloud.generate_from_frequencies((("hi", 3),("seven", 7)))のようにしても良かったのですが、上手くいかなかった。

#!/usr/bin/env 
# -*- coding: utf-8 -*-
from wordcloud import WordCloud

def wordcloud():
    
    f = open('shuffled.txt', 'r')
    v = f.read()
    wordcloud = WordCloud(background_color="white", font_path="/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", width=1024, height=674).generate(v)
    wordcloud.to_file("./result.png")
    f.close()
    print("終了しました。")

if __name__ == '__main__':
    wordcloud()

抽出した結果としては、以下になります。
刺激が強すぎる可能性があるので、モザイク加工を行っております。
※リンク先はモザイク加工がされておりません。
※全画面で表示されるので、誰も見ていないところで開けるのが懸命です。
ポップアップウインドウで別の画像を表示する方法を知っている方がいらっしゃいましたら教えてください。



まとめ

※この記事は10月時点で執筆したものでして、ブログにどのようにして発表すればよいものか。
そもそも、必要なのか必要でないのかすら悩みに悩んで、ひっそりと掲載します。
なにかあったら、消すかもしれないです。
「メール」や「女装」といった言葉の出現頻度が非常に多かったです。
ここで、LINEやカカオといったSNSは一般的ではないということが分かりました。
やっぱり、匿名性が比較して高いメールが選ばれるということでしょうか。

/* ブログタイトルを取得 */