Severity table

Bad backlinks checker: diff snapshots in SQLite

スクリプトがあなたにとってリンクが有毒であると断定することはできません。それは、どの参照ドメインが新規で、どのドメインが消失し、どのドメインが手動で確認する価値があるほど不自然かを示すものです。

Stone vault with clay tablets and a brass caliper measuring data points

Severity table for new referring domains

Our own rules of thumb for triage, not a Google definition
LevelSignalAction
IgnoreNew domain, DR above 30, anchor is your brand or a bare URLLog it, move on
Look50 or more new domains in a week from one TLD you never targetedOpen ten of them
Look hardSame exact-match anchor on 20 or more new domainsCheck the anchor cloud, find the source
ActPaid-looking network or hacked pages pointing at youDocument it; consider a disavow file only if you also hold a manual action
WaitA domain vanished this monthRecheck after the next monthly refresh

Why diff instead of scanning once

A single snapshot tells you what is there. A diff tells you what changed, and that is where problems show up.

Google has said for years, since the Penguin 4.0 release in September 2016 and again in the December 2022 link spam update, that it mostly ignores spammy links instead of punishing them. SpamBrain does that work. So a flood of junk is more often noise than a threat, and the diff mainly saves you from reacting to noise.

Keep your own history. Suites overwrite theirs.

Build the history table in four steps

The diff only works if every run lands in the same table, in the same shape. These steps keep it that way.

01

Create the table once

Run a create-if-not-exists statement with the columns you need: the day, the domain and the score. Keep it in the script, so a new machine rebuilds the same table with no manual step.

02

Insert one row per domain per day

Use a parameterised insert and commit once per run. A single commit keeps a crash from leaving half a day in the file.

03

Guard against a second run

Delete the day's rows before inserting, or add a unique key on day and domain. A rerun on the same day then replaces the rows instead of doubling them.

04

Compare with the previous day on file

Pick the latest earlier date with a max(day) filter, not yesterday's date. The comparison stays correct when a week was skipped.

diff.py

Two tables would be tidier. One is enough for a hobby setup: a row per domain per snapshot date.

python
import json, sqlite3, sys, datetime
db = sqlite3.connect('links.db')
db.execute('create table if not exists snap(day text, dom text, dr int)')
rows = json.load(open(sys.argv[1]))
day = datetime.date.today().isoformat()
db.executemany('insert into snap values (?,?,?)',
               [(day, r.get('domain'), r.get('dr')) for r in rows])
db.commit()
prev = db.execute('select max(day) from snap where day < ?', (day,)).fetchone()[0]
if prev:
    new = db.execute('select dom from snap where day=? except select dom from snap where day=?', (day, prev)).fetchall()
    gone = db.execute('select dom from snap where day=? except select dom from snap where day=?', (prev, day)).fetchall()
    print(len(new), 'new,', len(gone), 'gone')

Troubleshooting the diff

SymptomLikely causeFix
Hundreds of 'gone' overnightIndex refresh swapped the sampleCompare monthly, not daily
Every domain looks newTable was empty or day format changedCheck select distinct day
DR column all NULLRow field name differs from drPrint one raw row and adjust
Duplicates after a rerunSame day inserted twiceDelete that day first, or add a unique key
Count below totalPagination stopped earlyFix the loop in the Actions runbook
Gardener observing ferns with a hand lens to compare growth patterns

Choosing thresholds from your own history

A threshold copied from a blog post is a guess about someone else's site. A bad backlinks checker is only as useful as its limits, and your own history tells you what normal looks like for your domain.

Start by logging a few months without acting on anything. Let it settle. Then write down the typical weekly swing. Only after that do you set the limits in the severity table.

Start with a baseline

Record new and gone counts for each run without reacting to them. A couple of months of quiet logging gives you a range that belongs to your domain and no one else's.

Once the baseline exists, a single bad week stands out against it, and you can explain the difference without guessing. That is the whole point of the baseline.

Pick one alarm per signal

Each signal in the table gets one threshold, one owner and one action. A signal with three alarms is a signal nobody reads, and the alarms end up ignored together.

