2017-12-24

個人で片手間サービス開発日記①プログラミングメンターサービスの要件定義

今までのプログラミング教育サービスの概要


- 今までのプログラミング教育サービスは、コーディングをWeb上で学べる(1)Web教材型(以下Web教材型)と、企業がプログラミングを教える(2)BtC教育型(BtC教育型)に分かれる

- Web教材型のサービスは例えばこんなものがある


Progate


・対象:無料で言語ごとのカリキュラムが存在する(フロントエンド~サーバーサイドまで)
・料金:課金(980円)払うとすべてのサービスが解放される
・対象:初心者むけ
・ポイント:あるレベルまでいくと、クラウドワークスから案件を取得できる


ドットインストール

・概要:無料でプログラミング言語を学べる(フロントエンド~サーバーサイドまで)
・対象:対象は初心者~中級者向け
・料金:月額980円
・ポイント:動画解説でかなりバラエティーがある


PyQ

・概要:機械学習、統計分析に特化したPython
・対象:初心者~中級者
・料金:2,980円 ~ 98,000円
 - PyQの利用:2980円
 - メールサポート込み:7,980円
 - メンター・チャットサポート:29,800円
 ー プレミアムサポート:98,000円
・ポイント:チャットで対応サービスが存在する(3万円ほど)



paiza

・概要:プログラミング学習からスキルをもとに転職まで支援してくれるサービス
・対象:中級者~上級者
・料金:
 ・ 1カ月プラン:880円
 ・ 6カ月プラン:680円
 ・ 12カ月プラン:600円



Web教材型をまとめると、こんな感じになる。




- BtC教育型については以下のサービスがある


TECHCAMP


・ 概要:カリキュラムとメンターをそろえたプログラミングスクールサービス
・ 料金:入会費用:128,000円、月額12,800円(初月無料)
・ 特徴:実際に受託したり、就活で使えるようになるようになる。質問し放題
・備考) 一括払い

TechAcademy


・概要:パーソナルメンターがフォローしてくれるプログラミングスクールサービス
・対象:初心者~中級者
・料金:79,800円から(月額15000円程度)
・備考)先生はクラウドソーシングで配置している、一括払い


CodeCamp

・概要:初心者でも一からプログラミングを学び、ECサイト等のwebサービスを開発できるスキルが身につきます。実際にPHPの最終課題ではオリジナルECサイトを制作するため、より実践的な技術習得ができます。



メンター紹介型サービス


ランサーズストリート

・Lancersがやってるサイト
・一般的なスキルシェアサービス

Pook

teacha
・メルカリのスキル売買サービス


CoconalaのWebサイトサービス

・日本のスキルシェアサービス


Code Mentor

・海外のメンターマッチングサイト(英語)
・価格:コースによる(25分3000円みたいな値段の場合もある)
・フリーランスエンジニアがメンターしていることがおおい

今までのプログラミング教育サービスの問題点


・ 完全にプログラミング初心者みたいな人が、本当に基礎的な知識をつけるサービスは存在するし、そこの需要と供給は満たされている


・しかし、プログラミング中級者(自分でサービスなどの成果物を作れる)になるための中級者向けのサービスは、かなり価格が高い(BtC教育型)

・ちなみに、プログラミング技術レベルの定義

- 定義
 ・プログラミング初心者:Progateで基礎的な学習ができ、プログラミングの基礎的な構文は理解できる
 ・プログラミング中級者:最低限動くものが作成でき、またそれでお金がもらえる成果物を作れる
 ・プログラミング上級者:拡張性やセキュリティ、動作等、大規模なシステム開発にも対応できる


・ プログラミング中級者になるためには、第三者のメンターが必要になる

 - 成果物を作成するには、何を学ぶべきか、エラーをどうやって解決するかなどのサポートは、知識がないためググれない

 - 【企業側】しかしそれをマネタイズしようとすると、企業側でも非常に価格が高くなってしまう。
 - 【顧客側】一括払い10万円以上がデフォで、月額1万円以上がデフォで、かつスクールしか存在しないので、始めるのが非常に困難

- メンターをやってくれる人 => 企業のインターンでは中級者であればよいのだが
 ・そもそも技術力がないとプログラマーとしては採用されず、ただのアルバイトになりかねない(オウンドメディアやら、テスターやら。)
・じゃあ安いサービスがあるか
 ・スキルシェア系の一部のサービスではあるが、プログラマーでない人のサービスが多いため、プログラミングにおける商品量が少ない
 ・またプログラミングに特化して作られているわけではないため、必須スキル等のエンジニアが欲しい情報が見られない




ユーザー想定

顧客想定

・1. 大学生

・Progateなどの無料サービスである程度のプログラミング知識はつくった
・できればエンジニア採用とかされたいし、そのためにインターンで成果物を見せられるようになればうれしい
・しかし実際でサービスを受注したり、インターンで選考を受けられるほどのアウトプットはない。試したけど、何から始めればいいかわからない。
・だからと言って数十万円のサービスにお金を支払うほどお金と時間に余裕がない

・2. 新卒エンジニア

・初級レベルの技術であればもう大丈夫
・しかし、もっと発展的なアウトプット(スクレイピング、自動テスト運用とか)
・だけどそこまでの業務に基づいた知識は、社内リソースに頼らざるを得ない

・3. 個人事業主

・初級レベルの技術であればもう大丈夫
・たとえば楽天のAPIを使って価格を毎日取得できるようなツールを作成したい
・だけどそこに至るまでの技術をどうやって身に着ければよいかがわからない
・本を探しても、どういう風にそれができるようになるのかわからない
・というかそれをやるために多大な時間をかけられないので、効率よく何を学べばそれができるのか教えてほしい

