Python + Scrapy ではてブ情報を取得してみました

PyLaides Advent Calendar 2017 の5日目の記事です。





以前、ちょこっと行ってみたPyLaidesのイベントが楽しかったので、
Advent Calendarに参加させていただきました。

12/10の勉強会にも参加させていただく予定なので、行かれるかたはよろしくお願いします。





Python歴は浅くてまだまだ未熟なのですが、
今回はScrapyを使ったスクレイピングをやってみたのでそのことについて書きます。

Scrapyの基本的な使い方はこちらにまとめました。



mtomitomi.hatenablog.com





ここでは上の記事に加えて、取得した記事の投稿日付・タイトル・URLを抜き出して
csvに出力するところまでやります。






Itemの作成



Spiderを作成したあと、雛形として作成されているitems.pyを修正します。
Itemクラスはスクレイピングをしたデータを取得するための箱になります。
今回は、投稿日付・タイトル・URLを取得したいので、
date・name・link を追加しました。



items.py




import scrapy

class FirstScrapyItem(scrapy.Item):
date = scrapy.Field()
name = scrapy.Field()
link = scrapy.Field()
pass






CSV出力にする設定を追加



scrapy実行結果をCSVで出力するよう、setting.pyに下記設定を追加します。
FEED_URIは適当に出力先を指定してください。
ちなみにFEED_URIプレースホルダーにすることも可能です。



setting.py




FEED_FORMAT = 'csv'
FEED_URI = '/Users/Python/result.csv'






Spiderを実装



条件を指定してスクレイピングでデータを収集し、itemオブジェクトに格納します。
XPATHで条件を指定しようとしたのですが、指定方法が悪かったせいか上手く収集できず…cssを使うことにしました。

XPATHだと階層を調べたり手間がかかりましたがcssだとダイレクトに指定できるので、個人的にはcssのほうが使いやすいかったです。

cssの書き方については下記サイトがわかりやすかったです。



scrapyでよく使うxpath, cssのセレクタ | Python Snippets





また、cssで取得したデータをitemオブジェクトに格納するときは、extract()でなくextract_first()を使いました。
こうすることで、指定した要素が見つからなかった場合にIndexErrorにならずに、Noneが返されるようにしています。



hatena.py




# -*- coding: utf-8 -*-
import scrapy
import sys
sys.path.append("/Users/Python/first_scrapy")
from first_scrapy.items import FirstScrapyItem

class HatenaSpider(scrapy.Spider):
name = 'hatena'
allowed_domains = ['b.hatena.ne.jp']
start_urls = ['http://b.hatena.ne.jp/ctop/it']

def parse(self, response):

# entry-contentsごとにデータを取得
for sel in response.css('div.entry-contents'):
# 不要なデータが混ざることがあるので不要データは排除
if sel.css('li.date::text').extract_first() is None :
continue

item = FirstScrapyItem()
# 条件を指定してitemオブジェクトに格納
item['date'] = sel.css('li.date::text').extract_first()
item['name'] = sel.css('a.entry-link::text').extract_first()
item['link'] = sel.css('a::attr(href)').extract_first()

# Itemオブジェクトを返却
yield(item)





ちなみに、Itemクラスをimportするところで少しハマりました。

普通に from first_scrapy.items import FirstScrapyItem と書くと下記のようなErrorになってしまいました。



ImportError: No module...





原因としては、ItemクラスはこのSpiderクラスよりも上位階層にあるので、
ディレクトリ先を指定できずにErrorになっているようです。(下の階層はこのまま指定できます)
なので sys.path.append() を使ってパスを通したら解決しました。






Spiderを実行



runspiderコマンドを実行します。



$ scrapy runspider --nolog first_scrapy/spiders/hatena.py





指定したフォルダにCSVファイルが作成され、欲しいデータを取得することができました。



result.csv



date,link,name
2017/12/03 14:24,https://anond.hatelabo.jp/20171203141020,深センをディストピアとかいう記事バズってるけど
2017/12/03 07:30,http://tech.mercari.com/entry/2017/12/03/070000,技術書を作るための技術スタック - Mercari Engineering Blog
2017/12/03 05:45,https://applech2.com/archives/20171203-month-13-is-out-of-bounds-bug-on-high-sierra.html,macOS High Sierraに2017年12月を過ぎると「Month 13 is out of bounds」と...
2017/12/03 12:33,http://mztn.hatenablog.com/entry/2017/12/03/122429,Web系企業に転職して最高だったという話をしたい - ある研究者の手記
2017/12/03 11:39,https://qiita.com/i_yudai/items/3336a503079ac5749c35,Goroutineハンターが過労死する前に - Qiita
2017/12/02 19:19,https://jmatsuzaki.com/archives/22006,タスクを5つに分類すればもっとうまく時間を管理できるようになる | jMatsu...
2017/12/03 13:21,https://anond.hatelabo.jp/20171203130828,定額課金制のスマホげーってないの?
2017/12/03 14:59,http://nmi.jp/2017-12-03-Profiling-of-huge-wasm,巨大 WebAssembly ファイルのコンパイル時間
2017/12/03 06:53,http://jp.wsj.com/articles/SB11567225428262983324604583551083272836170,ビットコイン、最もホットな通貨だが誰も使わない - WSJ
2017/12/03 16:05,https://qiita.com/ninomiyt/items/fb2ac63f195d0b9a4c7d,ちょっとしたツールを作るのに便利なPythonライブラリ - Qiita
2017/12/03 14:24,http://co-world.me/2017/12/03/post-338/,【全32冊】Web業界の著名人が #Sarahah でおすすめしていたビジネス本をまと...
2017/12/03 12:34,http://netafull.net/iijmio/056560.html,【IIJmio】SIM再発行の手続きから届くまで
2017/12/03 12:20,http://www.bbc.com/japanese/42212873,トランプ氏、フリン被告の行動「合法」 「FBIに嘘」は知っていたと - BBCニ...
2017/12/03 11:56,https://kadenkaigi.com/entry/350796386,話題の独立型イヤホン、ソニー「WF-1000X」の音途切れ問題はアップデートで...
2017/12/02 20:22,https://kadenkaigi.com/entry/350699891,ラズパイと完全互換で低価格なのに高性能でAndroidもサポートするシングルボ...
2017/12/02 12:15,https://kadenkaigi.com/entry/350674753,年末年始休暇にはAlexaとFurbyを合体させる神をも恐れぬDIYプロジェクトに挑...
2017/12/02 11:04,https://kadenkaigi.com/entry/350671775,あの「ストームトルーパー」がロボットに!ARゲーム搭載で約4万円 | Mogura ...
2017/12/03 19:08,https://kadenkaigi.com/entry/350828324,ASCII.jp:人気ASUS格安スマホ「ZenFone 3」が100円に! 3万8000円引きの大...
2017/12/03 02:37,https://kadenkaigi.com/entry/350738157,ASCII.jp:この茶色いスロットは何ですか? Socket 478マザーが11年ぶりに再販
2017/12/02 16:58,https://kadenkaigi.com/entry/350689200,人気のヨドバシ福袋「夢のお年玉箱」今年は抽選に アクセス集中を回避 - IT...
2017/12/02 16:20,https://kadenkaigi.com/entry/350687223,サムスン次期モデルGalaxy S9/S9+は来年1月に発表、画面埋込み型指紋センサ...