Write the alarm next to its signal in the code, so the table on this page and the script never disagree.

Revisit the limits each quarter

Your site grows, and so does its normal. Recheck the limits every three months, and write the reason next to any change, so the history explains itself later.

Lighthouse keeper scanning horizon with telescope and anchor on workbench

Reading the severity table in practice

The table is a triage aid, not a judgement. Each row tells you where to look, and the action column keeps you from reaching for a disavow file in a panic.

Work from the top. Most weeks end at Ignore or Look. A Look row starts a few minutes of reading, and a Look hard row starts a longer review of the anchors. Do not skip rows because the week was busy. A skipped row is the week the flood goes unnoticed.

Sample before you decide

Open ten of the flagged domains and read the pages. A flood that turns out to be one directory scraper is a different problem from a flood of unrelated blogs, and only the pages tell them apart.

Record what you decided

Keep one line per decision: the domain, the level, what you checked and what you did. The next person to read the log, including future you, gets the reasoning without a meeting.

A monthly summary of those lines takes little time, and it makes a good first draft of a review.

What this cannot tell you

It cannot prove a link hurts you. Nobody outside Google can, and Search Console's manual actions report is the only place a penalty is stated.

It also cannot see links younger than the last refresh. For fresher evidence ask the people who run the linking site, or read your server logs for the referrers. Our note on timing goes deeper.

If you want the anchors side, anchor_cloud costs 2 credits and groups text by frequency, which is the quickest way to spot an exact-match pile-up.

Links that arrive between refreshes

Anything younger than the last refresh is missing from both sides of the diff. A link that appears and disappears inside one refresh window never shows at all, which is why the comparison should run monthly.

Anchors that look wrong to you

A diff sees the domain, not the sentence around the link. Two identical anchors on two different pages can mean very different things, so keep the anchor check as a separate, manual step.

Storage choices for a hobby setup

SQLite is the simplest database that still answers a diff question with one query. It lives in a single file, so it fits in the repo or a backup folder without a server.

Dated JSON files work too. It is slower to query, but easier to open in a text editor when something looks wrong. Either way, copy the store to a second place before any cleanup, and keep the copy for a full quarter before you delete anything.

When one file is enough

One table with a day column covers a single site for years. The file stays small, and the queries on this page need no tuning.

When to move to a server

Once several people write to the same history, a file on one machine becomes the bottleneck. At that point a managed database is the honest choice, and the schema stays the same.

Until then, a file in the repo is the simplest thing that survives a laptop change.

Wiring the diff to a weekly job

Run diff.py right after the snapshot step. Print the counts, and exit with a non-zero code when new crosses a limit you picked, say 200 in a week for a small site. The job turns red, and GitHub emails you. Simple.

Pick the limit from your own history. After two months you know whether 15 new domains a week is normal or a spike, and any fixed number copied from a blog post is a guess.

Quiet weeks are the norm.

Bad link questions, answered plainly

Is there a reliable bad backlinks checker?
No tool can label a link harmful with certainty, because only Google knows how it treats each one. A script can surface patterns worth reading: sudden floods, repeated exact anchors, odd TLDs. You make the call after opening the pages.
Should I disavow what the diff flags?
Rarely. Google has said it largely ignores spam links, and a disavow file is meant for cases such as a manual action or a paid-link mess you made yourself. Document first, and check Search Console before you submit anything.
How often should I run it?
Snapshot weekly, review monthly. The index refreshes about once a month, so a daily diff mostly compares a table with itself. A monthly review matches the data and keeps the noise down.
Which columns should the table keep?
Keep the day, the domain, the DR and a source column. Add the anchor only if you pull anchors too. More columns feel thorough, but each extra field is another one that can drift between runs.
Can I diff two suites against each other?
You can compare their counts for the same site, but the two results will not match. Run the diff inside one source, and compare the direction of change across sources, the same rule that applies everywhere on this site.
What if a domain leaves and then returns the next month?
It shows as gone and then as new, which is noise. Count returns separately, or compare three months at a time, so a domain that drops out for one refresh does not trigger a review.