・4. 営業マン

・普段の退屈な業務を自動化したい
・だけどそこまでのことをすべて自分で勉強してって一からやるのもしんどい
・というかプログラミング初心者すぎてどこから始めたらいいかわからない

・5. 中級プログラマーの人

・Javaエンジニアで転職活動をしたい
・ある程度のプログラミングのスキルはある
・転職には成果物が必要
・今度はWeb系の企業に行きたいと考えている
・成果物を作るうえで、メンターが欲しい

メンター想定

・1. フリーランスの人

・普段は受託案件で稼いでいる
・だけど追加でもうちょっとお金が欲しい
・フリーランスになりたい人を応援したい

・2. Web系ベンチャーの人

・

・3. 大企業で開発している人

・仕事で高級取り
・副業したい
・普段も趣味でコードを書いている
・誰かのために立ちたい

採用想定(これはやるかわからない)

・インターンでエンジニア候補になるような学生のタレントプールが欲しい
・成果物が可視化されていれば、オファーも出しやすい

作りたいもの


・プログラミングのメンターとメンティーのマッチングサービス
・Code Mentorの日本語版


メンティーができること


・言語、作りたいものごとに検索できる(イメージ)
  ・検索結果は作りたいもの・サービス・技術
  ・プログラミング言語

・ユーザーのメンターのプロフィールページを閲覧できる
  ・企業名
  ・ここ最近のレスポンス数(日付で確認)
  ・使用できる言語スキル
  ・自己紹介
  ・関連SNS
  ・受託している案件の数
  ・評価
  ・販売しているサービス

・ユーザーはコースを閲覧できる
  ・

・ユーザーはコースをリクエストできる
  ・ダイレクトメッセージを送ることができる
  ・リクエストの結果、コースを作成する

・ユーザーはメンターに連絡を取ることができる
  ・ダイレクトメッセージを送信できる (Action Cable)

・ユーザーは商品を購入することができる
  ・メンティーはコースを購入することができる(Web Pay)

メンター


・ユーザーはコースを作成できる
  ・タイトル
  ・コースの説明
  ・requisite
  ・お気に入り数
  ・購入にあたってのお願い
  ・評価、感想
  ・ユーザーが制作した成果物(GITHUB)

・ユーザーはプロフィールを作成できる


機能


  • ログイン
    • Google
    • Twitter
    • Facebook
    • 普通のログイン
  • UI
    • Qiitaの色をパクる
    • Techっぽい色って緑だよね(適当)


ビジネスモデル


・仲介手数料(10%くらいかなぁ)
・理想的には、メンティーが制作したものから採用できる仕組みにする


#TODO

- 実際にフロントエンド作ってみて、気になるところを追加していく。

2017-12-23

プログラマーとして個人で月40万円稼ぐために必要そうなスキルセットを考えてみた

目的



今の給与が手取り35万くらいだが、全くスキルが身の丈にあってないと感じるので、なるべく早く身の丈に合うスキルをつけたい(30代になって死ぬから)

目標


そのスキルのKPIをとりあえず手取りと計算すると、少なくとも月40万円土日で働くことで稼げなければならない。


今月は受託開発ではじめて2万円稼いだので、これを来年に20倍にしたい。


あとこれをやることに対するメリットとして、転職の時の最低限の能力保証を実績で示せる。

戦略


ということで、これを実現するための戦略を考える。
以下の軸で考える

  • 単価:一件当たりの料金
  • 工数:一件当たりの工数
  • 必要なスキルセット:その案件を受託し納品するまでのスキルセット

案件

いろいろなクラウドソーシングの案件を見て見て、何を学ぶか決める。


クローラー系


【フロントエンド不要!】Airbnbスクレイピングツールの依頼/外注|サイト構築・ウェブ開発の仕事 [ID:1808998] 
https://crowdworks.jp/public/jobs/1808998?ref=apiv1


  • 1-5万円
  • スクレイピングもしくはAPIを利用して、サイトのデータを更新しつづける技術が必要そう
  • そう考えるとWebアプリケーションの開発経験がないとしんどい。
  • from scratchだったら普通に20日くらいは超える

アパレルサイトの在庫・画像・価格情報のクローニングツールの開発


  • 1-5万円
  • スクレイピングしてGoogle SpreadSheetにまとめる作業
  • たぶん一回きりなのでそこまで難しくない
メルカリのアラートツール


  • 1万円
  • スクレイピングしてその結果を何らかに通知するだけなので簡単そう

Bot開発系


「LINEBOT開発【ブランド商品への問い合わせ用】【Python】【LINE Message API】」 https://crowdworks.jp/public/jobs/1824195?ref=apiv1


Web開発系


家庭教師と生徒のマッチングサイト サイト構築・ウェブ開発の仕事の依頼

  • 5-10万円



育毛クリニック のホームページの制作 育毛の知識不要 原稿は100%出せますので仕事がやりやすいです。30ページ位 。デザイン能力があればさらに金額アップします。 


  • 20-30万円
  • 静的なページなので、Ruby on Railsでひな形作れば簡単につくれそう。
  • サーバー周りのことが理解できれば、わりと今の実力でできそう


【要お見積り!】アミューズメント業に特化した人材派遣会社のHP制作(4~5ページ程度)


  • 5-10万円
  • 静的なページなので、Ruby on Railsでひな形作れば簡単につくれそう。
  • サーバー周りのことが理解できれば、わりと今の実力でできそう
  • SEO対策が必要なので、新規に作ったRuby on RailsのSEO対策ができればいけそう

