MarketLens AI とは
個人・フリーランス・中小企業向けの市場調査補助ツールです。
「競合他社が最近何をしているか調べたい」「業界ニュースを毎日チェックしたい」という需要は多いですが、専門の調査会社に依頼するコストを払えない層は多くあります。MarketLens AI はその層向けに、ニュースサイトや企業プレスリリースなどから情報を自動で収集し、カテゴリ分類・要約・感情分析をかけて画面上で読みやすく整理して表示するツールです。「全自動で完璧なレポートが出てくる」というより、毎日の情報収集・整理の手間を減らすことを目指しています。
技術スタックは Python(FastAPI + Celery + Redis)+ React + Docker 構成です。
今回のアップデートで解決した問題
「AI が見出ししか読めていなかった」
このツールは収集した記事を AI に渡して分類・要約を行う設計にしています。ところが実装当初、情報収集の処理がニュースサイトの一覧ページからメタ情報を取得するだけで、記事の中身はほとんど取れていませんでした。
実際の取得内容はこんな状態でした:
| 情報源 | 取得内容の例 | 文字数 |
|---|---|---|
| あるプレスリリースサイト | "9分前\n会社名" |
16文字 |
| ニュースサイトA | "[カテゴリ] タイトル" |
32文字 |
| ニュースサイトB | タイトルのみ | 36〜88文字 |
16文字の「9分前 会社名」を AI に渡しても、分析できる内容はゼロです。ツールとして成立していませんでした。
改修内容(v11)
一覧ページ → 記事詳細ページへの変更
各情報源の収集処理を 「一覧ページのメタ情報を取得」から「記事詳細ページの本文を取得」 する方式に全面改修しました。
# 改修前:一覧ページから title, url のみ取得
def parse(self, response):
for item in response.css(".article-list li"):
yield {"title": item.css("a::text").get(), "url": item.css("a::attr(href)").get()}
# 改修後:記事URLをたどって本文まで取得
def parse(self, response):
for item in response.css(".article-list li"):
url = item.css("a::attr(href)").get()
yield response.follow(url, callback=self.parse_detail)
def parse_detail(self, response):
yield {"title": ..., "content": response.css("article.main-content").get()}
結果として取得できる内容が大きく変わりました:
| 情報源 | 改修前 | 改修後 |
|---|---|---|
| プレスリリースサイト | 16文字 | 6,510文字 |
| ニュースサイトA | 88文字 | 1,568文字 |
| ニュースサイトB | 36文字 | 1,178文字 |
| ECサイト商品ページ | 116文字 | 1,151文字 |
取得済みデータの再収集対応
改修前に取り込んだレコードは内容が薄いまま残っています。save_article のロジックを変更し、同じ記事でもコンテンツが 100 文字以上増加した場合は上書きして再分析対象(status='pending')に戻すようにしました。
-- 改修前
INSERT INTO articles (...) ON CONFLICT (url) DO NOTHING;
-- 改修後
INSERT INTO articles (...) ON CONFLICT (url) DO UPDATE
SET content = EXCLUDED.content,
status = 'pending',
scraped_at = NOW()
WHERE length(EXCLUDED.content) - length(articles.content) > 100;
情報源の死活監視機能を追加("ドクターイエロー")
22ある情報源がいつの間にか壊れていても、ログを掘らないと気づきません。そこで 全情報源を 12 時間ごとに自動チェックする監視機能 を追加しました。プロジェクト内では新幹線の線路点検車にならって "ドクターイエロー" と呼んでいます。
HEALTH_STATUS = Literal["healthy", "degraded", "stale", "down"]
@app.task
def check_scraper_health():
for target in scrape_targets.all():
recent = articles.filter(target=target, scraped_at__gte=12h_ago)
if not recent.exists():
status = "down"
elif recent.avg_content_len() < target.min_content:
status = "degraded"
else:
status = "healthy"
notify_if_changed(target, status)
- healthy → degraded/down に変化したタイミングで Webhook・メール通知
-
GET /api/v1/system/scraper-healthで現在の状態を一覧取得できる
テスト:API 338 / DB 68 / 定期実行 51 = 457/457 PASS
GUI の改善(v12 — 36項目)
情報収集が動くようになってきたので、管理画面(React SPA)の使い勝手も整えました。主な変更点:
ローディング中の表示(Skeleton)
// 改修前:データ取得中は何も表示されない
{isLoading ? null : <KpiCard data={data} />}
// 改修後:枠線アニメーションで「読み込み中」を表現
{isLoading ? <Skeleton variant="rectangular" height={120} /> : <KpiCard data={data} />}
空状態の統一表示
データが 0 件のとき空のテーブルが表示される問題を解消。全 6 ページに <EmptyState message="まだデータがありません" /> を統一適用しました。
相対時間表示
// "2026-06-07T09:00:00+09:00" → "昨日" or ホバーで正確な日時
<Tooltip title={formatDateTime(date)}>{formatRelative(date)}</Tooltip>
詰まっていた情報源の修復(v14)
v11 の改修後も 2 つの情報源が取得件数ゼロのままでした。
パターン①:レスポンス形式の読み違い
# APIが [{...}] のリスト形式で返すのに、オブジェクトとして処理していた
data = response.json() # [{...}]
items = data.get("ResultList") # None → 全件スキップ
# 修正後
data = response.json()
items = data[0].get("ResultList", [])
パターン②:User-Agent による弾き返し
# 修正前:独自UAでアクセス → CDN の bot-protection にブロックされる
custom_settings = {"USER_AGENT": "MarketLensBot/1.0"}
# 修正後:通常ブラウザと同じ UA を使用
custom_settings = {"USER_AGENT": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."}
2つ合わせて 108件/回 の取得が可能になり、全 22 情報源が healthy 状態になりました。
現在の状態
| 項目 | 状態 |
|---|---|
| 全22情報源 | healthy |
| テスト | 457/457 PASS(API 338 / DB 68 / 定期実行 51) |
| 取得本文量 | 16〜116文字 → 521〜6,510文字 |
情報収集の基盤がようやく使い物になってきました。次は収集した記事を AI がどう分析・整理するかの精度向上に取り組む予定です。