Hatena::ブログ(Diary)

I* HACK! ウェブ関連のタレコミ

2017-05-12

Scrapyでサイトをクロールし、ElasticSearchでサイトの概要を把握する (3)

16:49

前回までで、Scrapyでクロールしたページ情報をElasticSearchにインデックスするところまで設定したが、

実はあれだけだと、うまくESやkibana上で検索できなかったりする。

デフォルトではDynamic Mappingといって

データをPOSTすれば勝手にフィールドなどを作ってくれるが、url文字列をうまく扱ってくれなかったり、細かい

データ型の指定や、analyzerの指定ができない。

https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-mapping.html


なので、scrapyから送る属性をelasticsearchにマッピングしてあげる。


今回は、kibanaのdev toolを使って下記のようなマッピングを行った。

#一度インデックスを削除
DELETE scrapy-2017

#マッピングルールを設定
# - タイトルやdescriptionは日本語解析ができるように
# - URLはngramでなど
PUT scrapy-2017
{
  "settings": {
    "index": {
      "analysis": {
        "tokenizer": {
          "ja_text_tokenizer": {
            "type": "kuromoji_tokenizer",
            "mode": "search"
          },
          "ngram_tokenizer": {
            "type": "ngram",
            "min_gram": 2,
            "max_gram": 2,
            "token_chars": [
              "letter",
              "digit",
              "punctuation",
              "symbol"
            ]
          }
        },
        "analyzer": {

          "ja_text_analyzer": {
            "tokenizer": "ja_text_tokenizer",
            "type": "custom",
            "char_filter": [
              "icu_normalizer"
            ],
            "filter": [
              "kuromoji_part_of_speech"
            ]
          },
          "ngram_analyzer": {
            "tokenizer": "ngram_tokenizer"
          }
        }
      }
    }
  },
  "mappings": {
    "crawledpage": {
      "dynamic": "strict",
      "properties": {
        "title": {
          "type": "text",
          "analyzer": "ja_text_analyzer"
        },
        "description": {
          "type": "text",
          "analyzer": "ja_text_analyzer"
        },
        "url": {
          "type": "text",
          "analyzer": "ngram_analyzer",
          "copy_to": "url_raw"
        },
        "path": {
          "type": "keyword",
          "index": "not_analyzed",
          "ignore_above": 256
        },
        "host": {
          "type": "keyword",
          "index": "not_analyzed",
          "ignore_above": 256
        }, 
        "referer": {
          "type": "text",
          "analyzer": "ngram_analyzer"
        },
        "word_count": {
          "type": "integer"
        },
        "status": {
          "type": "integer"
        }
      }
    }
  }
}

これでkibanaなどで適切に検索ができるようになった。

Scrapyでサイトをクロールし、ElasticSearchでサイトの概要を把握する (2)

16:09

Vol1の続き。


今回はScrapyでクロールし、その結果をElasticSearchにインデックスするところまで。


Scrapyのプロジェクトを作成

$ scrapy startproject scrapy_tutorial

ElasticSearchとの連携設定

$ cat scrapy_tutorial/settings.py
ITEM_PIPELINES = {
    'scrapyelasticsearch.scrapyelasticsearch.ElasticSearchPipeline': 500
}

ELASTICSEARCH_SERVERS = ['localhost']
ELASTICSEARCH_INDEX = 'scrapy'
ELASTICSEARCH_INDEX_DATE_FORMAT = '%Y'
ELASTICSEARCH_TYPE = 'items'
ELASTICSEARCH_UNIQ_KEY = 'url'  # Custom uniqe key

実はそんなに接続設定は多くなかったりする。

気にしないといけないのは、インデックスのフォーマットぐらい。

今回は、年ごとにインデックスを作っていく前提でインデックス名を設定した

(例えば、scrapy-2017, scrapy-2018.....)


Scrapyのコーディング

今回は、CrawlSpiderを使ってクローラを作成。

Python初めてだし、日本語の情報も少なく、細かい設定しようとするとはまって結構時間かかりました。

#基本的にはLinkExtractorのコールバックでElasticSearchへ送るITEMを作成
#それ以外は割愛・・
def parse_items(self, response):
....
       return item


#items.py には下記属性を定義
#pathには、urlのディレクトリパスを第二階層まで格納→各階層ごとのページボリュームを計算したかったため
    url = scrapy.Field()
    host = scrapy.Field()
    path = scrapy.Field()
    referer = scrapy.Field()
    title = scrapy.Field()
    description = scrapy.Field()
    word_count = scrapy.Field()
    status = scrapy.Field()

scrapyを実行して、elasticsearchにインデックスされるか確認

$ scrapy crawl myspider
$ curl -XGET 'localhost:9200/scrapy-2017/_search'
#クロールされたページ情報が表示されれば成功

Scrapyでサイトをクロールし、ElasticSearchでサイトの概要を把握する (1)

15:48

サイトリニューアルなどで、既存のサイト状況を把握するためのツールとして

サイトをScrapyでクロールし、その統計情報などをElasticSearchにインデックスしてkibanaで解析することを思いついたので、

やってみた。

CentOS6へセットアップします


ElasticSearch のインストール

今回は最新の5.x系を利用した。

# rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch
# vi /etc/yum.repos.d/elasticsearch.repo
[elasticsearch-5.x]
name=Elasticsearch repository for 5.x packages
baseurl=https://artifacts.elastic.co/packages/5.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md

# yum search java | grep 1.8.0-openjdk
# yum install java-1.8.0-openjdk
# alternatives --config java
# service elasticsearch start

動作確認

こんなのがかえってきたらOK

$ curl -XGET 'localhost:9200/?pretty'
{
  "name" : "UPusCPa",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "YRaDwsW4SIG0SraDDKMPTQ",
  "version" : {
    "number" : "5.2.1",
    "build_hash" : "db0d481",
    "build_date" : "2017-02-09T22:05:32.386Z",
    "build_snapshot" : false,
    "lucene_version" : "6.4.1"
  },
  "tagline" : "You Know, for Search"
}

日本語解析用プラグインインストール

コマンド一発でインストールしてくれて便利。

$ cd /usr/share/elasticsearch
#kuromoji 日本語形態素解析エンジン
$ sudo bin/elasticsearch-plugin install analysis-kuromoji
# 大文字小文字全角半角の正規化ができるプラグイン
$ sudo bin/elasticsearch-plugin install analysis-icu

#プラグインを有効化
$ sudo service elasticsearch restart

#設定ファイルの場所はここ
/etc/elasticsearch/elasticsearch.yml

クロールした情報を可視化するためにKibanaをインストール

# vi /etc/yum.repos.d/kibana.repo
[kibana-5.x]
name=Kibana repository for 5.x packages
baseurl=https://artifacts.elastic.co/packages/5.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md

$ sudo yum install kibana
$ sudo vi /etc/kibana/kibana.yml

server.host: "192.168.33.11"
~
elasticsearch.url: "http://localhost:9200"

$ sudo service kibana start

接続確認

http://192.168.33.11:5601/


python2.7のインストール

$ sudo yum install centos-release-scl-rh
$ sudo yum install python27

#有効化
$ scl enable python27 bash

#恒久的有効化
source /opt/rh/python27/enableを.bashrcに書いておく

Scrapy+ES接続用モジュールインストール

# pip install --upgrade pip //pip 7から9にアップグレードしないとエラーが出た
# pip install lxml
# pip install cryptography
# pip install ScrapyElasticSearch

これで一通り準備完了

2017-04-25

Scrapy のLinkExtractorとブラウザのリンク仕様

00:29

Scrapyでは

/ top.html

|----- subdir_a/index_a.html

|----- subdir_b/index_b.html

というディレクトリ構造があり、

top.html に、<a href="../../../../subdir_a/index_a.html">

とあった場合、正しいURLではないので、400エラーとなり、クロールができません。

一方で、最近のブラウザは頭がよくて、

ドキュメントルートトップ以上に遡れないリンクは、勝手にドキュメントルート直下にあるものと扱うようで、

上記のリンクは正しく動作するんです!


