この記事は ただの集団 AdventCalendar PtW.2019 の1日目の記事です。
明日は kzone さんの DjangoをTerraformでFargateにデプロイする話 です。
はじめに
今までは求人サイトから求人情報をクロールして集めようとしても、各サイトで作りがバラバラなので自動で正確なデータを収集することが困難だった。
しかし、 "Google for jobs" がJobPosting形式のクローリングを始めたことで、各求人サイトが求人情報をJobPosting形式で掲載するようになってきたため、求人情報クローリングが今までより簡単になるかもしれない。
JobPosting構造化データとは?
- JobPosting構造化データ とは、検索エンジン用に様々な構造化データを定義している schema.org が作った求人用の構造化データ定義。
- 現在は34項目程度のプロパティがある。
- Microdata, RDFa, JSON-LDの3形式に対応している。
JSON-LDの場合の例
<script type="application/ld+json">
{
"@context": "http://schema.org/",
"@type": "JobPosting",
"name": "Mobile App Developer",
"hiringOrganization": {
"@type": "Organization",
"name": "ACME Software",
},
"relevantOccupation": {
"@type": "Occupation",
"name": "Software Developers, Applications",
"occupationalCategory": "15-1132.00"
}
}
</script>
JobPostingに対応している求人サイト
「求人」でGoogle検索して上位にきたサイトのうち、ログインしないと求人情報が見れないサイトを除いた上位30サイトを調査したところ21サイトが対応していて、7割のサイトが既に対応済みということになる。
特筆すべきは対応していた全サイトがGoogle推奨のJSON-LD形式であること。
GoogleのJobPostingの扱い
- Googleの求人情報に関する構造化データ に詳細が書いてある。
必須プロパティ
プロパティ名 | 内容 | 所感 |
---|---|---|
datePosted | 雇用主が求人情報を投稿した最初の日付(ISO 8601 形式)。 | 前回と変更があるかの確認や新着表示のために使えそう |
description | HTML形式での求人の詳細な説明。 | HTML形式なので色々な表現ができそう |
hiringOrganization | 職位を提供している組織。 | ロゴも含められるようなので視認性も豊かになりそう |
jobLocation | オフィスや作業現場など、従業員の職場となる特定の場所(求人情報を投稿した場所ではない)。 | かなり柔軟に細かく入力できるみたい |
title | 職務の名称(求人情報のタイトルではない)。 | 目立つ文言を入れたくなるけど職務の名称のみが推奨されている |
validThrough | 求人情報が期限切れになる日付(ISO 8601 形式)。 | 必須と言いつつ有効期限がある場合のみで良いみたい |
推奨プロパティ
プロパティ名 | 内容 | 所感 |
---|---|---|
applicantLocationRequirements | 従業員がリモートワークを行うために所在する必要のある地域。 | リモートワークに関する項目が推奨されているのがGoogleらしい |
baseSalary | 雇用主から提示された実際の基本給(概算額ではない)。 | 時給〜年収、min〜maxなど細かく指定できる |
employmentType | 雇用形態。 | 雇用形態を知らずに応募できないので必須でも良い気がする |
identifier | 求人に関する採用側組織の一意の識別子。 | 掲載側でIDが付けられるのは嬉しい |
jobLocationType | 業務時間中、自宅など本人が選択した場所で常にリモートワークする求人の場合、このフィールドに TELECOMMUTE を設定します。 | リモートワークに関するプロパティが多い |
JobPosting構造化データを取得する
Scalaとjsoupを使って実際にJobPosting構造化データを取得してみる。
ソースは job-postiong-crawl-sample にアップした。
import org.jsoup.Jsoup
import io.circe._, io.circe.parser._
object Main extends App {
val detailUrl = "https://cookbiz.jp/job/job54024.html"
val jobPostingScriptTag = Jsoup.connect(detailUrl).get().select("script[type=\"application/ld+json\"]")
val jobPostingJson = parse(jobPostingScriptTag.html()) match { case Right(json) => json }
// required values
println("datePosted=[%s]".format(extract(jobPostingJson, "datePosted")))
println("description=[%s]".format(extract(jobPostingJson, "description")))
println("hiringOrganization=[%s]".format(extract(jobPostingJson, "hiringOrganization", "name")))
println("jobLocation=[%s]".format(extract(jobPostingJson, "jobLocation", "addressRegion")))
println("title=[%s]".format(extract(jobPostingJson, "title")))
println("validThrough=[%s]".format(extract(jobPostingJson, "validThrough")))
// recommended values
println("applicantLocationRequirements=[%s]".format(extract(jobPostingJson, "applicantLocationRequirements")))
println("baseSalary=[%s: %s~%s]".format(extract(jobPostingJson, "baseSalary", "unitText"), extract(jobPostingJson, "baseSalary", "minValue"), extract(jobPostingJson, "baseSalary", "maxValue")))
println("employmentType=[%s]".format(extract(jobPostingJson, "employmentType")))
println("identifier=[%s.%s]".format(extract(jobPostingJson, "identifier", "name"), extract(jobPostingJson, "identifier", "value")))
println("jobLocationType=[%s]".format(extract(jobPostingJson, "jobLocationType")))
def extract(json: Json, keys: String*) = {
val result = keys.foldLeft(json)((j, k) => j.\\(k) match {
case head :: _ => head
case _ => Json.Null
})
if (result == Json.Null) "" else result.toString
}
}
実行結果
datePosted=["2019-04-26"]
description=["◆大人から子どもまで、みんなが満たされる大衆寿司居酒屋「回転寿司だと子どもたちは楽しめるけど、お父さんはちょっと物足りない…」「カウンターの寿司店だと、子どもを連れていくには懐も雰囲気も気がきでない」そんなファミリー層もお腹と舌を満たせるような寿司店をめや盛り付け、寿司の提供を習得してください。対面式カウンターなので、調理をしながら接客や会話もお願いします。また、ホールでの接客も経験してください。店長就任後は、シフト管理・棚卸・発注・計数管理など店舗運営全般をお任せします。◆たくさんの「楽しい!」が生まれる場所としすることで、お客さまがスタッフに話しかけてくれます。そこからコミュニケーションが生まれ、お店の雰囲気もにぎやかになるんです。提供した料理を見た瞬間「なにこれ!」と驚いたり喜んだり。とにかくその場にいるみんなが「楽しい!」と思えるお店づくりを心掛けています。お客さまに喜んでもらえる仕組みをたくさん作っていきませんか?"]
hiringOrganization=["株式会社スシロークリエイティブダイニング"]
jobLocation=["東京23区"]
title=["店舗スタッフ(店長・副店長候補)"]
validThrough=[]
applicantLocationRequirements=[]
baseSalary=["MONTH": 306000~370800]
employmentType=["FULL_TIME"]
identifier=["株式会社スシロークリエイティブダイニング"."54024"]
jobLocationType=[]
値の入っている項目は全て取得できた。
かなり汎用的な作りが可能になりそう。
まとめ
今まで正確に自動化することが難しかった求人情報クローリングの分野に"Google for Jobs"が参入したことによって、JobPosting構造化データでの求人公開が一般的になりつつあり、今後は自動化がかなり楽になりそう。
参考にした本・サイト
クローリングハック あらゆるWebサイトをクロールするための実践テクニック
Webスクレイピングの注意事項一覧
JobPosting
Googleの求人情報に関する構造化データ