使い方
各スニペットは独立した関数として定義。必要なものだけコピーして使う。
共通の前提として、以下の定数が定義済みとする。
const API_ID = process.env.API_ID;
const AFF_API = process.env.AFFILIATE_ID_API; // 末尾990〜999
const AFF_SITE = process.env.AFFILIATE_ID_SITE; // 通常ID(ブログ掲載用)
1. fetchItems — 商品情報の取得
用法: 任意の検索条件で商品情報APIを叩く汎用関数。全スニペットの土台。
思想: パラメータをオブジェクトで受け取り、未指定キーは送信しない。呼び出し側の負担を最小にする。
計算量: O(1)(1回のHTTPリクエスト)
async function fetchItems(opts = {}) {
const p = new URLSearchParams({
api_id: API_ID, affiliate_id: AFF_API,
site: opts.site ?? "FANZA", output: "json",
});
for (const k of ["service","floor","hits","offset","sort","keyword","cid","gte_date","lte_date"]) {
if (opts[k] != null) p.set(k, String(opts[k]));
}
// article / article_id は配列対応
(opts.article ?? []).forEach((v, i) => p.set(`article[${i}]`, v));
(opts.article_id ?? []).forEach((v, i) => p.set(`article_id[${i}]`, String(v)));
const res = await fetch(`https://api.dmm.com/affiliate/v3/ItemList?${p}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()).result;
}
2. fetchAll — 全件取得(ページネーション)
用法: fetchItems をラップし、offset を自動で進めて全件(または上限件数)を回収する。
思想: API の 1 リクエスト最大 100 件制限を隠蔽。呼び出し側は件数を気にしなくてよい。
計算量: O(⌈N/100⌉) HTTPリクエスト(N = 全体件数、最大 offset 50000)
async function fetchAll(opts = {}, maxItems = 500) {
const items = [];
let offset = 1;
while (items.length < maxItems) {
const hits = Math.min(100, maxItems - items.length);
const result = await fetchItems({ ...opts, hits, offset });
if (!result.items?.length) break;
items.push(...result.items);
offset += hits;
if (offset > result.total_count) break;
}
return items;
}
3. fetchNewReleases — 直近N日の新着取得
用法: fetchNewReleases(7) で過去7日間の新着を発売日順で返す。
思想: アフィリエイトブログは鮮度が命。日次バッチの定形パターンを1行で呼べるようにする。
計算量: O(1)(内部で fetchItems を1回呼ぶ)
async function fetchNewReleases(days = 7, floor = "videoa", hits = 30) {
const since = new Date();
since.setDate(since.getDate() - days);
return fetchItems({
service: "digital", floor, sort: "date", hits,
gte_date: since.toISOString().slice(0, 19),
});
}
4. searchByActress — 女優IDで商品を絞り込む
用法: 女優検索APIで得た actress_id を渡して、その女優の商品一覧を取得する。
思想: 女優単位の特集記事はCVRが高い。女優ID→商品の導線を最短で繋ぐ。
計算量: O(1)
async function searchByActress(actressId, opts = {}) {
return fetchItems({
service: "digital", floor: "videoa", sort: opts.sort ?? "date",
hits: opts.hits ?? 20,
article: ["actress"],
article_id: [actressId],
});
}
5. swapAffiliateId — アフィリエイトIDの差し替え
用法: APIレスポンス内の全URLを、ブログ掲載用の通常IDに一括置換する。
思想: APIリクエスト用ID(990〜999)とリンク掲載用IDは別物。置換を忘れると報酬が入らない。
計算量: O(N)(N = items数)
function swapAffiliateId(items) {
return items.map((item) => ({
...item,
affiliateURL: item.affiliateURL?.replace(AFF_API, AFF_SITE),
}));
}
6. filterByReview — レビュー評価でフィルタ
用法: filterByReview(items, 4.0, 3) で「平均4.0以上 かつ レビュー3件以上」の商品だけ残す。
思想: レビュー高評価の商品は信頼性が高く、記事で紹介しやすい。低評価や件数不足のノイズを除外する。
計算量: O(N)
function filterByReview(items, minAvg = 3.5, minCount = 1) {
return items.filter((item) => {
const r = item.review;
return r && Number(r.average) >= minAvg && Number(r.count) >= minCount;
});
}
7. groupByGenre — ジャンル別グルーピング
用法: 取得した商品配列をジャンルIDごとにまとめる。ジャンル別ランキング記事の素材作りに使う。
思想: 1回のAPI取得結果を複数記事に分割できれば、API呼び出し回数を節約できる。
計算量: O(N × G)(N = items数、G = 1商品あたりの平均ジャンル数。通常 G ≤ 10)
function groupByGenre(items) {
const map = new Map();
for (const item of items) {
for (const g of item.iteminfo?.genre ?? []) {
if (!map.has(g.id)) map.set(g.id, { name: g.name, items: [] });
map.get(g.id).items.push(item);
}
}
return map;
}
8. rateLimitedBatch — レートリミット付きバッチ実行
用法: 複数の非同期関数を一定間隔で順次実行する。APIの連続リクエストでBANされないための安全装置。
思想: DMM APIに公式のレートリミット仕様は明記されていないが、短時間に大量リクエストを送るのはマナー違反であり、アカウント停止リスクがある。
計算量: O(N)(N = タスク数。合計実行時間は N × interval ms)
async function rateLimitedBatch(tasks, interval = 1000) {
const results = [];
for (const task of tasks) {
results.push(await task());
await new Promise((r) => setTimeout(r, interval));
}
return results;
}
// 使用例: 複数女優の商品を1秒間隔で取得
// const ids = [15365, 26225, 1044248];
// const results = await rateLimitedBatch(
// ids.map((id) => () => searchByActress(id)),
// 1000
// );
9. buildItemCard — 商品カードHTML生成
用法: 1商品分の紹介HTMLを生成する。ブログのテンプレートに合わせて編集して使う。
思想: API取得→HTML生成を関数化しておけば、記事の体裁変更はこの関数だけ直せばよい。
計算量: O(1)
function buildItemCard(item) {
const url = item.affiliateURL ?? "#";
const img = item.imageURL?.large ?? item.imageURL?.small ?? "";
const price = item.prices?.price ?? "";
const review = item.review ? `★${item.review.average}(${item.review.count}件)` : "";
const genres = (item.iteminfo?.genre ?? []).map((g) => g.name).join(" / ");
return `<div class="item-card">
<a href="${url}" rel="nofollow" target="_blank">
<img src="${img}" alt="${item.title}" loading="lazy" />
</a>
<h3><a href="${url}" rel="nofollow">${item.title}</a></h3>
<ul>
${price ? `<li>${price}</li>` : ""}
${review ? `<li>${review}</li>` : ""}
${genres ? `<li>${genres}</li>` : ""}
${item.date ? `<li>${item.date}</li>` : ""}
</ul>
</div>`;
}
10. fetchFloorMap — フロアコード逆引きマップ生成
用法: フロアAPIの結果をフラットな Map<code, {service, name}> に変換する。サービス・フロアコードの手打ちミスを防ぐ。
思想: DMM のフロア構造はサイト→サービス→フロアの3階層。API を叩く度にコードを調べるのは非効率なので、起動時にマップを1回作ってキャッシュする。
計算量: O(S × F)(S = サービス数、F = サービスあたりのフロア数。全体で数十件程度)
async function fetchFloorMap() {
const p = new URLSearchParams({
api_id: API_ID, affiliate_id: AFF_API, output: "json",
});
const res = await fetch(`https://api.dmm.com/affiliate/v3/FloorList?${p}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const map = new Map();
for (const site of data.result.site) {
for (const svc of site.service ?? []) {
for (const floor of svc.floor ?? []) {
map.set(floor.code, {
site: site.code, service: svc.code,
floorName: floor.name, serviceName: svc.name,
});
}
}
}
return map;
}
// 使用例:
// const floors = await fetchFloorMap();
// const info = floors.get("videoa");
// → { site: "FANZA", service: "digital", floorName: "ビデオ", serviceName: "動画" }
組み合わせ例
上記スニペットを組み合わせた日次バッチの骨格。
async function dailyUpdate() {
// 1. 過去3日の新着を取得
const result = await fetchNewReleases(3, "videoa", 100);
if (!result.items?.length) return;
// 2. レビュー高評価のみ抽出
const quality = filterByReview(result.items, 3.5, 2);
// 3. アフィリエイトID差し替え
const ready = swapAffiliateId(quality);
// 4. ジャンル別に分割
const byGenre = groupByGenre(ready);
// 5. ジャンル別にHTML生成
const fs = await import("node:fs/promises");
await fs.mkdir("./output", { recursive: true });
for (const [id, { name, items }] of byGenre) {
const cards = items.map(buildItemCard).join("\n");
const html = `<h2>${name}</h2>\n<div class="item-grid">\n${cards}\n</div>`;
await fs.writeFile(`./output/genre-${id}.html`, html, "utf-8");
}
}
dailyUpdate().catch(console.error);