1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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<EventInput[]> {
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 テーブルカレンダー形式
// <td class="day_..."> 内にイベント名と時刻が入る
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;
}
|