スクレイピングとJSONの書き込み/読み込みで作った「自動販売機の原材料とアレルギー情報がわかるサイト」の内容解説

このページはこちらのサービスの内容の解説(忘備)記事になります。

abrd.site

このサイトはその名の通り自動販売機に入っている商品の原材料名やアレルギーがわかるサービスを行っています。

そもそもこれを作った理由は「自販機は買う前に裏面を確認できないから怖い」というアレルギー持ちの友人がいたため。

新商品や自分で覚えている定番商品以外を選べないそうです。

そんな友人の小さな生き辛さを改善できればとコツコツ作っているのですが、ある程度形になったので忘備録の意味も込めてどのように作っていったかを書いていきたいと思います。

 

まず、流れとしてはこんな形です。

1.各社サイトから商品情報と商品画像をスクレイピングする

2.その情報をJSONデータで記録

3.JSONを読み込んでサイトに表示

スクレイピング

さすがに手作業でコピペしていくのは骨なので、スクレイピングで情報を一気に取得します。

スクレイピングと言ったらPythonが有名ですが、今回はphpQueryというjQuery風にDOM操作ができるというPHP用ライブラリを使って行いました。

 

想定しているのはこのように商品情報がtableになっているページです。

f:id:inubayashi:20180822112122p:plain

 

まずはphpQueryを使うためのファイルを読み込みます。

そして読み込み先のページを指定・内容を読み込み、phpQueryが扱える形にしましょう。

<?php

require_once("phpQuery-onefile.php");

$html ="https://●●●.html"; // 取得するwebページの指定 $html =file_get_contents($html); // 指定したページの内容を読み込む $dom = phpQuery::newDocument($html);

スクレイピングする準備ができたので早速やっていきます。

$count= count($dom["table. tr"]);
// 行数を取得