TODO


Web開発(特にコーポレート系)が一番よさそう(工数がどれくらいかかるかわからないが)受注のためにも、実際に成果物をつくっていなければならない。
自分の作ったサービスでマネタイズするのもよさそう。


Web言語は、日本で一番チュートリアルが充実しているRuby on Railsでとりあえず作ろう。プログラマーのメンターとメンティーのマッチングサイト作る。
Web案件を月2個くらいのペースで完成させて、かつツール作成で10万円くらいいけばかなり稼げそうではある。それまでの間は実績を稼ぐために、LancersかクラウドワークスでPython案件を高単価で受託しよう。月10万円Pythonで稼ぎながら、Web系の学習。

  • Ruby on Railsで個人サイトの作成
  • 自分のサービスのポートフォリオ作成
  • Pythonでツール販売

【Python x Selenium】Instagramでタグ検索し、取得した写真にすべていいねできる自動化ツールを作成しました


結論から言うと、Instagramで自動でいいねできるツールを作りました。
実装内容としては


  • ログインページで認証
  • 指定したタグで検索
  • 写真にいいね
  • 次へボタンが存在しなくなるまで永遠に指定したタグのいいねをし続ける

って感じです。




そもそもInstagramに何も興味なかった



普段全然興味なくて使っていないInstagram。
最後に使ったのは、浴衣姿の女子大生が見れると聞いてインストールしたとき以来でした。しかし実際に使ってみて、インストールした初月は水着美女や浴衣姿をたくさんと、女子大生のクッソどうでもいいスターバックスの投稿やらなんやらで、こんなくそアプリ穴を吹く紙にもならんなと思ったのでした。


Instagramに興味をもった理由



しかし、ここ最近再び興味を持ったのは、Instagramの自動ツールが割と需要があるという点でした。例えばLancersとかCloud Worksとか見ていると、ちらほらInstagramの自動化ツールの作成依頼が5-10万円くらいの単価で存在していたりとか。


あとは知り合いで実際にInstagramの自動化ツールを有料で利用している人がいたので、なるほどぉと思って興味を持ちました。


Instagramでマーケティングとは?


Instagramは結構マーケでも注目されているそうで、例えばこの辺の記事とか参考になりそうです




で、どういう風にInstagramをマーケティングで使っているんだろうなんて調べてみましたが、基本的には特定のタグで写真を投稿している人にいいねをすることで、集客を促進しているようですね。


例えば、たまたま#よみうりランドというコメントで検索した際に見つけた画像ですが、こんな感じです。




面白いですよね、よみうりランドに近いところのハッシュタグを利用して、そのように集客につなげるっていう戦法みたいですね。おそらくいいねすると近くにお店があることがわかるので、集客につながるんだと思います。


確かに、こうすると割と地域に応じたマーケとかもできそうで、よさそうですね。



商用化されているInstagramマーケティングツールを見ていると、


  • 特定のハッシュタグを選択
  • 定期実行プログラムにて、そのハッシュタグに対して投稿している人にたいしていいねを実行

というのが大半なようです。例えば、ハッシュライクというサイトでは、いいねを自動化させることができます。30日3600円で、指定したタグを設定し、定期実行でいいねをしてくれるようです。











ということで、この機能を今回は実装しました。


実装方法


この機能を実装するためには、当初二つ方法がありました。一つはInstagramの公式APIを利用してやる方法、もう一つはWebスクレイピングでごり押しする方法です。


前者を最初に試したのですが、どうやら2016年度にAPIの利用制限がかかり、自分の写真しか見れなくなるという、どうしようもないAPI仕様になってしまったようです。(Sandboxモードという、開発者向けお試し用)


これを無制限にするための審査が非常に面倒になりました。実際にAPIを利用したプロダクトを作成し、さらにその動画までつくってInstagramに送らなければいけないみたいです。しかもその審査もかなり厳しいようで、普通に利用するにはかなりハードルが高いようです。






できればこちらを利用したほうがよいのですが、ちょっと難しそうなので安定のSeleniumでごりおし作戦にしてみました。

環境


* Windows 10
* WSL
* Selenium
* Chrome Driver

ソースコード


usernameとpasswordを入れれば、おそらく動くかと。
日本語検索で少し困りましたが、URLのエンコーディング周りだったのが原因でした。
動くのですが、Webelementを複数取得したときに、Webelementの最初だけ取得する方法がわかりませんでした。わかる人がいらっしゃいましたら教えて下さい。

api.py

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import urllib.parse
import time

#Webdriver
browser = webdriver.Chrome(executable_path='/mnt/c/workspace/pydev/chromedriver.exe') #ここには任意のWebdriverを入れる

#URL

loginURL = "https://www.instagram.com/" #ログインする際のページ
tagSearchURL = "https://www.instagram.com/explore/tags/{}/?hl=ja" #.format()で{}の中の値を入れられるようになっている

#TagSearch

tagName = "よみうりランド" #タグの名前 #よみうりランド

#selectors
#ここには書くページのSelectorを選ぶ。x-pathもしくはcss selector

loginPath = '//*[@id="react-root"]/section/main/article/div[2]/div[2]/p/a' #xpath @https://www.instagram.com/
usernamePath = '//*[@id="react-root"]/section/main/article/div[2]/div[1]/div/form/div[1]/div/input' #xpath @https://www.instagram.com/
passwordPath = '//*[@id="react-root"]/section/main/article/div[2]/div[1]/div/form/div[2]/div/input' #xpath @https://www.instagram.com/

notNowPath = '//*[@id="react-root"]/div/div[2]/a[2]'

