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の練習や、草を生やすついでに
気軽に参加できるよーにしたいと思うの
夏休みの宿題にどうでしょう