Akamaiに監視されているインターネット社会

Webサイトの高速化は非常に重要で、GoogleSEO対策にもとにかく高速なレスポンスを、みたいなことが言われている。そこで活躍するのがCDNというサービスで、Akamaiが有名。Wikipediaにも説明があります。

コンテンツデリバリネットワーク - Wikipedia

例えば、あなたが買い物する楽天akamaiによってCDNが提供されています。

$ nslookup www.rakuten.co.jp
Server:     8.8.8.8
Address:    8.8.8.8#53

Non-authoritative answer:
www.rakuten.co.jp   canonical name = evsan.rakuten.edgekey.net.
evsan.rakuten.edgekey.net   canonical name = e16791.a.akamaiedge.net.
Name:   e16791.a.akamaiedge.net
Address: 23.197.96.227

アクセスが高速化するならいいじゃないか、ということなんですけど、 akamai access denied とかで検索すると、けっこうな人がアクセス制限を受けているみたい。しかも、契約しているそもそもの会社はあまり認識していない様子。セキュリティ機能ありますよ?有効にしませんか? あ、じゃぁお願いします、みたいな感じなのかもしれない。

Akamaiに監視されるインターネット社会を許していいのでしょうか。

ちなみに、Akamaiにアクセス制限をうけるとIPアドレスを変更するしか制限を回避する方法はないみたいで、VPNを使った回避策をいろんなYouTuberがあげてました。が、これは本質的な解決策ではないですね。Akamaiとの契約を見直すべきだと思いますよ。安易に導入した謎のセキュリティ機能でお客様のアクセスを遮断していませんか?

Akamaiの何が問題か?

Akamaiはかなりの企業が導入をしています。オムロン株式会社、楽天市場プレイステーションネットワークヒルトンホテル、アメックス、ANAなどなど。日本だけではなく海外の企業もたくさんです。EPICPASSも使っています。各社、どのような設定でAkamaiを使っているかは不明ですが、IPアドレスの評価はすべてのAkamaiサイトに共通で評価され、その評価が全体に採用されます。

別に、楽天市場に対してスクレイピングしなくても、EPICPASSにスクレイピングした結果、EPICPASSにはアクセスできなくならないのに楽天市場にはアクセスできなくなるんです。これが、各社どのような設定をしているのか不明なところです。

Access Denied とか Reference # 謎の数値 とかに遭遇したら、Akamaiに遮断されています。

サポートチャットの担当が外国人

ちょっと困ったことがあってサポートサイトを見たら、チャットが用意されていた。よし、問い合わせようと思って開始してみたら、担当してくれたのが中国でトップ3に入るであろう名字の人だった。最近コンビニは全員外国人バイトなんじゃないかと思っていたけど、こういうところまで外国人が入ってきているんだね。なかなか流暢な日本ではあったんだけど、こんな感じでもあった。

A: 恐れ入りますが、他のネット環境でサインインをお試しお願いいたします。

お試しお願い、ですね。はい、すでに試してますよ。

Q: Emailでご連絡いただけるということでしょうか? A: はい、さようでございます。

おぉ、そんな日本語が出てくるとは。

Q: ありがとうございました。 A: とんでもないでございます。 A: 何か不明なことがありましたら、またお問い合わせますようにお願いいたします。

おぉ、そんな日本語が出てくるとはPart 2、と思ったら、お問い合わせますように、ときましたか。しかし、解決できそうな情報へのアクセス権もなさそうだったし、想定しているサポートって、ログインページはどこですか?みたいな単純な質問なんでしょうかね。時給いくらなんだろう。

seleniumでウェブスクレイピング

ウェブスクレイピングとは、Webサイトから欲しい情報を取り出すことです。

  • 注意事項
  • 必要なソフトウェアたち
  • 日経からドル円を取り出す
    • 実際にChromeをたちあげたく無い場合はheadlessモード
  • epic passの価格を取り出す
    • 時間を指定して適当に待つ
    • WebDriverWaitで待つ
  • 正規表現で欲しい部分だけ取り出す

注意事項

Webサイトによってはロボットによる巡回、つまりスクレイピングを禁止している場合があります。まずは /robots.txt を参照してスクレイピングが禁止されていないことを確認しましょう。

たとえば日本経済新聞のサイト https://www.nikkei.com/robots.txt ではこのような指定があります。

User-agent: *
Disallow: /search/site/

