0
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?

クロス投稿は重複コンテンツか — 一致率を測ったら散文の一致は393行中3行だった

0
Posted at

同じ記事を Zenn・Qiita・dev.to に出していると、そのうち「これ重複コンテンツ扱いされていないか」が気になります。自分は気になった勢いで「外部の投稿を一時的に消す」手順書まで書きました。

実行前に、本文がどれくらい一致しているのかを数えました。11記事・22ファイルで、散文の一致は393行中3行。うち2行は水平線の --- でした。手順書は不要になり、作業は canonical を付けられるプラットフォームに付けるだけになりました。

測り方と、canonical を指定できる/できないプラットフォームの一覧をまとめます。

一致率は行単位で数えて、種別で割る

やることは3つだけです。

  1. frontmatter を落とす
  2. 空白を正規化して空行を捨てる
  3. コードフェンスの内側かどうかで行に種別を付ける

3番目が結論を左右します。付けないと「一致率48%」という数字だけが出て、その48%が何なのか分かりません。

import fs from "node:fs";

const norm = (s) => s.replace(/\s+/g, " ").trim();

function bodyOf(file) {
  let t = fs.readFileSync(file, "utf8");
  if (t.startsWith("---")) t = t.slice(t.indexOf("\n---", 3) + 4); // frontmatter を落とす
  return t;
}

// 行を [正規化テキスト, 種別] に分解する
function classify(text) {
  const out = [];
  let inFence = false;
  for (const raw of text.split("\n")) {
    const line = norm(raw);
    if (/^```/.test(line)) { inFence = !inFence; out.push([line, "code"]); continue; }
    if (!line) continue;
    if (inFence) { out.push([line, "code"]); continue; }
    if (/^#{1,6}\s/.test(line)) { out.push([line, "heading"]); continue; }
    if (/^\|/.test(line)) { out.push([line, "table"]); continue; }
    out.push([line, "prose"]);
  }
  return out;
}

// 原本側の行を集合に入れて、外部側の各行が含まれるかを見る
const origin = new Set(classify(bodyOf(originFile)).map(([l]) => l));
const ext = classify(bodyOf(externalFile));
const matched = ext.filter(([l]) => origin.has(l));

const count = (k) => matched.filter(([, t]) => t === k).length;
console.log({
  ext: ext.length,
  matched: matched.length,
  code: count("code"),
  table: count("table"),
  heading: count("heading"),
  prose: count("prose"),
});

inFence のトグルを省いて行頭の記号だけで分類すると、シェルの # コメントや YAML・SQL の | を含む行が散文に化けます。信じたい数字がそこなので、ここは省けません。

空行を落とすのも先に決めておきます。空行は必ず一致するため、残すと分母も分子も膨らみます。

結果: 一致795行のうち762行はコードだった

自分の11記事(EN / JA を別ファイルとして22ペア)に通した結果です。

指標 値
外部記事の総行数 1,658
原本と一致した行 795(47.9%)
うちコードフェンス内 762
うち表 13
うち見出し 17
うち散文 3
外部記事の散文行 393
散文の一致率 0.8%

一致した散文3行を印字したら、2行は水平線の ---、1行は英語の箇条書きが1行でした。

一致率が高い記事ほどコードの割合が高い、という関係になります。手順やコードを丁寧に載せた記事ほど数字が跳ねるので、比率だけ見ると読み違えます。コードが一致するのは同じ実装を説明しているからで、外部向けに書き換えたらどちらかが嘘になります。

書き下ろしで運用しているなら、まずこの数字を出してから対応を決めるのがおすすめです。

canonical を指定できるプラットフォーム、できないプラットフォーム

原本を宣言する手段は揃っていません。API の受け口を見ると早いです。

プラットフォーム canonical 指定場所
dev.to 可 記事作成 API の article.canonical_url
Hashnode 可 publishPost mutation の originalArticleURL
Medium 可 投稿 API の canonicalUrl、または Import from URL
Qiita 不可 items API に該当パラメータが無い
Zenn 不可 記事 frontmatter に該当項目が無い
note 不可 エディタに設定箇所が無い

dev.to はForem API の記事オブジェクトに canonical_url があるので、ペイロードに1行足すだけです。

body: JSON.stringify({
  article: {
    title,
    body_markdown: body,
    published: false,
    tags,
    canonical_url: "https://example.com/blog/<slug>/",
  },
}),

Qiita は API v2 の items 作成が title / body / tags / private / tweet / organization_url_name で、外部 URL を原本として宣言する項目はありません。Zenn の記事 frontmatter も title / emoji / type / topics / published / publication_name で同じです。

ハマりどころ: Hashnode の下書きには canonical を渡せない

originalArticleURL は publishPost の入力にはありますが、createDraft の入力にはありません。下書きで作って管理画面から公開する運用だと、canonical が付かないまま公開されます。

const input = publish
  ? { title, contentMarkdown, publicationId, tags, originalArticleURL: canonical }
  : { title, contentMarkdown, publicationId, tags };   // draft には入れられない

dev.to も、canonical は投稿時のペイロードで確定します。すでに投稿済みの記事には後から PUT /articles/{id} で入れるか、エディタで設定し直す必要があります。

「一時的に消す」が選べないプラットフォームがある

canonical を設定できないプラットフォームでは、一度消したあとに戻す方法が「もう一度投稿する」しかありません。記事 URL は変わり、それまでの閲覧数も被リンクも切れます。可逆に見えて実際は削除です。

外部を消す判断をする前に、まず一致率を測る。30行のスクリプトで済んで、結果次第でその後の作業が全部消えます。


実測に至った経緯(AdSense の審査結果を検索側の重複判定と混同していた話)、チャネル別の閲覧数を並べた撤回の判断材料、記事ごとの一致率の内訳は Aulvem 本家にまとめました → Aulvem|クロス投稿は重複コンテンツか — 消す前に一致率を測ったら散文の一致は3行だった

0
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
0
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?