for($i=0;$i<=$count;$i++){
 // (行数)まで$iをインクリメント
 $title=$dom["tr:eq($i) td:eq(0) a"]->text();
 // tr:eq($i)で行を上から指定しその中の文字列を取得

 $data = $dom["tr:eq($i) td:eq(1)"]->text();
 // 同じように原材料名を取得

 $allr = $dom["tr:eq($i) td:eq(2)"]->text();
 // アレルギー情報を取得

情報を取得するにはその情報が位置する要素やclass、idを指定して行く必要があります。

例えばこれなら

$data = $dom["tr:eq($i) td:eq(1)"]->text();

「($i)行目の2つめのtd要素から情報を取得」、という意味になります。(●●:eq(数字)は「●●の(数字)目の要素」という意味。数字は0が1つ目の要素に対応)

 

今回はテーブルから繰り返し取得するので、ループできるようにまずはtr(行)の数をcount()で取得。

そしてforを使って先程取得した[総tr数]になるまで回します。

 これだけだと文字データだけなので画像もスクレイピングしましょう。

$src = $dom["tr:eq($i) td:eq(0) img"]->attr("src");
// 画像のパスを取得

 同じようにテーブルから情報を取得しますが、これまでとは違いattr("src")でsrc属性の値を選択します。

こうすることで画像が配置してあるパス情報を手に入れます。

次は画像が保存される際の名前と保存先を決めましょう。

$fileName = "img".$i.".jpg";
// 画像の名前 $filePath = "../images/".$fileName;
// 画像の保存先
$imgData = @file_get_contents($src);

ファイル名はtrの順番と対応させたいと思います。

これで必要な情報を取得する準備はOK。

扱いやすいようにJSONとして保存します。

 if($imgData){
  @file_put_contents("$filePath", $imgData);
 }
 $jsonArray[] = ['title'=> $title, 'data'=>$data, 'allr'=>$allr, 'filePath'=>$filePath];
 // 以上の情報を配列に格納
 $jsonData = json_encode($jsonArray, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
// 日本語を文字化けさせない&インデントさせる file_put_contents("data.json",$jsonData); sleep(1); }

書き出されたJSONファイルはこのようになります。

[
    {
        "title": "プレミアムボス",
        "data": "牛乳、コーヒー、砂糖、ぶどう糖、乳加工品、カゼインNa、香料、乳化剤 ",
        "allr": "乳 ",
        "filePath": "../images/suntory/img1.jpg"
    },
    {
        "title": "ボス レインボーマウンテンブレンド",
        "data": "牛乳、砂糖、コーヒー、乳製品、デキストリン、カゼインNa、乳化剤、香料 ",
        "allr": "乳 ",
        "filePath": "../images/suntory/img2.jpg"
    },
    {
        "title": "ボス 贅沢微糖",
        "data": "牛乳、コーヒー、砂糖、乳製品、デキストリン、カゼインNa、乳化剤、香料、甘味料(アセスルファムK) ",
        "allr": "乳 ",
        "filePath": "../images/suntory/img3.jpg"
    },
    {
        "title": "ボス 無糖ブラック",
        "data": "コーヒー ",
        "allr": " ",
        "filePath": "../images/suntory/img5.jpg"
    },
... ]

これでスクレイピングは完了です。

 

全体のコードはこちら。

問題やより効率的な方法がありましたらコメントお願いします。

<?php

require_once("phpQuery-onefile.php");

$html ="https://●●●.html";
// 取得するwebページの指定

$html =file_get_contents($html);
// 指定したページの内容を読み込む
$dom = phpQuery::newDocument($html);
 
$count= count($dom["table. tr"]);
// 行数を取得

for($i=0;$i<=$count;$i++){
 // (行数)まで$iをインクリメント
 $title=$dom["tr:eq($i) td:eq(0) a"]->text();
 // tr:eq($i)で行を上から指定しその中の文字列を取得

 $data = $dom["tr:eq($i) td:eq(1)"]->text();
 // 同じように原材料名を取得

 $allr = $dom["tr:eq($i) td:eq(2)"]->text();
 // アレルギー情報を取得

$src = $dom["tr:eq($i) td:eq(0) img"]->attr("src");
// 画像のパスを取得

$fileName = "img".$i.".jpg";
// 画像の名前
$filePath = "../images/".$fileName;
// 画像の保存先
$imgData = @file_get_contents($src);

if($imgData){
  @file_put_contents("$filePath", $imgData);
 }
 $jsonArray[] = ['title'=> $title, 'data'=>$data, 'allr'=>$allr, 'filePath'=>$filePath];
 // 以上の情報を配列に格納
 $jsonData = json_encode($jsonArray, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
// 日本語を文字化けさせない&インデントさせる
 file_put_contents("data.json",$jsonData);
 sleep(1);
}

出力ページ

次は実際の出力ページに取り掛かります。

まずは先程のJSONを読み込みましょう。

<?php
$jsonUrl = "data.json";
if(file_exists($jsonUrl)){
$json = file_get_contents($jsonUrl);
// 内容を読み取り $json = mb_convert_encoding($json, 'UTF8', 'ASCII,JIS,UTF-8,EUC-JP,SJIS-WIN');
// エンコードする $obj = json_decode($json,true);
// 内容を取り出す } ?>

JSONの内容を$objに取り出したので、今度はhtml上に表示します。ちなみにBootstrapを使っています

      <div class="row">
        <?php foreach($obj as $key => $val): ?>
        <div class="ddata">
          <div class="col-3 dimage">
            <img src="<?php echo $val["filePath"]; ?>" onerror="this.src='../images/vm/no-image.png'">
          </div>
          <div class="product">
            <h2>
              <?php echo $val["title"]; ?>
            </h2>
              <div class="card card-body">
                <p>
                  <span>原材料名:
                  </span>
                  <?php echo $val["data"]; ?>
                </p>
                <p>
                  <span>アレルギー:
                  </span>
                  <?php echo $val["allr"]; ?>
                </p>
              </div>
          </div>
        </div>
        <?php endforeach; ?>
      </div>
      <!-- /row -->

 html内でforeachを使うので、表示したい部分を

<?php foreach($obj as $key => $val): ?>

<?php endforeach; ?>

で囲みます。

画像表示部分:

<img src="<?php echo $val["filePath"]; ?>" onerror="this.src='../images/vm/no-image.png'">

さきほどの画像のファイルパスをimgタグ内でechoします。

もとから画像が用意されていない商品は404エラーになるので、

onerror="this.src='../images/vm/no-image.png'

 この部分でエラー時に表示するノーイメージ画像を指定しています。

 後はそれぞれ同じように商品名、原材料名、アレルギー物質をechoします。

CSSで形を整えて、最終的にはこのように表示されます。

f:id:inubayashi:20180822103009p:plain

 

しかしこのままでは全部が表示されて画面を圧迫し見づらいです。

最初は商品名と画像以外折りたたんでおいて、タップをしたらその他の情報を表示するようにjQueryで制御します。(最初はBootstrapのcollapse機能を使って折り畳みをしたのですが、PCではできたのになぜかスマホでは動作せず…)

   $(function(){
        $('h2').click(function(){
          $(this).next(".card").slideToggle();
        });
      });

 これでスマホでも一覧して見やすくなりました。

f:id:inubayashi:20180822105312p:plain

 

↓タップ後

f:id:inubayashi:20180822105433p:plain

これから

現状このサイトはここまでです。

「各社商品をまとめて原材料/アレルギー情報を表示する」という目的は達成できていますが、ズラッと商品が並んでいるためすぐに探しづらいという欠点があります。

そのためこれからは商品ブランドごとにもっと細かく分けたり、検索機能などを追加していくつもりです。

更に使いやすくして、自分の友人のような問題を解決するようなサービスにしたいと思います。

 

PHPで作ったサービスの中身解説

このページはこちらのサービスの内容の解説(忘備)記事になります。

abrd.site

内容としては入力された文字列に対して、その文字列にマッチする猫のgif画像を出力、さらにその猫に関するプロフィールをデータベースから取得し、HTMLに表示する、という形になります。

f:id:inubayashi:20180801150646g:plain

PHPの練習用として制作しましたが、どうせなら(自分のプログラミングスキルでも)使用者に楽しんでもらえる物を作りたい、と思い「感情を体現」とフックのあるキーワードにユーモラスに動く猫、というクスリと笑ってもらえるようなシュールなテイストでまとめました。

プログラムと画像はすべて自作、Bootstrap4で作られているのでレスポンシブ対応です。

 

トップ画面

form action="feeling/cats.php" method="GET">
<p><input type="text" name="feeling" placeholder="今の気持ちをひらがなで入れてね" size="30"></p>
<input type="submit" value="にゃんこ体現" class="btn btn-primary btn-l">
</form>

まずはトップページ。

ここは「自分の気持ち」の文字列を入力する部分です。
重要な処理はaction先のページで処理します。

methodをGETにしたのは入力情報をURLに表示させてツイッターなどで呟けるようにするためです。

入力ボタンを押したら次のページに飛びます。


結果表示ページ

<?php
$data = [];
$feeling = htmlspecialchars($_GET['feeling'], ENT_QUOTES, "UTF-8");

$positive01 = array('かわいい' , 'ねこ' , 'にゃんこ', 'たのしい' , 'よい' , 'よき', 'いい', 'つよい', 'つよみ','ばぶみ', 'ごきげん', 'むてき', 'さいこう', 'かっこいい', 'いけいけ', 'ぱりぴ', 'りあじゅう', 'ようきゃ', 'わらい', 'かんしゃ', 'しあわせ', 'そうかい', 'ゆかい', 'かんき', 'えくすたしー', 'ぜっさん', 'きぼう', 'ばんざい', 'ときめき', 'さつい', 'うれしい');
$negative01 = array('かなしい' , 'いかり', 'つらい' , 'つらみ' , 'つかれた', 'だめ', 'げつようび', 'しゃちく', 'いたい', 'ひりあじゅう', 'いんきゃ', 'いらいら', 'ふこう' , 'まけいぬ', 'なく', 'しつぼう', 'みじめ', 'むなしい', 'ふゆかい', 'げきど', 'いらだち', 'きもい', 'きもちわるい', 'ぶさいく','くさい','くさそう');
$surprise = array('おどろき', 'びっくり', 'なんと', 'まっど', 'くれいじー', 'なげき', 'ぜつぼう', 'いかり', 'やばい' , 'こわい', 'どうよう', 'しょうげき','すごい');

 

まずは前ページから受け取った情報を取得します。ENT_QUOTESで特殊文字を無害にして$feelingに代入。

ポジティブ・ネガティブその他いろいろな言葉を感情ごとにグループ分けして配列に入れます。

この感情に合わせて猫の画像を出すので、arrayの数だけ表示する猫の画像が必要になるわけです。

 

foreach($positive01 as $words){
  if(strpos($feeling , $words) !== false){
    $feeling_img = 'kawaii';
    $cat_id = 'watanabe';
    $feeling_txt = '<span>' . $feeling . '</span>' .'<br>という気持ち';
  }
}

foreach($negative01 as $words){
  if(strpos($feeling , $words) !== false){
    $feeling_img = 'tsurami';
    $cat_id = 'nakamura';
    $feeling_txt = '<span>' . $feeling . '</span>' .'<br>という気持ち';
  }
}

foreach($surprise as $words){
  if(strpos($feeling , $words) !== false){
    $feeling_img = 'surprise';
    $cat_id = 'shimizu';
    $feeling_txt = '<span>' . $feeling . '</span>' .'<br>という気持ち';
  }
}

foreachを使って先程の配列を$wordsに代入。繰り返し処理させます。

if(strpos($feeling , $words) !== false)

で(入力された文字列)が(配列)内にあったらif内を実行。

$feeling_imgに代入される文字列はimgフォルダ内にあるgif画像のファイル名になります。

$cat_idには猫の名前が入ります。

のちのちデータベースからこの名前にマッチするものを引っ張る用のものです。

$feeling_txtは画像の下に表示される言葉です。

 

if(!isset($feeling_img)){
  $feeling_img ='notfound';
  $cat_id = 'notfound';
  $feeling_txt = 'ねこには少しむずかしい言葉でした。<br>(他の言葉を入れてね)';
}
if($feeling === ''){
  $feeling = 'notfound';
  $feeling_txt = '気持ちを入力してね';
}
  if(mb_strlen($feeling) > 10){
    $feeling_img = 'notfound';
    $feeling_txt = '長い言葉は難しかったみたい<br>(10文字以内で入力してね)';
  }

入力した文字列が配列になかったり、空だったり、不正な文章を入れられないようにする処理の部分です。

最初のif文は「もしこれまでの流れで$feeling_imgに何も入ってなかったらそれぞれにnotfoundを代入・他の言葉を入れるよう促す」という処理。

次のif文は言葉が入ってないときの場合。同じくnotfound猫を出します。

最後のif文は入力された文字列が10字以上だった場合の処理。notofound猫と警告文を表示します。

とりあえず前準備はここまで。次はデータベースに接続します。

データベースから猫の情報を引っ張る

 $dsn = 'mysql:dbname=●●●;host=●●●;charset=utf8';
 $user= '●●●';
 $password ='●●●';

 try{

  $dbh = new PDO($dsn, $user, $password);
  $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  $sql = "SELECT cat_name, cat_introduction FROM cats WHERE cat_id LIKE :cat_id";
  $stmt = $dbh->prepare($sql);
  $stmt->bindValue(':cat_id', '%'.$cat_id.'%', PDO::PARAM_STR);
  $stmt->execute();
  while($row = $stmt->fetch(PDO::FETCH_ASSOC)){
    $data[] = $row;
  }

 }catch (PDOException $e){
 echo('接続エラー:'.$e->getMessage());
 die();
}

事前に作っていた猫の情報を登録しているデータベース(mySQL)に接続します。

 

$cat_idを持つ猫を探し、猫の名前(cat_name)と猫の紹介文(cat_introduction)をcatsテーブルから引っ張ってきます。

 

 <img src="../img/<?php echo $feeling_img; ?>.gif" class="img-fluid mx-auto d-block catimg">
      <p class="catfeeling text-center">
        <?php echo $feeling_txt ?> </p>
      <table border="1" class="table table-bordered table-sm align-middle cat-table table-light">
        <?php foreach($data as $row): ?>
        <tbody>
          <tr>
            <td class="cat-icon align-middle"> <img src="../img/<?php echo $cat_id ?>.jpg"></td>
            <td class="cat-name align-middle">
              <?php echo $row['cat_name']; ?> </td>
            <td class="align-middle">
              <?php echo $row['cat_introduction']; ?> </td>
          </tr>
        </tbody>
        <?php endforeach; ?> </table>

データベースから引っ張ってきた情報をHTML本体に表示させます。

(bootstrapで作っているのでclassが煩雑です…。)

$feeling_imgでgif画像を表示、$feeling_txtでそれに合わせた文章を出すします。

データベースからのデータをtable内に表示させます。

 

<p class="small">このねこちゃんをツイートする</p>
<a href="https://twitter.com/share" class="twitter-share-button" data-text="<?php echo $feeling; ?>ねこちゃん #にゃんこうちのう" data-size="large">Tweet</a>

twitterのツイート文もechoで入力された文が出るよう記述してあります。

titleタグも同様です。

 

仕組みとしては大まかにこのような形になっています。

よろしければ是非つぶやいてみてください。

  

abrd.site