From f817604858891edb79e26459dae884b158774db1 Mon Sep 17 00:00:00 2001 From: yyamashita Date: Wed, 6 May 2026 22:20:00 +0900 Subject: =?UTF-8?q?Add=204=20new=20venue=20scrapers:=20Meets=20=E5=A4=A7?= =?UTF-8?q?=E5=A1=9A,=20WARP=20=E5=90=89=E7=A5=A5=E5=AF=BA,=20FLAT=20?= =?UTF-8?q?=E8=A5=BF=E8=8D=BB=E7=AA=AA,=20Pitbar=20=E8=A5=BF=E8=8D=BB?= =?UTF-8?q?=E7=AA=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit meets-otsuka: rinky.info プラットフォーム。div.blog-entry.event-wrap[event-date] から日付・タイトル・出演者・時間・価格・チケットURLを取得。 warp-kichijoji: WordPress カスタムテーマ。

YYYY
MM

で 年月を取得、article.schedules-box から各イベントをパース。 flat-nishiogikubo: Wix サイトのため JS レンダリング必須。エラーを返す プレースホルダー実装(Playwright 等への移行が必要)。 pitbar-nishiogikubo: freecalend.com (mem25771) から取得を試みるが、 ボット遮断のため現状はエラー。URL パターン・代替策をコメントに記載。 SCRAPE_TARGETS.md に状態列(✅/⚠️)を追加。 Co-Authored-By: Claude Sonnet 4.6 --- app/scrapers/pitbar-nishiogikubo.ts | 101 ++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 app/scrapers/pitbar-nishiogikubo.ts (limited to 'app/scrapers/pitbar-nishiogikubo.ts') diff --git a/app/scrapers/pitbar-nishiogikubo.ts b/app/scrapers/pitbar-nishiogikubo.ts new file mode 100644 index 0000000..5c70023 --- /dev/null +++ b/app/scrapers/pitbar-nishiogikubo.ts @@ -0,0 +1,101 @@ +/** + * Pitbar 西荻窪 — http://freecalend.com/open/mem25771_date{YYYYMM} + * + * スケジュールは Ameblo (https://ameblo.jp/pitbar/) 経由で + * freecalend.com に掲載されているが、自動リクエストをブロックしている。 + * + * 代替案: + * - User-Agent を設定したヘッドレスブラウザで freecalend を取得 + * - 公式 Instagram / X (@pitbar_nishiogi) の投稿を取得 + * - 手動でイベントを登録する管理画面を用意する + * + * 月ごとの URL パターン: http://freecalend.com/open/mem25771_date{YYYYMM} + */ +import type { Scraper, VenueMeta } from "./base"; +import type { EventInput } from "~/lib/db.server"; + +export const venue: VenueMeta = { + id: "pitbar-nishiogikubo", + name: "Pitbar 西荻窪", + url: "https://ameblo.jp/pitbar", + area: "西荻窪", +}; + +const FREECALEND_MEMBER = "25771"; + +export const scraper: Scraper = { + venue, + async scrape(): Promise { + const months = upcomingMonths(2); + const events: EventInput[] = []; + + for (const ym of months) { + const url = `http://freecalend.com/open/mem${FREECALEND_MEMBER}_date${ym}`; + const res = await fetch(url, { + headers: { + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/124 Safari/537.36", + Referer: "https://ameblo.jp/pitbar/", + }, + redirect: "follow", + }); + if (!res.ok) continue; + + const html = await res.text(); + if (!html.trim()) continue; + + // freecalend は HTML テーブルカレンダー形式 + // 内にイベント名と時刻が入る + const { load } = await import("cheerio"); + const $ = load(html); + + $("td[class*='day_']").each((_, el) => { + const $el = $(el); + const text = $el.text().trim(); + if (!text || /^\d+$/.test(text)) return; // 日付のみのセルはスキップ + + const dayMatch = $el.attr("class")?.match(/day_(\d+)/); + if (!dayMatch) return; + const day = dayMatch[1].padStart(2, "0"); + const date = `${ym.slice(0, 4)}-${ym.slice(4)}-${day}`; + + const lines = text.split(/[\n\r]+/).map((l) => l.trim()).filter(Boolean); + const title = lines[0] ?? text.slice(0, 100); + + const timeMatch = text.match(/(\d{1,2}:\d{2})/g); + const openTime = timeMatch?.[0] ?? null; + const startTime = timeMatch?.[1] ?? null; + + events.push({ + venue_id: venue.id, + title, + date, + open_time: openTime, + start_time: startTime, + source_url: url, + }); + }); + } + + if (events.length === 0) { + throw new Error( + "Pitbar freecalend からデータを取得できませんでした。" + + "freecalend.com が自動リクエストをブロックしている可能性があります。" + ); + } + + return events; + }, +}; + +function upcomingMonths(count: number): string[] { + const months: string[] = []; + const now = new Date(); + for (let i = 0; i < count; i++) { + const d = new Date(now.getFullYear(), now.getMonth() + i, 1); + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, "0"); + months.push(`${y}${m}`); + } + return months; +} -- cgit v1.2.3