1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

オプチャグラフ開発記⑥ 20年前のSEOスパム「キーワードスタッフィング」を正規表現で退治する

Last updated at Posted at 2025-12-28

はじめに

オプチャグラフは、LINE OpenChatの統計情報を収集・分析・可視化するWebサービスです。

前回までの記事:

1. 背景:20年前の手法が繰り返されている

キーワードスタッフィングとは

「キーワードスタッフィング(Keyword Stuffing)」とは、検索結果の上位表示を狙って、ページ内に大量のキーワードを詰め込む手法です。

Googleの公式スパムポリシーでは、以下のようなパターンが該当するとされています:

  • 同じキーワードを不自然に何度も繰り返す
  • 都市名・地域名のリストを羅列する
  • 関連キーワードを文脈なく大量に並べる
【同じキーワードの繰り返し】
安い靴をお探しですか?当店の安い靴は最も安い靴です。
安い靴を買うなら安い靴の専門店で安い靴をお買い求めください。

【地域名・関連キーワードの羅列】
石狩 札幌 かみのやま 富谷 野々市 射水 入間 新三郷 つくば 前橋
30代 40代 50代 60代 主婦 一人暮らし 節約 ポイ活 お得

Googleでの歴史:すでに「終わった」手法

キーワードスタッフィングは、1990年代後半〜2000年代初頭のSEO黎明期には実際に効果がありました。当時の検索エンジンは単純で、キーワードの出現回数が多いほど、またより多くのキーワードを含むほど上位に表示されていたのです。

しかし、Googleは段階的にこの手法を無効化してきました:

アップデート 内容
2003年 Florida キーワードスタッフィングへの最初の本格的対策
2011年 Panda 低品質コンテンツ・スパムサイトの排除
2013年 Hummingbird 自然言語処理の導入、文脈理解の向上
2019年 BERT 高度な自然言語処理により文脈・意図を理解

現在、キーワードスタッフィングはGoogleの「スパムに関するポリシー」で明確に禁止されており、発覚すると検索順位の低下やインデックスからの完全削除といったペナルティを受けます。

SEOの世界では20年以上前に「終わった」手法なのです。

オープンチャットで繰り返される歴史

ところが、LINEオープンチャットの説明文では、この古典的なスパム手法が今も横行しています。

オープンチャットにはLINE公式の検索機能があり、説明文に含まれるキーワードで検索できます。そこで一部のユーザーは、検索でヒットさせるために関連・無関連を問わず大量のキーワードを詰め込むのです。

検索アルゴリズムが単純な環境では、20年前と同じ問題が繰り返される ── これがオプチャグラフでキーワードスタッフィング対策を実装した背景です。


2. 対策の概要

実際の例

コストコオプチャ最大規模のメンバーです!
コストコのあれこれ、㊙️情報まで盛りだくさん✨
よろしくお願い致します🙇‍♀️

コストコ costco Costco 主婦 一人暮らし 節約 ポイ活 お得 ブランド お買い得 
爆買い 大家族 フードファイター 矢田亜希子 ジャンクSPORTS 業務スーパー KALDI
石狩、札幌、かみのやま 富谷 野々市 射水 入間 新三郷 つくば 前橋 ひたちなか
30代 40代 50代 60代 20代 きゅん スティッチ

前半は普通の説明文ですが、後半は明らかに検索用のキーワードを詰め込んでいます。

このようなスパムを放置すると、以下の問題が発生します:

  • サイト内検索の品質低下:検索結果がスパム的なオープンチャットで埋まってしまう
  • GoogleのSEOスパム判定リスク:キーワードスタッフィングはGoogleのスパムポリシー違反であり、サイト全体の評価が下がる恐れがある
  • ユーザー体験の低下:説明文が読みにくくなる

なぜ機械学習ではなく正規表現か?

  • 月額1,000円のサーバーで25万件を処理する制約
  • パターンの調整がしやすい(新しいスパムに即対応可能)
  • 判定理由を説明できる