昔はそんなことなかったと思うんですがねぇ。。

2017-04-21

Wordpressの検索結果画面にスクリーンキャプチャを表示してみた

17:49

商用のサイト内検索サービスだと、よく検索結果画面にページのキャプチャが載っているかと思います。

それを簡単にWordpressで実現できました(落とし穴あり)

結論から言うと、Wordpressの非公式APIであるスクリーンショットを取得するサービスを利用するというものです。

コードはこんな感じ。

<img width="230" height="130" src="https://s.wordpress.com/mshots/v1/<?php echo esc_url( get_permalink(get_the_ID()) ); ?>?w=230" class="attachment-post-thumbnail size-post-thumbnail wp-post-image">

これで、検索結果画面で、キャプチャを表示してくれます。

f:id:go_nash:20170421175054p:image

おおー、ちょっとリッチな検索結果画面になった。

と思ったのもつかの間、wordpressのこのapiは、日本語フォントは対応していないんですね。

まあ字を細かく見るものでもないし、十分っちゃ十分か・・・

Googleサイトサーチ(カスタム検索)終了!チーン!

17:35

3月いっぱいで、Googleカスタム検索の有料版であるGoogleサイトサーチが新規申し込みを締め切りました。

https://enterprise.google.co.jp/intl/ja/search/products/gss.html


ちゃんとサイトを設計していればあまり迷うことなく、すなわち検索機能を使わないことも多いですが、

やはり最後の受け皿としてサイト内検索は欲しいところです。


そういった中で、Googleのサイトサーチは、年間$100〜と、広告のないサイト内検索を導入可能というとこで

重宝していたウェブ制作者の方々も多かったのではないでしょうか。


もしかするとGoogle代替サービスを出す可能性もありますが、

2018年4月以降は実質

  1. 広告の出るカスタム検索でがまんする
  2. 月額3-5万ほどするサイト内検索サービスを利用する
  3. CMSの検索機能を利用する

といった三択になってくるでしょう。

お手軽なサイト内検索がなくなったことで、逆にサービス開発のチャンスかもしれないですね。

2017-04-11

qTranslateX を使用してWordpressで作成されたサイトをクロールするとクロールできないページが出る件

14:34

https://qtranslatexteam.wordpress.com/browser-redirection-based-on-language/

によれば、URLに言語情報を含まない場合は、下記の値を元に判断しているようだ。

  • referrer url (if cookie is not set)
  • cookie (‘qtrans_front_language’)
  • browser setting (if main home page ‘/’)
  • default language (as set on Settings/Languages configuration page)

すこしビビったのが、referer urlを使っての言語判断が優先されていたということ。


つまり、下記のページがあったとして

http://example.com/en/about

そのあと、リファラを保持しながら下記URLにアクセスした場合

http://example.com/about/history

302リダイレクトで下記英語サイトにリダイレクトされてしまうということだ。

http://example.com/en/about/history

そして永遠にhttp://example.com/about/historyページはクロールされないことになる。。。

クローラーなどを作る際はお気をつけください。

2017-02-02

ansibleのlineinfile での落とし穴

16:30

bindの社内DNSのゾーン設定をansibleで自動化しようとしていたところ、


下記の記述だと、resolver 1のタスク しかincludeステートメントが追加されていなかった。


  - name: resolver 1
    lineinfile: dest={{namedconf}} insertafter="^###localhost_resolver" line='include "/etc/named.{{domain}}.zone";' state=present

  - name: resolver 2
    lineinfile: dest={{namedconf}} insertafter="^###internal_resolver" line='include "/etc/named.{{domain}}.zone";' state=present


どうも、lineinfileだと、lineの中身が同一だと、insertafterのマッチ文字列が異なっていても、そこにすでに、行がpresentなものとされ

二つ目は追加されないようです。


2つ目のline=includeの後ろにもう一個半角スペースを入れることで、二つとも追記できるようになりました。

line='include "/etc/named.{{domain}}.zone";'line='include "/etc/named.{{domain}}.zone"; ' ← これで1回目の文字列と異なるものとしてansibleに認識される