ということで、/search/site/ 以下をウェブスクレイピングしないようにしましょう。

(2018/11/28追記) スクレイピングAkamaiに低評価をつけられる可能性があります。 こちらにも書きましたが、何をしたらどれくらい低評価をつけるのかは教えてくれません。複数サイトに対してスクレイピングをするのは問題ありです。今回のスクレイピングで5日間ペナルティくらいました。 スクレイピングする前に、nslookupしてakamaiの場合はスクレイピングしない方が安全です。

tomo3i.hatenablog.com

必要なソフトウェアたち

OSX 10.14上で python3, selenium, chromedriver を想定しています。

python3は標準では入っていないので、Homebrewでインストールしましょう。

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

表示に従ってHomebrewをインストールしたら

brew install python3
brew cask install chromedriver
pip3 install selenium

日経からドル円を取り出す

日経のサイトは一番上にドル円が買いてあります。1ドル=112.96-97円というやつ。この部分をどうやって指定するかですが、次の方法が良いでしょう。 1 普通にChromeで実際にアクセスします 2 112.96-97と買いてあるところで右クリックして検証を選びます 3 右側に表示された検証ウィンドウから112.96-97と買いてあるところを探し、右クリックして Copy - Copy selector を選びます

この手順により、このようなCSSセレクタが取得できたと思います。

#js-ticker_list > ul > li > div:nth-child(3) > a > span.m-miH01C_rate

あとはこのCSSセレクタを使用して、seleniumとchromedriverでこんな風にすれば、112.96-97という文字列を取り出すことができます。

from selenium import webdriver

url = "https://www.nikkei.com/"
selector = "#js-ticker_list > ul > li > div:nth-child(3) > a > span.m-miH01C_rate"

driver = webdriver.Chrome()
driver.get(url)

elements = driver.find_elements_by_css_selector(selector)
for e in elements:
    print (e.text)
driver.close()

たとえば、nikkei.py という名前で保存したとすると、

python3 nikkei.py

と実行します。すると実際にChromeが立ち上がって、読み込みが終わるまでしばらく待つと、ターミナルには112.96-97 と出力されます。

実際にChromeをたちあげたく無い場合はheadlessモード

headlessモードを使用すればChromeが見えなくなります。見えなくなるだけで実際には起動しています。JavaScirptの実行が終わらないと値が取得できないとか、今どんな状況かわからないとか、そういうよくわからない状況があるので、最初は実際にChromeが開いて読み込みが進んでいる様子と、ターミナルへの出力を一緒に眺めた方が動作確認しやすいと思います。

headlessモードを有効にした場合は、このような記述になります。selenium.webdriver.chrome.optionsを使うのがポイントです。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

url = "https://www.nikkei.com/"
selector = "#js-ticker_list > ul > li > div:nth-child(3) > a > span.m-miH01C_rate"

options = Options()
options.headless = True

driver = webdriver.Chrome(options=options)
driver.get(url)

elements = driver.find_elements_by_css_selector(selector)
for e in elements:
    print (e.text)
driver.close()

epic passの価格を取り出す

本当にやりたかったのはこれでした。CSSセレクタを取り出すところまでは日経のときと同じですが、同様にやると問題が起きました。何も表示されないままスクリプトの実行が終わってしまいます。headlessモードを無効にしていると、まだぜんぜんページの表示が終わっていないのにスクリプトが終わっていることがわかりました。

時間を指定して適当に待つ

なんだかよくわからない場合、10秒ぐらい待てばなんとかなるだろうという考えのもと、time.sleepで待つのも良いと思います。とりあえず待てばなんとかなります。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time

url = "https://www.epicpass.com/passes/epic-4-day.aspx"
selector = "#c27_Product_Detail_0 > div.col-xs-3.hidden-xs.pass_category_detail__price_col > span"

options = Options()
options.headless = True

driver = webdriver.Chrome(options=options)

driver.get(url)
time.sleep(10)

elements = driver.find_elements_by_css_selector(selector)
for e in elements:
    print (e.text)
driver.close()

WebDriverWaitで待つ

指定した要素が表示されるまで待つ、ということもできます。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.common.by import By

url = "https://www.epicpass.com/passes/epic-4-day.aspx"
selector = "#c27_Product_Detail_0 > div.col-xs-3.hidden-xs.pass_category_detail__price_col > span"

options = Options()
options.headless = True