📝 初期コミット: 763456cb

🔗 関連リンク

リソース URL
実装コード CollapseKeywordEnumerations.php
テストコード CollapseKeywordEnumerationsTest.php

3. 日本語の「キーワードらしさ」を判定する

3.1 ひらがな率による判定

日本語の特徴として、キーワード(名詞)はひらがなが少なく、文章はひらがなが多いという傾向があります。

テキスト ひらがな率 判定
カフェ 0% キーワード ✅
マッキンゼー 0% キーワード ✅
Bitcoin 0% キーワード ✅
これは 100% 文章の一部 ❌
食べました 60% 文章の一部 ❌
についての 80% 文章の一部 ❌

この法則を使い、ひらがな率30%以下をキーワードと判定します。

$hiragana = preg_match_all('/\p{Hiragana}/u', $t);
return ($hiragana / max(1, $letters)) <= 0.3;

🔗 コード: isKeywordLike() L316-L384

3.2 英語への対応

英語の場合は別のルールが必要です:

パターン 判定 理由
the, and, for 文章 ❌ 一般的な英単語
Apple, Microsoft キーワード ✅ 大文字を含む(企業名)
Ernst & Young キーワード ✅ &記号を含む
test, word, long 文章 ❌ 4文字以上の小文字のみ

3.3 具体例:スペース・読点区切りのキーワード羅列

この判定を使って、以下のようなパターンを検出・削除します。

【入力】
コストコ costco Costco 主婦 一人暮らし 節約 ポイ活 お得 ブランド お買い得 
爆買い 大家族 フードファイター 矢田亜希子 ジャンクSPORTS 業務スーパー KALDI

【出力】
(削除される)
【入力】
石狩、札幌、かみのやま、富谷、野々市、射水、入間、新三郷、つくば、前橋、
ひたちなか、千葉ニュータウン、幕張、木更津、多摩境、金沢シーサイド

【出力】
(削除される)

判定理由: 各トークンがひらがな率30%以下(カタカナ・漢字・英語)で、12個以上並んでいる


4. 文章とキーワード羅列を見分ける

4.1 助詞の存在をチェック

日本語の文章には必ず助詞(が、を、に、へ、で、と、や...)が含まれます。

$particleMatches = preg_match_all('/[がをにへでとやからまで]/u', $matchedText);

🔗 コード: isSentenceLike() L386-L441

4.2 読点・縦棒の数と助詞の比率

判定ルール:

条件 判定
読点5個以上 かつ 助詞が読点の半分以下 キーワード羅列
縦棒5個以上 かつ 助詞が縦棒の1/3以下 キーワード羅列
100文字以上 かつ スペース10個以上 かつ 助詞密度0.02未満 キーワード羅列
上記以外で助詞が含まれる 文章(保持)

📝 コミット: 3f9ae1d7 - スペース区切りの検出を追加

4.3 具体例:利用ルール vs 企業名羅列

縦棒区切りでも、文章として意味があるものは保持します。

【入力:利用ルール】
敬語で会話すること|建設的な議論を行うこと|情報共有を抑制する発言は禁止|
秘密保持を求められた場合はその事実のみ共有すること

【出力】
敬語で会話すること|建設的な議論を行うこと|情報共有を抑制する発言は禁止|
秘密保持を求められた場合はその事実のみ共有すること
(そのまま保持される)

判定理由: 「で」「を」「の」などの助詞が多く含まれる → 文章と判定

【入力:企業名羅列】
コンサル/シンクタンク|マッキンゼー|BCG|ベイン|ATカーニー|PwC|
Deloitte デロイト|KPMG|EY|アクセンチュア|IBM|NRI 野村総合研究所

【出力】
(削除される)

判定理由: 助詞がほとんどなく、各トークンがキーワード的 → キーワード羅列と判定


5. ハッシュタグの重複削除

5.1 本文と重複するハッシュタグは削除

【入力】
これはコストコの情報共有グループです #コストコ #お買い得 #節約