mediaSelector = 'div._e3il2' #表示されているメディアのwebelement @https://www.instagram.com/explore/tags/%E3%82%88%E3%81%BF%E3%81%86%E3%82%8A%E3%83%A9%E3%83%B3%E3%83%89/?hl=ja
likeXpath = '/html/body/div[3]/div/div[2]/div/article/div[2]/section[1]/a[1]'
nextPagerSelector = 'a.coreSpriteRightPaginationArrow' #次へボタン

#USER INFO

username = ""
password = "" 

#list

mediaList = []

#counter

likedCounter = 0

if __name__ == '__main__':

    #Login 
    browser.get(loginURL)
    time.sleep(3)
    browser.find_element_by_xpath(loginPath).click()
    time.sleep(3)
    usernameField = browser.find_element_by_xpath(usernamePath)
    usernameField.send_keys(username)
    passwordField = browser.find_element_by_xpath(passwordPath)
    passwordField.send_keys(password)
    passwordField.send_keys(Keys.RETURN)

    #Finished logging in. now at 
    time.sleep(3)
    encodedTag = urllib.parse.quote(tagName) #普通にURLに日本語は入れられないので、エンコードする
    encodedURL = tagSearchURL.format(encodedTag)
    print("encodedURL:{}".format(encodedURL))
    browser.get(encodedURL)

    #Finished tag search. now at https://www.instagram.com/explore/tags/%E8%AA%AD%E5%A3%B2%E3%83%A9%E3%83%B3%E3%83%89/?hl=ja
    time.sleep(3)
    browser.implicitly_wait(10)

    #写真を取得してクリックする

    mediaList = browser.find_elements_by_css_selector(mediaSelector)
    mediaCounter = len(mediaList)
    print("Found {} media".format(mediaCounter))

    for media in mediaList:
        media.click()

        # 次へボタンが表示されるまで
        while True:
            try:
                time.sleep(3)
                browser.find_element_by_xpath(likeXpath).click()
                browser.implicitly_wait(10)
                likedCounter += 1
                print("liked {} of {}".format(likedCounter,mediaCounter))
                browser.find_element_by_css_selector(nextPagerSelector).click()
            except:
                break #もう次へボタンが存在しない場合、エラーをはくのでそこで終了
        break #for文自体も終了させる

    print("You liked {} media".format(likedCounter))

ご依頼について

スクレイピングツール欲しい方は、@never_be_a_pmにご連絡お願いします。





2017-12-22

Python IndentationError: unindent does not match any outer indentation levelの対処法

IndentationError: unindent does not match any outer indentation levelの対処法。
エラーの通り、インデントがそろってないよ!というエラー。


で、テキストエディタで見ると、「え、高さ揃ってるやん!」と思ってもこのエラーが出ることがあり、なんでかなぁと調べていると、


タブでインデントしていて、またスペースでインデントしていて、そのインデントの高さが一緒でも、Python上ではそのように認識してくれないよう。


なので、タブかスペースでインデントを統一してあげると、解決できます。

Python Cloud9でPhantomJSをインストールする方法

Cloud9でPhantomJSを利用するのはかなり簡単です。

手順

* Python3に設定
* SeleniumとPhantomJSのインストール

Python3に設定


Python2から3へのバージョンアップします。


で、以下のコマンドを実行

$ sudo mv /usr/bin/python /usr/bin/python2
$ sudo ln -s /usr/bin/python3 /usr/bin/python

SeleniumとPhantomJSのインストール

続いてPhantomJSのインストールを行います。

$ sudo pip3 install selenium
$ sudo npm install -g phantomjs

これで終わりです。

Python SeleniumでJavascriptを実行するときのメモ

PythonでSeleniumを利用する際、Javascriptを実行したいことがある。その方法を調べていた時のメモ。execute_script()の中でJavascriptを実行してあげればよいみたいだ。

Running javascript in Selenium using Python


driver = webdriver.Firefox()

driver.get("http://stackoverflow.com/questions/7794087/running-javascript-in-selenium-using-python")
driver.execute_script("document.getElementsByClassName('comment-user')[0].click()")

Javascriptでページ最下部まで行くときの忘備録

Javascriptでページの一番下までデータを取得したいときに使う際、Javascriptでどうやればいいか調べてみたものの忘備録。このサイトを参考にしたらできた。
Scroll Automatically to the Bottom of the Page


window.scrollTo(0,document.body.scrollHeight);


ただ、ページ最下部でさらにロードが必要な場合、もう何回かscrollしてあげる必要があるので、その場合はfor文で回数を指定してあげたりするとよいかもしれない。

2017-12-21

Evernoteのオープンソース版Joplinを体験してみた

Evernoteのオープンソース版Joplinを体験してみた


Evernoteの機能を丸ごとオープンソースで作成したJoplinを使ってみた。特徴としては

  • PCはMac・Windows・Linuxにいずれも対応
  • Android・iOSにも対応
  • Evernoteからファイルをエクスポートして利用することができる
  • Mark Down記法が利用できる


となっている。

2017-12-20

【Python Instagram】 Instabot.pyのコードを読み解く

前回、APIを利用してInstagramでハッシュタグ検索をしたり、いいねしたりすることは、Sandboxモードで実行不可能だということが分かった。


仕方がないので、APIを通さずにスクレイピングすることにする。
利用するのはPythonとrequests

やりたいこと


以下のことを自動化させたい。基本的な商用Instagramのメイン機能がこれだから。


  • ログインする
  • 特定のタグで検索
  • 検索一覧から写真・動画のURLをクリック
  • その写真・動画をいいねする


いろいろ調べてたら、それをすでにbot化できるプログラムがgithub上に存在した。https://github.com/instagrambot/instabot