driver = webdriver.Chrome(options=options)
driver.get(url)
wait = WebDriverWait(driver, 10)
e = wait.until(expected_conditions.visibility_of_element_located((By.CSS_SELECTOR, selector)))
print (e.text)
driver.close()

正規表現で欲しい部分だけ取り出す

上記の方法で結果を取得したとき、目的の文字列が取り出せる場合とそうでない場合があります。例えば、価格をとりだしたかったのに取得した文字列がこんな風になっている場合があります。

$100 is the latest price! Buy now!

こういう場合は正規表現で欲しい部分だけ取り出すことができます。

import re
s = "$100 is the latest price! Buy now!"
r = "[0-9]+"
re.search(r, s).group()

本当は、"¥$[0-9]+" でマッチさせようと思ったんですが、なぜかマッチしませんでした。検索してもよくわからず。¥$ で $ をエスケープできているんじゃないんだろうか。わかる方教えてください。

というわけで、全ソースコードを貼っておきます。が、どうやらこれを実行するとAkamaiからマイナス評価を受けるみたいです。実行しないことをおすすめします。適当に待ち時間を入れればいいのかな。

続きを読む

前売りチケットの価格3

カリフォルニア州でスキーをするのに絶対に必要になるリフトチケットの価格調査3回目。しばらくさぼってしまいました。価格はどうなってるのでしょうか?チェックしていきましょう。

チケット 10/7 10/11 11/22
EPIC 4day $479 $489 $489
EPIC 7day $689 $709 $709
EPIC Tahoe local $589 $609 $609
ECPI Tahoe value $499 $519 %519
EPIC $929 $949 $949
Tahoe Super 4 $369 $369 $389
Sugar Bowl unrestricted $799 $799 $829
Sugar Bowl slightly restriced $589 $589 $619
IKON PASS $999 $1049 $1049
IKON BASE PASS $699 $749 $749

感想、大差ないですね。何ヶ月も前からお金を払っていくのかいかないのかそわそわし続けるよりは、サンクスギビングまで待ってから買えば良い、という気持ちになりました。ちなみにEPICはあと2日で値上がりするそうです。まあでもこの感じだとあがっても$20くらいなのかな、と思うとそんなに焦らなくてもいいような気がしますね。いや、$20もあったらスターバックス4回いけるやん、という方は急いで買いましょう。

ちなみに、それぞれのチケットでどこに行こうと思っているかというのがこちら。

チケット スキー場
EPIC 4/7 day, Tahoe local/value Heavenly, Northstar, Kirkwood
EPIC Heavenly, Northstar, Kirkwood (, Hakuba Valley)
IKON Squaw Valley (, Mammoth, Niseko United)
Tahoe Super 4 Squaw Valley (, Alpine Meadows)

ワイヤレスリモコンを改造しよう6

前回の続き

TinyduinoのWiFi TinyShieldが無事に動くようになったので、JSONを取得してONかOFFかを判定して信号を出すようにしてみた。 JSONをどう読み取ろうか悩んだけど、潔くさぼりにさぼってほぼ判定なし。まあONかOFFだけだし大丈夫でしょう。

WiFi経由でHTTPアクセスする部分はこちらのコードを大いに参考にしました。 Arduino - WiFiWebClientRepeating

というわけで、以下ほぼ全ソースコード。いちぶ伏せています。

#include <SPI.h>
#include <WiFi101.h>

#include "arduino_secrets.h" 
///////please enter your sensitive data in the Secret tab/arduino_secrets.h
char ssid[] = SECRET_SSID;        // your network SSID (name)
char pass[] = SECRET_PASS;    // your network password (use for WPA, or use as key for WEP)
int keyIndex = 0;            // your network key Index number (needed only for WEP)

int status = WL_IDLE_STATUS;

// Initialize the WiFi client library
WiFiClient client;

// server address:
char server[] = "himitsu";

unsigned long lastConnectionTime = 0;            // last time you connected to the server, in milliseconds
const unsigned long postingInterval = 1L * 1000L; // delay between updates, in milliseconds

#define O 4 // control signal output pin
#define SHORT 390 -20 // target - adjustment us
#define LONG  990 -10 // target - adjustment us
#define INTER 9930 -1000 // us
#define NUM 5 // repeat the same command

String inputString = "";         // a String to hold incoming data
bool stringComplete = false;  // whether the string is complete

String on    = "0001010100010101010101110";
String off   = "0001010100010101010101000";