【出力】
これはコストコの情報共有グループです #お買い得 #節約

判定理由: #コストコ は本文に「コストコ」が含まれるため冗長 → 削除。他は本文に含まれないため保持。


6. 埋め込み型キーワード羅列への対応

6.1 問題:文章の中にキーワード羅列がある

単純なキーワード羅列だけでなく、文章の途中に埋め込まれたキーワード羅列もあります。

【入力】
ビットコイン、NFT、チリーズ、SAND、ENJIN、リップル、ネム、IOST、ステラ、
イーサリアム、エイダ、トロン、バージ、ポルカドット、モナコイン、ライトコイン、
リスク、オミセゴー、ファクトム、バイナンスコイン、テザー、ビットコインキャッシュ、
モネロ、テゾス、VeChain、BAT、QTUM、LINK、EOS、NEO、DOGE、IOTA、DASH、AVAX、
FIL、NANOなどのアルトコインから草コインまでの仮想通貨のチャートを予想するグループです。

この場合、「〜などの」という接続部分を検出して処理します。

6.2 「など」を目印にする

// 「など」「から」「まで」「といった」の直前でマッチ
$embeddedPattern = '/([^。!?\n\r]*[、,,][  ]*[^、,,。!?;:\n\r]+
    (?:[、,,][  ]*[^、,,。!?;:\n\r]+){' . $embeddedMinCount . ',})
    (?=など|から|まで|といった|について)/u';

🔗 コード: processEmbeddedKeywords() L443-L530

📝 コミット: c440a66d - 埋め込み型処理を追加

6.3 keepFirst パラメータ:先頭N個を残す

埋め込み型の場合、全削除すると文章が不自然になります。そこで keepFirst パラメータで先頭N個を残し、残りを「etc…」に省略できます。

【入力】
ビットコイン、NFT、チリーズ、SAND、ENJIN、リップル...NANOなどの
アルトコインから草コインまでの仮想通貨のチャートを予想するグループです。

【出力(keepFirst=1の場合)】
ビットコイン、etc…などのアルトコインから草コインまでの
仮想通貨のチャートを予想するグループです。

【出力(keepFirst=0の場合)】
などのアルトコインから草コインまでの
仮想通貨のチャートを予想するグループです。

7. 正規表現のエラーハンドリング

7.1 PHPの正規表現関数の落とし穴

PHPの preg_* 関数は、エラー時に例外を投げずに特殊な値を返すという罠があります。

関数 正常時 エラー時
preg_replace() 置換後の文字列 null
preg_split() 配列 false
preg_match_all() マッチ数 false

これを見逃すと、本番環境で予期しないエラーが発生します。

7.2 対策

// 対策1: 戻り値のnullチェック
$content = preg_replace('/[##][^\s##]+/u', '', $text);
if ($content === null) {
    $content = $text; // エラー時は元の文字列を使用
}

// 対策2: パラメータの範囲制限
if ($minItems < 1) $minItems = 1;
if ($minItems > 10000) $minItems = 10000;

// 対策3: エラーハンドラーでWarningをキャッチ
set_error_handler(function () {
    return true; // Warningを無視
}, E_WARNING);
// ...処理...
restore_error_handler();

🔗 コード: エラーハンドリング L109-L293

📝 コミット: 83b31144 - エラーハンドリング強化


8. まとめ

日時 コミット 内容
8/23 16:09 763456cb 初期実装(ひらがな率判定、ハッシュタグ処理)
8/23 17:17 83b31144 エラーハンドリング強化
8/23 3f9ae1d7 複数段落処理、スペース区切り検出
8/23 18:33 c440a66d 埋め込み型キーワード羅列(etc…)対応
8/24 17:26 cbeab961 インターフェース追加、DI対応

機械学習を使わない選択のメリット:

  • 低コスト:追加のインフラ不要
  • 説明可能:なぜその判定になったか追跡できる
  • 調整しやすい:新しいスパムパターンに即対応可能
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?