ただ、これをそのまま利用するのはあまり面白くないので、内部の言語使用を見ながら自分でコーディングしてみる(自由度も高まりそうだし)


まずログインして、そのセッションを保持する。ログインをするときのエンドポイントはここらしい。

https://www.instagram.com/accounts/login/ajax/

ここでidとpassを入れ、authをセッションで維持する。
保持した後に、特定のタグで検索を行う。ちなみにタグを検索するときは、以下のようなURLになる


https://www.instagram.com/explore/tags/{tagname}/?hl=ja


このtagnameのところに、任意のキーワードを入れれば検索結果を取得できるはず。また、日本語の検索だとエンコーディングされないので、かならず日本語はURLエンコーディングにパースしてあげる必要がある。


タグの検索結果を読み取って、その写真を特定する必要がある。いろいろ見たけど、いいねを押すときのURIエンドポイントが

https://www.instagram.com/web/likes/{media-id}/like/

らしい。これはinstabot.pyのgithubから見つけた。なので、一覧ページからmedia IDをなんとか取得できれば、よいみたい。

media IDの取得方法は、いろいろあさってみたが、写真・動画のページ一つ一つに任意のIDが付与されているみたいだ。例えば#よみうりランドでタグ検索すると

https://www.instagram.com/explore/tags/%E3%82%88%E3%81%BF%E3%81%86%E3%82%8A%E3%83%A9%E3%83%B3%E3%83%89/?hl=ja

#よみうりランドのタグ検索結果が現れるページとなる。


さて、これらの写真一つ一つにいいねするには、それらのIDを指定して、以下の以下のエンドポイントに追加してあげるとよい。

https://api.instagram.com/oembed/?callback=&url={media}

mediaには実際の一枚一枚の画像のURLを張り付けてあげればよい。例えばよみうりランドで一番いいね数が多いもののURLは

https://www.instagram.com/p/Bc5kG_ahqjg/?hl=ja&tagged=%E3%82%88%E3%81%BF%E3%81%86%E3%82%8A%E3%83%A9%E3%83%B3%E3%83%89

になるので、これを先ほどのエンドポイントに代入すると、以下のようなURLになる。


https://api.instagram.com/oembed/?callback=&url=https://www.instagram.com/p/Bc5kG_ahqjg/?hl=ja&tagged=%E3%82%88%E3%81%BF%E3%81%86%E3%82%8A%E3%83%A9%E3%83%B3%E3%83%89



実際にアクセスすると以下のようなJSONを吐いているようだ。ぐちゃぐちゃな結果だけど、このサイトで取得したJSONをパースすると、以下のようなデータが取得できる。


{
    "author_id": 1401232884, 
    "author_name": "haatan.85", 
    "author_url": "https://www.instagram.com/haatan.85", 
    "height": null, 
    "html": "<blockquote class="instagram-media" data-instgrm-captioned data-instgrm-permalink="https://www.instagram.com/p/Bc5kG_ahqjg/" data-instgrm-version="8" style=" background:#FFF; border:0; border-radius:3px; box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15); margin: 1px; max-width:658px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);"><div style="padding:8px;"> <div style=" background:#F8F8F8; line-height:0; margin-top:40px; padding:50% 0; text-align:center; width:100%;"> <div style=" background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAMAAAApWqozAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAMUExURczMzPf399fX1+bm5mzY9AMAAADiSURBVDjLvZXbEsMgCES5/P8/t9FuRVCRmU73JWlzosgSIIZURCjo/ad+EQJJB4Hv8BFt+IDpQoCx1wjOSBFhh2XssxEIYn3ulI/6MNReE07UIWJEv8UEOWDS88LY97kqyTliJKKtuYBbruAyVh5wOHiXmpi5we58Ek028czwyuQdLKPG1Bkb4NnM+VeAnfHqn1k4+GPT6uGQcvu2h2OVuIf/gWUFyy8OWEpdyZSa3aVCqpVoVvzZZ2VTnn2wU8qzVjDDetO90GSy9mVLqtgYSy231MxrY6I2gGqjrTY0L8fxCxfCBbhWrsYYAAAAAElFTkSuQmCC); display:block; height:44px; margin:0 auto -44px; position:relative; top:-22px; width:44px;"></div></div> <p style=" margin:8px 0 0 0; padding:0 4px;"> <a href="https://www.instagram.com/p/Bc5kG_ahqjg/" style=" color:#000; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none; word-wrap:break-word;" target="_blank">. . よみうりランド🎡 . 純子と「超綺麗〜✨」「でも寒すぎる〜」の繰り返し。笑 . #1219  #よみうりランド  #ジュエルミネーション #イルミネーション  #噴水ショー</a></p> <p style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; line-height:17px; margin-bottom:0; margin-top:8px; overflow:hidden; padding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;"><a href="https://www.instagram.com/haatan.85/" style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px;" target="_blank"> はぁたん</a>さん(@haatan.85)がシェアした投稿 - <time style=" font-family:Arial,sans-serif; font-size:14px; line-height:17px;" datetime="2017-12-19T21:47:31+00:00">12月 19, 2017 at 1:47午後 PST</time></p></div></blockquote>
<script async defer src="//platform.instagram.com/en_US/embeds.js"></script>", 
    "media_id": "1673527546626877664_1401232884", 
    "provider_name": "Instagram", 
    "provider_url": "https://www.instagram.com", 
    "thumbnail_height": 612, 
    "thumbnail_url": "https://scontent-nrt1-1.cdninstagram.com/t51.2885-15/s612x612/e35/25008802_1968682780054057_1024404490993795072_n.jpg", 
    "thumbnail_width": 612, 
    "title": ".
.
よみうりランド🎡
.
純子と「超綺麗〜✨」「でも寒すぎる〜」の繰り返し。笑
.
#1219 
#よみうりランド 
#ジュエルミネーション
#イルミネーション 
#噴水ショー", 
    "type": "rich", 
    "version": "1.0", 
    "width": 658
}


