Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save kako-jun/5de1ffb435ec25f52234ae72181965ef to your computer and use it in GitHub Desktop.
Save kako-jun/5de1ffb435ec25f52234ae72181965ef to your computer and use it in GitHub Desktop.
石川県の猫犬里親募集ページをスクレイピングしてみた

kako-junです

JAISTが近いのでaptが速いです


殺処分の部屋の横で 里親募集されてた捨て猫をもらってきたよー

さっそく 猫駆動開発するよー


日本では 捨て猫の里親募集は

国でなく地方自治の管轄なので

募集ページのデザインが 統一されてないのは 無理もありませぬ


猫を保護しながら 職員が更新してるので

スクレイピングしやすいよーな 意味を構造化したよーな

今ふうの マークアップにはなってないんよ


なので パーサーの開発が難しいのか

自動的に巡回して 日本全国で募集中の猫を 一覧にするよーな

クローラーは ググっても見つからんかった


国が作っても越権行為だし

県が ほかの県のまで作っても越権行為だし

公的には解決できない

これって 絶妙な課題やんー♪


Nodeでcheerioを使ったスクレイピングなど

日本の技術共有サイトには 情報があるのに

ネタが、ニュースの取得とかSNSとか 経済とか統計とか

興味の持てないサンプルばかりで


私が興味持てるのは猫だけ

なので、作ってみた

以下が、石川県の保護センターの募集ページを スクレイピングできるHTMLパーサーやよ


'use strict';

const _ = require('lodash');
const cheerio = require('cheerio');