void short_out(){
  digitalWrite(O, HIGH);
  delayMicroseconds(SHORT);
  digitalWrite(O, LOW);
  delayMicroseconds(LONG);
}

void long_out(){
  digitalWrite(O, HIGH);
  delayMicroseconds(LONG);
  digitalWrite(O, LOW);
  delayMicroseconds(SHORT); 
}

void modulate_out(String s){
  int l = s.length();
  //Serial.println(l);
  int i, j;
  short_out();
  for(j=0;j<NUM;j++){
    delayMicroseconds(INTER);
    for(i=0;i<l;i++){
      char c = s.charAt(i);
      if(c == '0'){ short_out(); }
      else{ long_out(); }
    }
  }
}


void setup() {
  //Initialize serial and wait for port to open:
  Serial.begin(115200);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }

  pinMode(O, OUTPUT);
  digitalWrite(O, LOW);
  
  WiFi.setPins(8, 2, A3, -1);
  // check for the presence of the shield:
  if (WiFi.status() == WL_NO_SHIELD) {
    Serial.println("WiFi shield not present");
    // don't continue:
    while (true);
  }

  // attempt to connect to WiFi network:
  while ( status != WL_CONNECTED) {
    Serial.print("Attempting to connect to SSID: ");
    Serial.println(ssid);
    // Connect to WPA/WPA2 network. Change this line if using open or WEP network:
    status = WiFi.begin(ssid, pass);

    // wait 10 seconds for connection:
    delay(10000);
  }
  // you're connected now, so print out the status:
  printWiFiStatus();
}

int countDown = 0;
int checkNF = 0;
int lastCommand = 0;
int thisCommand = 0;
void loop() {
  // if there's incoming data from the net connection.
  // send it out the serial port.  This is for debugging
  // purposes only:
  while (client.available()) {
    char c = client.read();
    //Serial.write(c);

    if(checkNF == 1){
      checkNF = 0;
      if(c == 'n'){ // command is on
        thisCommand = 1;
      }
      else if(c == 'f'){  // command is off
        thisCommand = 0;
      }
    }
    if(countDown > 0){
      countDown--;
      if(countDown == 0){
        //Serial.println("check if it is o");
        if(c == 'o'){ checkNF = 1; }
      }
    }
    if(c == '{'){
      //Serial.println("in JSON");
      countDown = 7;;
    }
  }

  if(lastCommand != thisCommand){
    Serial.println("command changed");
    Serial.println(thisCommand);

    if(thisCommand == 1){
      modulate_out(on);
    }
    else{
      modulate_out(off);
    }

    lastCommand = thisCommand;
  }

  // if ten seconds have passed since your last connection,
  // then connect again and send data:
  if (millis() - lastConnectionTime > postingInterval) {
    httpRequest();
  }
}

// this method makes a HTTP connection to the server:
void httpRequest() {
  // close any connection before send a new request.
  // This will free the socket on the WiFi shield
  client.stop();

  // if there's a successful connection:
  if (client.connect(server, 80)) {
    //Serial.println("connecting...");
    // send the HTTP PUT request:
    client.println("GET /himitsu.json HTTP/1.1");
    client.println("Host: himitsu");
    client.println("Connection: close");
    client.println();

    // note the time that the connection was made:
    lastConnectionTime = millis();
  }
  else {
    // if you couldn't make a connection:
    Serial.println("connection failed");
  }
}


void printWiFiStatus() {
  // print the SSID of the network you're attached to:
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());

  // print your WiFi shield's IP address:
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);

  // print the received signal strength:
  long rssi = WiFi.RSSI();
  Serial.print("signal strength (RSSI):");
  Serial.print(rssi);
  Serial.println(" dBm");
}

ワイヤレスリモコンを改造しよう5

前回の続き

前回まではRaspberry Pi 3を使っていて、Pythonで指定したパルス幅と実際があわないから、なんとかしたいと思いつつArduinoでやってみることにした。基本的には制御部分を移植しただけ。結局のところ、390usとか990usというのをそのものずばりは作り出せなかったから、測定しながら適当に調整してこんな感じになった。Arduinoもあんまりだ、という結論でいいのか、そもそも作り方がよくないのか、誰か教えてください。さらにWifiで通信する部分も移植したかったんだけど、うまくいかずに今のところ断念。