jupyter notebook で出力



せっかくなので、jupyter を使って出力させてみました。

下記を参考にして同じ手順で実行しました。



http://www.jitsejan.nl/using-scrapy-in-jupyter-notebook.html






1. InteractiveShellなどをimport



実行するとpythonのバージョンが出力されます。



# Settings for notebook
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
# Show Python version
import platform
platform.python_version()






2. Scrapyのimport



実行するとダウンロードが開始します。Successfullyと出力されたら成功です。




try:
import scrapy
except:
!pip install scrapy
import scrapy
from scrapy.crawler import CrawlerProcess






3. jsonファイルに書き込むための設定



import json

class JsonWriterPipeline(object):

def open_spider(self, spider):
self.file = open('quoteresult.jl', 'w')

def close_spider(self, spider):
self.file.close()

def process_item(self, item, spider):
line = json.dumps(dict(item)) + "\n"
self.file.write(line)
return item






4. Spiderの実装



先ほど実装した内容と同じですが、下記2点を修正しました。




  • jsonで出力させるために custom_settings を追加


  • Itemオブジェクトが使えなさそうだったので取得したデータをディクショナリとして返却





import logging

class HatenaSpider(scrapy.Spider):
name = 'hatena'
allowed_domains = ['b.hatena.ne.jp']
start_urls = ['http://b.hatena.ne.jp/ctop/it']

custom_settings = {
'LOG_LEVEL': logging.WARNING,
'ITEM_PIPELINES': {'__main__.JsonWriterPipeline': 1},
'FEED_FORMAT':'json',
'FEED_URI': 'quoteresult.json'
}

def parse(self, response):
for sel in response.css('div.entry-contents'):
if sel.css('li.date::text').extract_first() is None :
continue

yield{
'date': sel.css('li.date::text').extract_first(),
'name': sel.css('a.entry-link::text').extract_first(),
'link': sel.css('a::attr(href)').extract_first(),
}






5. Scrapyを実行



scrapy crawl コマンドを使うことで、API を利用してスクリプトからScrapyを実行することができるようです。

下記を実行するとログと<Deferred at 〜 >という文字が出力されます。



process = CrawlerProcess({
'USER_AGENT': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)'
})

process.crawl(HatenaSpider)
process.start()






6. 作成されたjsonファイルを確認



下記を実行すると quoteresult.jl、quoteresult.json が作成されていることがわかります。
quoteresultというファイル名は、Spiderで設定した名前です。



ll quoteresult.*






7. jsonファイルの中身を確認



日本語がユニコード文字になって出力されますが、あとでちゃんと出力されるので大丈夫です。



!tail -n 2 quoteresult.jl






!tail -n 2 quoteresult.json






8. 収集データを出力



Pandasを使用してjsonファイルの内容を出力します。



import pandas as pd
dfjson = pd.read_json('quoteresult.json')
dfjson





出力できました。



f:id:mtomitomi:20171204214947p:plain:w2000





jlファイルを出力する際は下記コマンドを実行するようです。



dfjl = pd.read_json('quoteresult.jl', lines=True)
dfjl






9. 出力結果をpickle化



pythonのオブジェクトをバイトストリームに変換することをpickle化というようです。
その逆でバイトストリームを元のオブジェクトに戻すように変換することは、非pickle化というようです。
pickleモジュールを使うことで、リストや辞書などの複雑なデータ型をファイルに保存することができるようになります。



dfjson.to_pickle('quotejson.pickle')
dfjl.to_pickle('quotejl.pickle')






10. pickle化したファイルを確認



quotejl.pickle と quotejson.pickleが作成されたことを確認できました。



ll *pickle






やってみた感想



Scrapyを使うととても簡単にWebサイトの情報を取得することができました。
Jupyterで綺麗に出力されると楽しさが増します。
あとは、他のサイトの情報も取得したり、いまのトレンドを分析とかしてみたいと思っています。

ちなみにJupyter に出力させたときに、URLをクリックしたらすぐそのページにいけるようなリンク表示にしたかったのですが、方法がわかりませんでした。

これからもっとJupyterやらPythonの勉強をしていきたいと思っていますので、よろしくお願いいたします。