class Parser {
  static parse(html, default_species) {
    const $ = cheerio.load(html);

    // common
    let no_new_dog = false;
    let no_new_cat = false;

    // h2
    $('#tmp_contents h2').each((i, el) => {
      const temp = $(el).text().trim();
      if (temp) {
        if (temp.match(/犬.*いません/)) {
          no_new_dog = true;
        }

        if (temp.match(/猫.*いません/)) {
          no_new_cat = true;
        }
      }
    });

    const level1s = [];
    const details = [];
    $('#tmp_contents p, #tmp_contents ul').each((i, el) => {
      // console.log(el.name);
      switch (el.name) {
        case 'p':
          // 更新日:2019年3月1日
          // 募集期限:平成31年2月14日
          // 現在里親募集中の犬はいません
          // 現在里親募集中の猫はいません
          // 1,メス(譲渡決定2/27)
          // 2.オス(譲渡決定2/26)
          // ※1/30譲渡
          // 仮名:クロちゃん
          // 譲渡決定2/26
          const temp = $(el).text().trim();
          if (temp) {
            level1s.push({
              i,
              p: temp
            });
          }

          break;
        case 'ul':
          // 種類:雑種
          // 体格:小
          // 年齢:1ヶ月半
          // 毛色:黒茶
          // 色:キジトラ(長毛)
          // 性別:オス (去勢手術済)
          // その他:ノミダニ駆除・内部寄生虫駆除済、2種混合ワクチン接種済
          const lis = [];
          $(el).find('li').each((i, el) => {
            // console.log($(el).text());
            const temp = $(el).text().trim();
            if (temp) {
              lis.push(temp);
            }
          });

          if (lis) {
            details.push({
              i,
              lis,
            });
          }

          break;
      }
    });

    // console.log('level1s');
    // console.log(level1s);

    // common
    let updated_at = '';
    let species = '';
    let left = '';

    let first_dog_i = 99;
    let first_cat_i = 99;

    // updated_at
    let found = _.find(level1s, (level1) => {
      return level1.p.match(/更新日/);
    });

    if (found) {
      updated_at = found.p;
    }

    // left
    found = _.find(level1s, (level1) => {
      return level1.p.match(/募集.*期限/);
    });

    if (found) {
      left = found.p;
    }

    // no_new_dog, first_dog_i
    let foundIndex = _.findIndex(level1s, (level1) => {
      return level1.p.match(/犬.*いません/);
    });

    // console.log(foundIndex);
    if (foundIndex >= 0) {
      no_new_dog = true;
      first_dog_i = foundIndex;
    }

    // no_new_cat, first_cat_i
    foundIndex = _.findIndex(level1s, (level1) => {
      return level1.p.match(/猫.*いません/);
    });

    // console.log(foundIndex);
    if (foundIndex >= 0) {
      no_new_cat = true;
      first_cat_i = foundIndex;
    }

    console.log(no_new_dog);
    console.log(no_new_cat);

    let posts = [];
    _.each(level1s, (level1, i) => {
      let target = false;
      let name = '';
      let sex = '';
      let age = '';
      let pattern = '';
      const images = [];
      let status = '';

      // if (level1.p.length < 20) {
      if (!level1.p.match(/犬猫/) && !level1.p.match(/ること/) && !level1.p.match(/の流れ/)) {
        // name
        if (level1.p.match(/仮名/)) {
          target = true;
          name = level1.p.trim();
        }

        // sex
        if (level1.p.match(/メス/)) {
          target = true;
          sex = 'f';
        } else if (level1.p.match(/オス/)) {
          target = true;
          sex = 'm';
        }

        // status
        if (level1.p.match(/譲渡/)) {
          target = true;
          status = level1.p;
        }

        if (target) {
          if (i > first_dog_i && i < first_cat_i) {
            species = 'dog';
          } else if (i > first_cat_i && i < first_dog_i) {
            species = 'cat';
          }

          const post = {
            i: level1.i,
            updated_at,
            species,
            name,
            sex,
            age,
            pattern,
            images,
            left,
            status,
          };

          posts.push(post);
        }
      }
    });

    // details
    _.each(details, (detail, i) => {
      const found_post = _.find(posts.slice().reverse(), (post) => {
        return post.i < detail.i;
      });

      if (found_post) {
        // age
        let found = _.find(detail.lis, (li) => {
          return li.match(/年齢/);
        });

        if (found) {
          found_post.age = found;
        }

        // pattern
        found = _.find(detail.lis, (li) => {
          return li.match(//);
        });

        if (found) {
          found_post.pattern = found;
        }

        // sex
        found = _.find(detail.lis, (li) => {
          return li.match(/性別/);
        });

        if (found) {
          found_post.sex = found;
        }
      }
    });

    // console.log('details');
    // console.log(details);

    posts = _.map(posts, (post, i) => {
      delete post.i;
      return post;
    });

    // merge
    posts = _.map(posts, (post, i) => {
      // ※1/30譲渡
      // 仮名:トラちゃん
      // のパターン
      if (post.status !== '' && post.name === '' && post.sex === '' && post.age === '' && post.pattern === '') {
        if (i + 1 <= posts.length - 1) {
          posts[i + 1].status = post.status;
          return null;
        }
      }

      return post;
      // });
    }).filter(Boolean);

    // console.log('posts');
    // console.log(posts);
    return posts;
  }
}

// class variables

module.exports = Parser;

ちょうど募集されてた クロちゃん、トラちゃんが

良いサンプルになってくれたわ

譲渡おめでとうです


これで

{
  "updated_at": "更新日:2019年1月31日",
  "species": "cat",
  "name": "仮名:クロちゃん",
  "sex": "性別:オス(去勢手術済)",
  "age": "年齢:推定8歳以上",
  "pattern": "色:黒",
  "images": [],
  "left": "募集期限:平成31年2月14日",
  "status": "※1/30譲渡",
  "id": "pref-17-ishikawa_01_foster-all",
  "pref_id": "pref-17-ishikawa",
  "center_id": "pref-17-ishikawa_01",
  "url": "http://www.pref.ishikawa.lg.jp/minamikaga/toppage/seikatu/inunekojyouto.html",
  "purpose": "foster"
}

のようなJSONが採れます。


こうやって 1保護センターあたり1パーサーずつ書いていけば

いまこの瞬間に 処分のカウントダウンされてる猫が

日本のどこに何匹いるかが すぐ分かるワケで


それを表にした サイトを公開してもイイし

Feedで 昨日と今日の差分に気づけるよーにしてもイイし

Web APIを公開して 自由にBotを作れるよーにしてもイイし

まず スクレイピング手順さえ確立すれば

いろいろOSSでやりたい人はいると思うんよ


なので

日本中の里親募集ページを巡回する クローラーを

誰も作る予定が無いなら 私が作ろーと思うんよ

巡回は 1日4回、6時間おきかなー


日本には47都道府県あり

石川県には保護センターが5つあるので、 ほかの県も同じよーな数とすると

47 * 5 = 235

(こーゆー場合、私は5 * 47とは書かないの)


石川県の場合、 5つの募集ページを作ってる企業は同一のようで

工夫すれば 1つのパーサーでイケた


なので 日本を網羅するには

最小で47個のパーサーを作ればイイだろうし

最大でも200個程度に収まると思うんよ


ただ、それを調べてる最中に ゲゲッってなったこと

  • 里親募集とは別に、迷い猫を掲示するページも県のサイト内にある
  • 迷い猫の掲示ページは、市でも持ってるところがある(石川県では金沢市だけ)

これらも いずれは網羅したいわ


まぁ 迷い猫は

飼い主がページを見つけて探すだろうし

里親募集ページを先にやるわ


まず 日本の都道府県、市区町村の

すべての保護センターとURLをリストアップするわ

静的なHTMLで作った人は いたよーなんだけど

更新されてなくて

載ってないセンターがたくさんあった


同じ轍を踏まないよーに Google Sheetsとか

持続可能な形式で共有するわ


そして 県ごとのパーサー作り(残り46個)を、46個のIssueにする

誰もやらなきゃ 徐々に私が作ってくけど


猫好き(ついでに犬も)でOSSな人が

Pull Requestの練習や、草を生やすついでに

気軽に参加できるよーにしたいと思うの

夏休みの宿題にどうでしょう

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment