印刷
カテゴリ: FreeBSDとLinux
参照数: 613

巷では、Webブラウザを制御して、色々と情報を取得するのが流行っているみたいです。

そう、スクレイピングですね

ちょっと、ダーティーな感じもしますが、元々、Webのテスト用ツールだったみたいで、本来そういった使い方をしてほしいんですかね

まー、そういうのは、どうでも良いんですが、Webの情報って、Htmlを解析して、ここのエレメントから属性取ってとかってしないといけないので、面倒です

昔から色々、そういったツールがあったんですが、最終的にSeleniumが生き残ったみたいです。

Seleniumは、C#で適当に組めば、動作させることが可能なんですが、環境構築が面倒です。

なので、今回は、Dockerを使って、やってみたいと思います。


ところで、アマテンってご存知でしょうか。

ネットのギフト券ショップです。

扱っているのは、コードで販売出来る金券だけですけど

アマゾンギフト券とか、GooglePlayカードとかですね

結構、割引が大きい時があるのですが、常に見張っておかないと値動きがあるので、良いタイミングを逃してしまいます。

そこで、値動きを監視するようなツールをnodejs(JavaScript)を使って作ってみましょう。

Dockerfile

FROM ubuntu
EXPOSE 8088
RUN apt -y update
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Tokyo
RUN apt install -y tzdata
RUN apt install -y nodejs npm
RUN apt install -y curl
RUN curl -LO https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
RUN apt-get install -y ./google-chrome-stable_current_amd64.deb
RUN rm google-chrome-stable_current_amd64.deb
RUN apt install -y fonts-noto
WORKDIR /project

docker-compose.yml

version: "3"
services:
 scrape1:
  build:
   context: .
  ports:
   - "8088:8088"
  expose:
   - "8088"
  volumes:
   - ./project:/project
  tty: true
  working_dir: /project
  command: bash /project/init.sh
  deploy:
   resources:
    limits:
     memory: 256m

amaten.js

const puppeteer = require('puppeteer');

let browser = null;

/*
AmatenよりAmazonギフト券価格を取得
*/
async function amazonprice() {

 if( browser == null )

  browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});

 const page = await browser.newPage();
 await page.setViewport({width:1280,height:1024});
 await page.goto('https://amaten.com/exhibitions/amazon');

 await page.waitForTimeout(1000);

 const amazonpricelistelem = await page.$x('//*[@id="contents_list"]/div[4]/table/tbody');
 const listelem = await amazonpricelistelem[0].$x("tr");

 var list = new Array();
 for( let i=0; i<listelem.length; i++) {
  var record = {};
  var giftcount = await(await (await listelem[i].$x('th/span[@class="js-gift-count"]'))[0].getProperty('textContent')).jsonValue();
  var facevalue = await(await (await listelem[i].$x('td/span[@class="js-face_value"]'))[0].getProperty('textContent')).jsonValue();
  var pricevalue = await(await (await listelem[i].$x('td/div/span[@class="js-price"]'))[0].getProperty('textContent')).jsonValue();
  var rate = await(await (await listelem[i].$x('td/span[@class="js-rate"]'))[0].getProperty('textContent')).jsonValue();
  record["giftcount"] = giftcount;
  record["facevalue"] = facevalue;
  record["pricevalue"] = pricevalue;
  record["rate"] = rate;
  list.push(record);
 }

 await page.close();

 return list;
}

// アマテン価格出力文字列
var pricecontents = "お待ち下さい\n";

/*
アマテン価格出力文字列作成
*/
function coockedprice( pricelist ) {

 var now = new Date();
 pricecontents = `${now.getFullYear()}年${now.getMonth()+1}月${now.getDate()}日${now.getHours()}時${now.getMinutes()}分${now.getSeconds()}秒`+"\n";
 for( var idx=0; idx<pricelist.length; idx++) {
  var record = pricelist[idx];
  pricecontents += `${record["rate"]}% ${record["giftcount"]}個 ${record["facevalue"]}円 → ${record["pricevalue"]}円`+"\n";
  if( idx>=5) {
   break;
  }
 }
}

/*
アマテン価格出力文字列作成タイマー
*/
setInterval(function(){
 amazonprice().then( val => coockedprice(val) );
},180000);

/*
Webサーバー起動
*/
function startserver() {
 const http = require('http');

 const hostname = '0.0.0.0';
 const port = 8088;

 const server = http.createServer((req, res) => {

  if( req.method == "GET") {
   switch(req.url) {
    case "/amaten":
     res.statusCode = 200;
     res.setHeader('Content-Type', 'text/plain; charset=utf-8');
     //amaten価格応答
     res.end(pricecontents);
    break;

   default:
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/plain');
    res.end("OK");
    break;
   }
  }
 });

 server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
 });
}

//Webサーバー開始
startserver();

init.sh

#/bin/bash

if [ ! -e node_modules ]; then
npm install puppeteer
fi

npm start

package.json

{
 "name": "amaten1",
 "version": "1.0.0",
 "description": "Amaten scraper",
 "main": "amaten.js",
 "directories": {
 "lib": "lib"
 },
 "dependencies": {
  "puppeteer": "^10.4.0"
 },
 "scripts": {
  "start": "node amaten.js"
 },
 "author": "DangerousWOO",
 "license": "ISC"
}

 

フォルダ構成

project

├Dockerfile

├docker-compose.yml

└project

  ├init.sh

  ├amaten.js

  └package.json

 

とまあ、こんな感じで作ってみました。

nodejsなんて、普段使わないので、ちょっと疲れました。

これは、seleniumじゃなくて、puppeteerというツールを使ってみました。

puppeteerの詳細は、ググって調べて下さいね

とりあえず、Chromeを起動して、ページ開いて、アマテンから価格情報を取って来ています。

Webサーバも起動していて、http://localhost:8088/amaten で、価格情報が取れるようになっています。

価格情報は、3分間隔で更新しています。

 

起動方法は、以下です。

docker-compose up -d

最初だけ、イメージを構築するので遅いです。


次にpython3版を作ってみましょう。

Dockerfile

FROM ubuntu
EXPOSE 8088
RUN apt -y update
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Tokyo
RUN apt install -y tzdata
RUN apt install -y curl python3 python3-pip
RUN pip install --upgrade pip
RUN curl -LO https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
RUN apt-get install -y ./google-chrome-stable_current_amd64.deb
RUN rm google-chrome-stable_current_amd64.deb
RUN apt install -y fonts-noto
RUN apt install -y python3-selenium unzip
WORKDIR /project

docker-compose.yml

version: "3"
services:
 scrapepy1:
  build:
   context: .
  ports:
   - "8088:8088"
  expose:
   - "8088"
  volumes:
   - ./project:/project
  tty: true
  working_dir: /project
  command: bash /project/init.sh
  deploy:
   resources:
    limits:
     memory: 256m

scrape.py

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import chromedriver_binary
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
import datetime
from http.server import HTTPServer, SimpleHTTPRequestHandler
from concurrent.futures import ThreadPoolExecutor

'''
AmatenよりAmazonギフト券価格を取得
'''
def amazonprice():
 options = Options()
 options.add_argument('--headless')
 options.add_argument('--disable-gpu')
 options.add_argument('--no-sandbox')
 options.add_argument('--disable-setuid-sandbox')
 options.add_argument('--window-size=1280x1024')

 driver = webdriver.Chrome(chrome_options=options)

 driver.get("https://amaten.com/exhibitions/amazon")

 WebDriverWait(driver,10).until(EC.presence_of_all_elements_located)
 time.sleep(1)

 list = []
 for elem_tr in driver.find_elements_by_xpath('//*[@id="contents_list"]/div[4]/table/tbody/tr'):
  record = {}
  giftcount = elem_tr.find_element_by_xpath('th/span[@class="js-gift-count"]')
  facevalue = elem_tr.find_element_by_xpath('td/span[@class="js-face_value"]')
  pricevalue = elem_tr.find_element_by_xpath('td/div/span[@class="js-price"]')
  rate = elem_tr.find_element_by_xpath('td/span[@class="js-rate"]')
  record["giftcount"] = giftcount.text
  record["facevalue"] = facevalue.text
  record["pricevalue"] = pricevalue.text
  record["rate"] = rate.text
  list.append(record)
  if len(list) > 5 :
   break

 driver.close()
 driver.quit()

 return list

'''
アマテン価格応答文字列
'''
content = 'お待ち下さい\n'

'''
Webサーバー起動
'''
class MyHandler(SimpleHTTPRequestHandler):
 def do_GET(self) -> None:

  body = ''
  if( self.path == '/amaten') :
   #amaten価格応答
   body = content.encode('utf-8')

  self.send_response(200)
  self.send_header('Content-type', 'text/application; charset=utf-8')
  self.send_header('Content-length',len(body))
  self.end_headers()
  self.wfile.write(body)

def StartWeb():
 host = '0.0.0.0'
 port = 8088
 httpd = HTTPServer((host, port), MyHandler)
 print('serving at port', port)
 httpd.serve_forever()

'''
アマテン価格応答文字列構築
'''
def coockedprice():
 global content
 dt_now = datetime.datetime.now()
 pricecontents = dt_now.strftime('%Y年%m月%d日 %H時%M分%S秒\n')
 for record in amazonprice():
  pricecontents += "{0}% {1}個 {2}円 → {3}円\n".format(record["rate"],record["giftcount"],record["facevalue"],record["pricevalue"])
 content = pricecontents

'''
メイン起動ループ
'''
with ThreadPoolExecutor(max_workers=2) as executor:
 #Webサーバスレッド開始
 executor.submit(StartWeb)
 #アマテン応答文字列構築ループ開始
 while True:
  executor.submit(coockedprice)
  time.sleep(60)

init.sh

#/bin/bash

webdriverexist=`pip list | grep chromedriver-binary | wc -l`

if [ $webdriverexist -eq 0 ]; then
 google-chrome --version
 #https://chromedriver.chromium.org/downloads
 pip install chromedriver-binary==94.0.4606.41
fi

python3 scrape.py

フォルダ構成

project

├Dockerfile

├docker-compose.yml

└project

  ├init.sh

  └scrape.py

 

Python3版も完成です。

Python3も普段使わないので、疲れますな

こっちは、ちゃんとSeleniumを使いましたよ

でも、WebDriverとChromeの互換性を一致させる必要があるので、init.shを修正する必要がありますね。

現代のプログラムって、どの言語も根源的な物は、ノイマン型コンピューターなんで、同じ様なもんですな

量子コンピューターだと、どうな風になるのかね