ここの

"media_id": "1673527546626877664_1401232884"

を手に入れられれば、先ほどの

https://api.instagram.com/oembed/?callback=&url={media}


ここをエンドポイントに、スクレイピングツールを作れるはず。ただしpostリクエストを送るなら、なんならかの証明が必要なはず。Githubでソースコードを読んでみると、どうやら


  • UUID
  • USER ID
  • TOKEN(CSRF Token)
  • MEDIA ID
  • Signature(署名認証)
を相対パスでエンドポイントに送信しているっぽい。後述するけど、SendRequestの引数にはエンドポイントと、UUID, USER ID, TOKEN, MEDIA IDが入った値からSignatureを作成した署名を引数に、いいねをしているようだ。



api.py

def like(self, mediaId):
        data = json.dumps({
            '_uuid': self.uuid,
            '_uid': self.user_id,
            '_csrftoken': self.token,
            'media_id': mediaId
        })
        return self.SendRequest('media/' + str(mediaId) + '/like/', self.generateSignature(data))


UUIDってなんだよって思って、QiitaやWikipediaで調べてみた



IDとある通り、X-Request-IDの中に入れる値のようだ。

UUID(Universally Unique Identifier)とは、ソフトウェア上でオブジェクトを一意に識別するための識別子である。UUIDは128ビットの数値だが、十六進法による550e8400-e29b-41d4-a716-446655440000というような文字列による表現が使われることが多い。元来は分散システム上で統制なしに作成できる識別子として設計されており、したがって将来にわたって重複や偶然の一致が起こらない前提で用いることができる。[1]マイクロソフトによるGUIDはUUIDの実装の1つと見なせる。[2]

コードを追ってみると、確かに最初にimportされているので、ここでなんらかのランダムなIDを生成できるパッケージを読み込んでいるのだろう。

api.py

import requests
import json
import hashlib
import hmac
import urllib
import uuid
import sys
import logging
import time
from random import randint
from tqdm import tqdm

 それはよいとして、パラメーターにはcsrfトークンも存在する。

            '_csrftoken': self.token,

これはなんだろうと思い、再度調べてみた。




そもそもCSRF攻撃とは何か調べてみたところ

「クロスサイトリクエストフォージェリ(CSRF)」とは?クロスサイトリクエストフォージェリ(CSRF)とは、Webアプリケーションに存在する脆弱性、もしくはその脆弱性を利用した攻撃方法のことです。掲示板や問い合わせフォームなどを処理するWebアプリケーションが、本来拒否すべき他サイトからのリクエストを受信し処理してしまいます。

クロスサイトリクエストフォージェリ(CSRF)から引用

 で、そのCSRF tokenをuuidで生成しているのかな。この辺ちょっとわからないのでいろいろ調べる必要あり。

HTTP ヘッダーでトークンを送信する
Ajax などでボディは JSON にする場合、上記のような方法でトークンを渡すことができない。その場合は HTTP ヘッダーでトークンを渡す。ヘッダー名は ${_csrf.headerName} で取得できる。あとは、ヘッダー名とトークンの値を HTTP 内に埋め込んで置き、 JavaScript で取り出して Ajax リクエストのヘッダーにセットすればいい。


さてさて、またコードを見てみる。
普段オブジェクト指向で書かないから、よくわからないコードばかりだ。

def like(self, mediaId):
        data = json.dumps({
            '_uuid': self.uuid,
            '_uid': self.user_id,
            '_csrftoken': self.token,
            'media_id': mediaId
        })
        return self.SendRequest('media/' + str(mediaId) + '/like/', self.generateSignature(data))

 このselfもよくわからないので調べる。たぶんインスタンス化することだと思うんだけどね。とりあえず最初にselfが出ているところを調べると、githubのソースにはこんな感じでコードが書かれている。


api.py

class API(object):
    def __init__(self):
        self.isLoggedIn = False
        self.LastResponse = None
        self.total_requests = 0

        # handle logging
        self.logger = logging.getLogger('[instabot]')
        self.logger.setLevel(logging.DEBUG)
        logging.basicConfig(format='%(asctime)s %(message)s',
                            filename='instabot.log',
                            level=logging.INFO
                            )
        ch = logging.StreamHandler()
        ch.setLevel(logging.DEBUG)
        formatter = logging.Formatter(
            '%(asctime)s - %(levelname)s - %(message)s')
        ch.setFormatter(formatter)
        self.logger.addHandler(ch)

    def setUser(self, username, password):
        self.username = username
        self.password = password
        self.uuid = self.generateUUID(True)

    def login(self, username=None, password=None, force=False, proxy=None):
        if password is None:
            username, password = get_credentials(username=username)

        m = hashlib.md5()
        m.update(username.encode('utf-8') + password.encode('utf-8'))
        self.proxy = proxy
        self.device_id = self.generateDeviceId(m.hexdigest())
        self.setUser(username, password)

        if (not self.isLoggedIn or force):
            self.session = requests.Session()
            if self.proxy is not None:
                parsed = urlparse(self.proxy)
                scheme = 'http://' if not parsed.scheme else ''
                proxies = {
                    'http': scheme + self.proxy,
                    'https': scheme + self.proxy,
                }
                self.session.proxies.update(proxies)

            url = 'si/fetch_headers/?challenge_type=signup&guid='
            url = url + self.generateUUID(False)
            if self.SendRequest(url, None, True):
                data = {'phone_id': self.generateUUID(True),
                        '_csrftoken': self.LastResponse.cookies['csrftoken'],
                        'username': self.username,
                        'guid': self.uuid,
                        'device_id': self.device_id,
                        'password': self.password,
                        'login_attempt_count': '0'}

                signature = self.generateSignature(json.dumps(data))
                if self.SendRequest('accounts/login/', signature, True):
                    self.isLoggedIn = True
                    self.user_id = self.LastJson["logged_in_user"]["pk"]
                    self.rank_token = "%s_%s" % (self.user_id, self.uuid)
                    self.token = self.LastResponse.cookies["csrftoken"]

                    self.logger.info("Login success as %s!" % self.username)
                    return True
                else:
                    self.logger.info("Login or password is incorrect.")
                    delete_credentials()
                    return False
        return False