#define O 4 // control signal output pin
#define SHORT 390 -20 // target - adjustment us
#define LONG  990 -10 // target - adjustment us
#define INTER 9930 -1000 // us
#define NUM 5 // repeat the same command

String inputString = "";         // a String to hold incoming data
bool stringComplete = false;  // whether the string is complete

String on    = "0001010100010101010101110";
String off   = "0001010100010101010101000";

void setup() {
  pinMode(O, OUTPUT);
  digitalWrite(O, LOW);

  Serial.begin(9600);
  inputString.reserve(200);  // reserve 200 bytes for the inputString:
}

void short_out(){
  digitalWrite(O, HIGH);
  delayMicroseconds(SHORT);
  digitalWrite(O, LOW);
  delayMicroseconds(LONG);
}

void long_out(){
  digitalWrite(O, HIGH);
  delayMicroseconds(LONG);
  digitalWrite(O, LOW);
  delayMicroseconds(SHORT); 
}

void modulate_out(String s){
  int l = s.length();
  Serial.println(l);
  int i, j;
  short_out();
  for(j=0;j<NUM;j++){
    delayMicroseconds(INTER);
    for(i=0;i<l;i++){
      char c = s.charAt(i);
      if(c == '0'){ short_out(); }
      else{ long_out(); }
    }
  }
}

void loop() {
  // put your main code here, to run repeatedly:
  if (stringComplete) {
    Serial.println(inputString);
    if(inputString.startsWith("on")){
      Serial.println("command on");
      modulate_out(on);
    }
    else if(inputString.startsWith("off")){
      Serial.println("command off"); 
      modulate_out(off);
    }
    // clear the string:
    inputString = "";
    stringComplete = false;
  }
}

void serialEvent() {
  while (Serial.available()) {
    // get the new byte:
    char inChar = (char)Serial.read();
    // add it to the inputString:
    inputString += inChar;
    // if the incoming character is a newline, set a flag so the main loop can
    // do something about it:
    if (inChar == '\n') {
      stringComplete = true;
    }
  }
}

太鼓の達人コントローラーが壊れた

ひさしぶりに太鼓の達人をやろうとおもって、PS4の電源を入れてみたんだけど何かがおかしい。ボタンが効かない。いや、効いてるんだけど太鼓の達人を始めることができない。アプリを開くところまではできるんだけど、太鼓の達人の画面では何も操作が効かない。どういうことだろうと思って、普通のコントローラーでプレイ画面まで進めて、太鼓コントローラーに変更してみると・・?

ぷぷぷぷぷぷぷぷぷぷぷぷぷぷぷ

とにかく左に移動し続ける。何これ?どうなってるの?

いやもうどうしようもない。壊れた。左が入りっぱなしになってる。遊べない。あきらめよう。

そう思ったんだけど、まあ壊れたらとりあえず分解してみるよね。夏に買ったからまだ保証期間内だと思うけど、アメリカ在住だから修理に出すことはできないし、壊れたら捨てるしかないし、捨てる前に分解するのは普通の流れ。ということで、普通のネジだしとりあえず底にあるネジを5つはず。そーっと開くとIDEみたいなケーブルがあるから、半開きのままコネクタをとりはずす。2pin-4pin-2pinみたいなコネクタはLとRがわかりにくいけど、基板をみればシルクでLとRが買いてあるし、ケーブル側にも買いてあるし、躊躇せずはずして大丈夫。

こんなやつで叩く部分がとまっている。 f:id:tomo3i:20181118121633p:plain

こんな風にはずれるので、横方向に指でおせばとれる。指は痛い。 f:id:tomo3i:20181118121656p:plain

そうするとこんなやつがあらわれる。 f:id:tomo3i:20181118121427p:plain

左が入りっぱなしになっているということは、このあたりの何かが悪いんだと思うけど、見た目には何も悪く無い。何が悪いのかわからないけど、とりあえず開いたまま接続してみる。

やっぱりダメ。左が入りっぱなし。よし、左のコネクタを抜こう。

直った。いや、直ってないけど、左無しで真ん中も右も動くようになった。やっぱり左がおかしい。でも、どれだけ眺めてみてもおかしいところはない。これは何センサなんだろう?圧力で何かがどうかなる感じ?よくわからないなぁ、どこもおかしくないよなぁと思いつつ、とりあえずホコリをとる。そして再接続。

直った!

よくわからないけど、治りました。良い子は保証期間内ならおとなしく修理に出しましょう。