と書いてある。selfはどんなもんで使うのか調べてみる。




api.py

class Test(object):
    c_var = "Hello"

    def __init__(self):
        self.i_var = "World"

    def __str__(self):
        return Test.c_var + " " + self.i_var

    def examine1(self):
        self.c_var = "Hey"
        return self

    def examine2(self):
        Test.c_var = "Hey"
        return self

t = Test()
print(t.examine1()) #Hello World
print(t.examine2()) #Hey World
print(Test()) #Hey World

つまりapy.pyのAPIクラスが呼び出されたときに、username, passwordなどの値がインスタンス化され、かつuuidが生成されているのか。


なんとなくやろうとしていることがわかったので、疑問点抽出


  • sendRequestsというメソッドはどこに存在する?
  • csrf tokenってどこからとってきている?
  • 一意のsessionを維持するための方法は?


    • 一個ずつ見ていく。

      • sendRequestsというメソッドはどこに存在する?

      api.pyの下部のコードにSendRequestメソッドを発見。


      api.py

      def SendRequest(self, endpoint, post=None, login=False):
          if (not self.isLoggedIn and not login):
              self.logger.critical("Not logged in.")
              raise Exception("Not logged in!")
      
          self.session.headers.update({'Connection': 'close',
                                       'Accept': '*/*',
                                       'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
                                       'Cookie2': '$Version=1',
                                       'Accept-Language': 'en-US',
                                       'User-Agent': config.USER_AGENT})
          try:
              self.total_requests += 1
              if post is not None:  # POST
                  response = self.session.post(
                      config.API_URL + endpoint, data=post)
              else:  # GET
                  response = self.session.get(
                      config.API_URL + endpoint)
          except Exception as e:
              self.logger.warning(str(e))
              return False
      
          if response.status_code == 200:
              self.LastResponse = response
              self.LastJson = json.loads(response.text)
              return True
          else:
              self.logger.error("Request return " + str(response.status_code) + " error!")
              if response.status_code == 429:
                  sleep_minutes = 5
                  self.logger.warning("That means 'too many requests'. "
                                      "I'll go to sleep for %d minutes." % sleep_minutes)
                  time.sleep(sleep_minutes * 60)
              elif response.status_code == 400:
                  response_data = json.loads(response.text)
                  self.logger.info("Instagram error message: %s", response_data.get('message'))
                  if response_data.get('error_type'):
                      self.logger.info('Error type: %s', response_data.get('error_type'))
      
              # for debugging
              try:
                  self.LastResponse = response
                  self.LastJson = json.loads(response.text)
              except:
                  pass
              return False


      このメソッドで

      def like(self, mediaId):
              data = json.dumps({
                  '_uuid': self.uuid,
                  '_uid': self.user_id,
                  '_csrftoken': self.token,
                  'media_id': mediaId
              })
              return self.SendRequest('media/' + str(mediaId) + '/like/', self.generateSignature(data))
      

      SendRequestの引数に入っているのは

      return self.SendRequest('media/' + str(mediaId) + '/like/', self.generateSignature(data))



      • エンドポイント(likeする部分):endpoint
      • generateSignature(data):post
      なんだろうなと。

      そうなるとgenerateSignature(data)ってなんぞやって話になるので、コードをさらに見てみるとメソッドが存在

      api.py

      def generateSignature(self, data):
          try:
              parsedData = urllib.parse.quote(data)
          except AttributeError:
              parsedData = urllib.quote(data)
      
          return 'ig_sig_key_version=' + config.SIG_KEY_VERSION + '&signed_body=' + hmac.new(
              config.IG_SIG_KEY.encode('utf-8'), data.encode('utf-8'), hashlib.sha256).hexdigest() + '.' + parsedData

      クラスからインスタンス変数となったuseridやmediaidなどが入った辞書型のデータをパースしていて、そこから何かハッシュ化して暗号化しているのがコードを読んでいるとわかる。で、Signatureを作成している。そもそもSignatureってなんだ。たぶん通信の際に使うものなんだろう。と思ったので調べてみる。http Signatureで検索したら署名認証ということがわかり、こんな記事があった。


      署名認証の一般プロセス
      1.プロデューサー (送信者) が必要な証明書を取得します。
      2.プロデューサーが証明書つきのリクエストをコンシューマー (受信者) に送信します。
      3.コンシューマーは証明書を使用して、プロデューサーが本当にリクエストを送信したか検証します。
      4.認証が成功すると、コンシューマーはリクエストを処理します。そうでない場合は、コンシューマーはリクエストを処理せず、必要に応じて応答します。(Amazon)

      また認証まわりの話っぽい。しんどい笑
      次。

      • csrf tokenってどこからとってきている?


      loginメソッドの中にいた

      api.py

      def login(self, username=None, password=None, force=False, proxy=None):
              if password is None:
                  username, password = get_credentials(username=username)
      
              #省略
      
              url = 'si/fetch_headers/?challenge_type=signup&guid='
          url = url + self.generateUUID(False)
          if self.SendRequest(url, None, True):
              data = {'phone_id': self.generateUUID(True),
                      '_csrftoken': self.LastResponse.cookies['csrftoken'],
                      'username': self.username,
                      'guid': self.uuid,
                      'device_id': self.device_id,
                      'password': self.password,
                      'login_attempt_count': '0'}
      
              signature = self.generateSignature(json.dumps(data))

      なんかLastResponseというメソッドからとってきているみたいだ。

      '_csrftoken': self.LastResponse.cookies['csrftoken'],

      じゃあLastResponseはどこ?と気になったので見てみると、まず最初にClassでインスタンス化させていて

      api.py

      class API(object):
          def __init__(self):
              self.isLoggedIn = False
              self.LastResponse = None
              self.total_requests = 0

      次にSendRequestでLastResponseが現れる

      def SendRequest(self, endpoint, post=None, login=False):
          if (not self.isLoggedIn and not login):
              self.logger.critical("Not logged in.")
              raise Exception("Not logged in!")
      
          self.session.headers.update({'Connection': 'close',
                                       'Accept': '*/*',
                                       'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
                                       'Cookie2': '$Version=1',
                                       'Accept-Language': 'en-US',
                                       'User-Agent': config.USER_AGENT})
          try:
              self.total_requests += 1
              if post is not None:  # POST
                  response = self.session.post(
                      config.API_URL + endpoint, data=post)
              else:  # GET
                  response = self.session.get(
                      config.API_URL + endpoint)
          except Exception as e:
              self.logger.warning(str(e))
              return False
      
          if response.status_code == 200:
              self.LastResponse = response
              self.LastJson = json.loads(response.text)
              return True

      GETかPOSTかの通信をrequests.sessionで行って、

      if post is not None:  # POST
                  response = self.session.post(
                      config.API_URL + endpoint, data=post)
              else:  # GET
                  response = self.session.get(
                      config.API_URL + endpoint)

      通信に成功したとき(200番が出たときに)

      if response.status_code == 200:

      インスタンス変数LastResponseに、最新のsessionを代入され、更新されるという流れになっているよう。

      self.LastResponse = response

      つまり通信のたびに、ここでインスタンス変数の中でsessionの値が保持され続けるようになるということなのね。このLastResponseの中に存在するsessionの値を利用して、その中からcsrftokenを取り出しているので、csrftokenの一貫性が維持できるようだ。

      ちなみにPOST, DELETE, UPDATEが必要なメソッドの中にはすべてSendRequestメソッドが存在して、引数にエンドポイントとデータを指定できるようにしているみたい。かなり便利な仕組みになっている。


      で、ここで更新されたLastResponseを、


      '_csrftoken': self.LastResponse.cookies['csrftoken'],

      で読み込んでいるんだね。勉強になる。そうか、requests.sessionで取得した値は、keyで指定することによって、呼び出すことができるのね。


      そういえば、ログインしてからのセッションの流れもよくわかってない。ログインのメソッドも読んでみる

      結構importしてきているファイルがあって、そこを追うのは面倒だからなんとなくで読んでみる。けど疲れたからまた今度。

      api.py

      def login(self, username=None, password=None, force=False, proxy=None):
              if password is None:
                  username, password = get_credentials(username=username)
      
              m = hashlib.md5()
              m.update(username.encode('utf-8') + password.encode('utf-8'))
              self.proxy = proxy
              self.device_id = self.generateDeviceId(m.hexdigest())
              self.setUser(username, password)
      
              if (not self.isLoggedIn or force):
                  self.session = requests.Session()
                  if self.proxy is not None:
                      parsed = urlparse(self.proxy)
                      scheme = 'http://' if not parsed.scheme else ''
                      proxies = {
                          'http': scheme + self.proxy,
                          'https': scheme + self.proxy,
                      }
                      self.session.proxies.update(proxies)
      
                  url = 'si/fetch_headers/?challenge_type=signup&guid='
                  url = url + self.generateUUID(False)
                  if self.SendRequest(url, None, True):
                      data = {'phone_id': self.generateUUID(True),
                              '_csrftoken': self.LastResponse.cookies['csrftoken'],
                              'username': self.username,
                              'guid': self.uuid,
                              'device_id': self.device_id,
                              'password': self.password,
                              'login_attempt_count': '0'}
      
                      signature = self.generateSignature(json.dumps(data))
                      if self.SendRequest('accounts/login/', signature, True):
                          self.isLoggedIn = True
                          self.user_id = self.LastJson["logged_in_user"]["pk"]
                          self.rank_token = "%s_%s" % (self.user_id, self.uuid)
                          self.token = self.LastResponse.cookies["csrftoken"]
      
                          self.logger.info("Login success as %s!" % self.username)
                          return True
                      else:
                          self.logger.info("Login or password is incorrect.")
                          delete_credentials()
                          return False
              return False




      注目の投稿

      めちゃくちゃ久しぶりにこのブログ書いたw 更新3年ぶりw > 多様性というゲームは尊厳と自由を勝ち取るゲームなのかもしれないな。  もともとツイッターでツイートした内容なんだけど、ちょっと深ぼる。 ----- 自分は男 x 30代x 二児の父 x 経営者 x 都心(共働き世...