每个数字营销机构都有人负责这样的工作:打开电子表格,访问每一个客户的网站地址,检查标题标签、元描述以及H1标签,记录下所有失效的链接,然后将这些信息整理成报告。下周再重复这个过程。
这种工作具有确定性,任何员工都能完成它。
在本教程中,你将使用Python、Browser Use以及Claude API从零开始构建一个用于进行本地SEO审计的工具。该工具会在可视化的浏览器窗口中访问真实的网页,利用Claude提取相关的SEO数据,异步检查失效链接,在遇到特殊情况时会暂停执行并等待人工干预,最后生成结构清晰的报告——即使中途中断,也可以继续后续操作。
完成制作后,你将拥有一个可以用于处理任意URL列表的工具。每个URL的运行成本不到0.01美元。
你将构建什么
这是一个由七个模块组成的Python工具,它可以:
-
从CSV文件中读取URL列表
-
在真实的Chromium浏览器中访问每一个URL(而不是使用无头爬虫程序)
-
通过Claude API提取标题、元描述、H1标签以及规范链接标签
-
使用httpx异步检查失效链接
-
检测特殊情况(如404错误、需要登录才能访问的页面、重定向等),并在必要时暂停执行以等待人工输入
-
将处理结果逐步写入
report.json文件中——即使中途中断,也可以继续后续操作 -
在任务完成后生成一份用通俗语言编写的
report-summary.txt报告
完整的代码可以在GitHub上找到,地址为:dannwaneri/seo-agent。
先决条件
-
Python 3.11或更高版本
-
Anthropic API密钥(可在console.anthropic.com获取)
-
Windows、macOS或Linux操作系统
-
对Python及命令行有一定的了解
目录
为什么使用浏览器而不是爬虫
进行SEO审计的标准方法是使用requests获取页面的HTML内容,然后利用BeautifulSoup对其进行解析。这种方法适用于静态页面,但对于由JavaScript生成的页面来说则无法正常工作;它还会遗漏动态插入的元标签,而在需要身份验证的页面上更是完全无法使用。
“Browser Use”采用了不同的方法:它控制一个真实的Chromium浏览器,在JavaScript执行完毕后再读取DOM结构,并通过Playwright的工具树来呈现页面内容。这样,该工具就能像人类一样看到页面的实际显示效果。
实际应用中的区别在于:基于requests
另一个值得注意的区别是:“Browser Use”能够以语义化的方式解析页面内容。例如,当某个按钮的CSS类从btn-primarybutton-main
项目结构
seo-agent/
├── index.py # 主要审计流程
├── browser.py # 使用浏览器进行页面解析的模块
├── extractor.py # 用于与Claude API交互的数据提取层
├── linkchecker.py # 异步检测失效链接的工具
├── hitl.py | 实时暂停机制
├── reporter.py | 报告生成工具
├── state.py | 状态保存机制(可在中断后继续执行)
├── input.csv | 需要审计的URL列表
├── requirements.txt
├── .env.example
└── .gitignore
安装与配置
首先创建一个项目文件夹,然后安装所需的依赖项:
mkdir seo-agent && cd seo-agent
pip install browser-use anthropic playwright httpx
playwright install chromium
接下来创建input.csv文件,将需要审计的URL列表保存其中:
url
https://example.com
https://example.com/about
https://example.com/contact
然后创建.env.example文件,并设置ANTHROPIC_API_KEY环境变量:
ANTHROPIC_API_KEY=your-key-here
在运行程序之前,请确保已将API密钥设置为相应的环境变量:
# macOS/Linux
export ANTHROPIC_API KEY="sk-ant-..."
# Windows PowerShell
$env:ANTHROPIC_API_KEY = "sk-ant-..."
最后创建.gitignore文件,指定哪些文件不需要被版本控制工具跟踪:
state.json
report.json
report-summary.txt
.env
__pycache__/
*.pyc
模块1:状态管理
该工具需要记录已经审计过的URL列表。如果审计过程被中断——无论是由于停电、键盘按键被按下还是网络故障——它都应该从上次停止的地方继续执行,而不是重新开始。
state.py文件通过一个简单的JSON文件来实现这一功能:
import json
import os
STATE_FILE = os.path.join(os.path.dirname(__file__), "state.json")
_DEFAULT_STATE = {"audited": [], "pending": [], "needs_human": []}
def load_state() -> dict:
if not os.path.exists(STATE_FILE):
save_state(_DEFAULT_STATE.copy())
with open(STATE_FILE, encoding="utf-8") as f:
return json.load(f)
def save_state(state: dict) -> None:
with open(STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2)
def is_audited(url: str) -> bool:
return url in load_state()["audited"]
def mark_audited(url: str) -> None:
state = load_state()
if url not in state["audited"]:
state["audited"].append(url)
save_state(state)
def add_to_needs_human(url: str) -> None:
state = load_state()
if url not in state["needs_human":
state["needs_human"].append(url)
save_state(state)
这一设计是经过深思熟虑的:mark_audited()函数会在处理完某个URL并将其写入报告之后立即被调用。如果代理程序在运行过程中崩溃,那么它最多只会丢失对那个URL的处理结果。
模块2:浏览器集成
browser.py负责实际的页面导航操作。它直接使用Playwright来打开一个可视化的Chromium窗口,导航到指定的URL,捕获HTTP状态码及重定向信息,并从DOM中提取原始的SEO数据。
一些关键的设计决策包括:
使用可视化浏览器,而非无头浏览器。将headless=False设置为True,这样就可以观察到代理程序的实际运行过程。这对于演示和调试来说非常重要。
通过响应监听器来捕获状态信息。当收到4xx或5xx类型的响应时,Playwright会抛出异常,但on("response", ...)处理函数会在异常发生之前被执行,因此我们可以在那里获取到状态码信息。
每次访问之间会间隔2秒钟。这样就可以避免触发代理程序所在客户端的速率限制机制或机器人检测系统。
以下是核心的页面导航功能代码:
import asyncio
import sys
import time
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout
TIMEOUT = 20_000 # 20秒
def fetch_page(url: str) -> dict:
result = {
"final_url": url,
"status_code": None,
"title": None,
"meta_description": None,
"h1s": [],
"canonical": None,
"raw_links": [],
}
first_status = {"code": None}
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
def on_response(response):
if first_status["code"] is None:
first_status["code"] = response.status
page.on("response", on_response)
try:
page.goto(url, wait_until="domcontentloaded", timeout=TIMEOUT)
result["status_code"] = first_status["code"] or 200
result["final_url"] = page.url
# 从DOM中提取SEO数据
result["title"] = page.title() or None
result["meta_description"] = page.evaluate(
"() => { const m = document.querySelector('meta[name=\"description\"]'); "
"return m ? m.getAttribute('content') : null; }"
)
result["h1s"] = page.evaluate(
"() => Array.from(document.querySelectorAll('h1')).map(h => h.innerText.trim())"
)
result["canonical"] = page.evaluate(
"() => { const c = document.querySelector('link[rel=\"canonical\"]'); "
"return c ? c.getAttribute('href') : null; }"
)
result["raw_links"] = page.evaluate(
"() => Array.from(document.querySelectorAll('a[href]'))"
".map(a => a.href).filter(Boolean).slice(0, 100)"
)
except PlaywrightTimeout:
result["status_code"] = first_status["code"] or 408
except Exception as exc:
print(f"[browser] 错误:{exc}", file=sys.stderr)
result["status_code"] = first_status["code"]
finally:
browser.close()
time.sleep(2)
return result
有几点需要注意:
raw_links的限制为100个链接,这是有意为之。DEV.to的个人主页页面上包含数百个链接——在进行失效链接检测时,并不需要使用所有这些链接。
wait_until="domcontentloaded"这个设置比networkidle更快,而且对于提取元标签来说已经足够了。由JavaScript渲染的内容需要等到DOM结构准备好才能被处理,而不是等到所有的网络请求都完成。
模块3:Claude提取层
extractor.py会从browser.py中获取原始页面数据,然后调用Claude来生成结构化的SEO审计结果。
大多数教程在这个环节都会出错。它们要么在Python中编写复杂的解析逻辑(这种做法很不可靠),要么让Claude返回非结构化的文本,然后再尝试解析这些文本(同样不可靠)。正确的做法是:为Claude提供一个严格的JSON格式规范,并要求它只返回符合这个规范的结果。
正是这种精确的提示设计才使得这一过程能够可靠地运行:
import json
import os
import sys
from datetime import datetime, timezone
import anthropic
MODEL = "claude-sonnet-4-20250514"
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
def _strip_fences(text: str) -> str:
"""从Claude的回复中去除多余的markdown格式标签。"""
text = text.strip()
if text.startswith("```"):
lines = text.splitlines()
# 删除开头的标记
lines = lines[1:] if lines[0].startswith("```") else lines
# 删除结尾的标记
if lines and lines[-1].strip() == "```":
lines = lines[:-1]
text = "\n".join(lines).strip()
return text
def extract(snapshot: dict) -> dict:
if not os.environ.get("ANTHROPIC_API_KEY"):
raise OSError("ANTHROPIC_API_KEY未设置。")
prompt = f"""你是一名SEO审计员。请分析这个页面数据,并仅返回一个JSON对象。
不要包含任何散文性内容,也不要添加解释或markdown格式标签。只返回原始的JSON数据。
页面信息:
- URL: {snapshot.get('final_url')}
- 状态码: {snapshot.get('status_code')]
- 标题: {snapshot.get('title'}
- 元描述: {snapshot.get('meta_description'}
- H1标题: {snapshot.get('h1s'}
- 规范链接: {snapshot.get('canonical')}
返回以下格式的JSON对象:
{{
"url": "string",
"final_url": "string",
"status_code": number,
"title": {{"value": "string or null", "length": number, "status": "PASS or FAIL"},
"description": {{"value": "string or null", "length": number, "status": "PASS or FAIL"},
"h1": {{"count": number, "value": "string or null", "status": "PASS or FAIL"},
"canonical": {{"value": "string or null", "status": "PASS or FAIL"},
"flags": ["array of strings describing specific issues"],
"human_review": false,
"audited_at": "ISO timestamp"
}}
PASS/FAIL规则:
- 标题:如果为空或长度超过60个字符,则视为失败
- 描述:如果为空或长度超过160个字符,则视为失败
- H1标题:如果数量为0(缺失)或数量大于1(重复出现),则视为失败
- 规范链接:如果为空,则视为失败
- flags:列出所有出错的字段,并附上详细的说明
- audited_at:使用当前UTC时间的ISO 8601格式"
response = client.messages.create(
model=MODEL,
max_tokens=1000,
messages=[{"role": "user", "content": prompt}],
)
raw = response.content[0].text
clean = _strip_fences(raw)
try:
return json.loads(clean)
except json.JSONDecodeError as exc:
print(f"[extractor] JSON解析错误:{exc}", file=sys.stderr)
return _error_result(snapshot, str(exc))
def _error_result.snapshot: dict, reason: str) -> dict:
return {
"url": snapshot.get("final_url", ""),
"final_url": snapshot.get("final_url", ""),
"status_code": snapshot.get("status_code"),
"title": {"value": None, "length": 0, "status": "ERROR"},
"description": {"value": None, "length": 0, "status": "ERROR"},
"h1": {"count": 0, "value": None, "status": "ERROR"},
"canonical": {"value": None, "status": "ERROR"},
"flags": [f"提取错误:{reason}"],
"human_review": True,
"audited_at": datetime.now(timezone.utc).isoformat(),
}
有两点确保了这一功能在实际生产环境中的可靠性:
首先,`_strip_fences()`函数能够处理这样一种情况:尽管被明确要求不要这样做,但Claude仍然会将其响应用`json`格式的标签括起来。这种情况在Sonnet中偶尔会发生,如果不加以处理,就会导致`json.loads()`函数出现错误。
其次,`_error_result()`函数能够确保代理程序在接收到Claude返回的错误响应时不会崩溃——它会记录错误信息,并将相关URL标记出来供人工审核,然后继续处理下一个URL。
成本:Claude Sonnet 4的服务费用为:每百万输入字符3美元,每百万输出字符15美元。一般来说,一个页面的快照大约需要500个输入字符;而结构化的JSON响应则大约需要300个输出字符。因此,对于一个包含20个URL的审计任务来说,总成本大概为0.006美元/URL,也就是0.12美元。
模块4:失效链接检测器
linkchecker.py会从浏览器快照中获取`raw_links`列表,然后使用异步HEAD请求来检查这些链接是否有效。
在设计上我们做了以下几项选择:
-
仅检测同一域名的链接。
如果检查页面上的所有外部链接,将会花费很长时间,而这也不是代理客户所需要的。因此,我们只检查与被审计页面属于同一域名的链接。
-
使用HEAD请求而非GET请求。
HEAD请求速度更快,占用的带宽也更少,完全足以用来检测链接的状态码。
-
每次最多检测50个链接。
像DEV.to这样的网站,其文章列表中通常会包含数百个内部链接。如果全部检查这些链接,将会严重影响程序的运行效率。
-
通过asyncio同时处理多个请求。
所有链接都会被并行检测,而不是依次处理。
import asyncio
import logging
from urllib.parse import urlparse
import httpx
CAP = 50
TIMEOUT = 5.0
logger = logging.getLogger(__name__)
def _same_domain(link: str, final_url: str) -> bool:
if not link:
return False
lower = link.strip().lower()
if lower.startswith("#
or "mailto:"
or "javascript:"
or "tel:"
or "data:"):
return False
try:
page_host = urlparse(final_url).netloc.lower()
parsed = urlparse(link)
return parsedscheme in ("http", "https") and parsed.netloc.lower() == page_host
except Exception:
return False
async def _check_link(client: httpx.AsyncClient, url: str) -> tuple[str, bool]:
try:
resp = await client.head(url, follow_redirects=True, timeout=TIMEOUT)
return url, resp.status_code != 200
except Exception:
return url, True # 表示请求超时或连接失败,因此链接无效
async def _run_checks(links: list[str]) -> list[str]:
async with httpx.AsyncClient() as client:
results = await asyncio.gather(*[_check_link(client, url) for url in links])
return [url for url, broken in results if broken]
def check_links(raw_links: list[str], final_url: str) -> dict:
same_domain = [l for l in raw_links if _same_domain(l, final_url)]
capped = len(same_domain) > CAP
if capped:
logger.warning("页面中包含%d个同一域名的链接,但最多只能检测%d个。",
len(same_domain), CAP)
same_domain = same_domain[:CAP]
broken = asyncio.run(_run_checks(same_domain))
return {
"broken": broken,
"count": len(broken),
"status": "FAIL" if broken else "PASS",
"capped": capped,
}
模块5:人工干预机制
大多数自动化教程都会跳过这一部分。当程序遇到登录障碍时会发生什么?当页面返回403错误代码时怎么办?或者当链接跳转到“订阅以继续阅读”页面时又该如何处理?
大多数脚本在这种情况下要么会崩溃,要么会默默地忽略这些异常。但在实际应用中,这两种情况都是不可接受的。
hitl.py通过两个函数来解决这些问题:一个函数用于判断是否需要暂停操作,另一个函数则负责执行暂停操作本身。
from state import add_to_needs_human LOGIN_KEYWORDS = {"login", "sign in", "sign-in", "access denied", "log in", "unauthorized"} REDIRECT_CODES = {301, 302, 307, 308} def should_pause(snapshot: dict) -> bool: code = snapshot.get("status_code") # 如果导航完全失败 if code is None: return True # 如果状态码不是200,且不属于重定向代码 if code != 200 and code not in REDIRECT_CODES: return True # 检查标题或H1标签中是否包含登录相关关键词 title = (snapshot.get("title") or "").lower() h1s = [h.lower() for h in (snapshot.get("h1s") or [])] if any(kw in title for kw in LOGIN_KEYWORDS): return True if any(kw in h1 for kw in LOGIN_keywords for h1 in h1s): return True return False def pause_reason(snapshot: dict) -> str: code = snapshot.get("status_code") if code is None: return "导航失败(状态码未知)" if code != 200 and code not in REDIRECT_CODES: return f"出现异常状态码:{code}" return "可能遇到了登录障碍"
should_pause()函数可以检测四种情况:导航失败、出现非预期的HTTP状态码、标题中包含登录相关关键词,以及H1标签中包含这些关键词。其中,对标题的检查能够有效识别那些虽然返回200状态码但实际上无法访问的页面。在
--auto模式下(用于定时执行任务),主循环会直接跳过pause_and_prompt()函数的调用,而是将相关信息记录到needs_human[]数组中,然后继续执行后续操作。模块6:报告生成工具
reporter.py会逐个记录处理结果。这一点非常重要:因为结果是在每次审核完一个URL后立即被记录下来的,而不是在所有操作完成后才一次性生成。这样一来,如果任务中途被中断,之前完成的工作也不会丢失。import json import os from datetime import datetime, timezone REPORT_JSON = os.path.join(os.path.dirname(__file__), "report.json") REPORTTXT = os.path.join(os.path.dirname(__file__), "report-summary.txt") def _load_report() -> list: if not os.path.exists(REPORT_json): return [] with open(REPORT_JSON, encoding="utf-8") as f: return json.load(f) def write_result(result: dict) -> None: """在 report.json 文件中添加或更新相关数据。""" entries = _load_report() url = result.get("url", "") # 如果该 URL 已经存在于文件中,则更新相应的记录(支持重试机制) for i, entry in enumerate(entries): if entry.get("url") == url: entries[i] = result break else: entries.append(result) with open(REPORT_JSON, "w", encoding="utf-8") as f: json.dumpentries, f, indent=2, ensure_ascii=False) def _is_overall_pass(result: dict) -> bool: fields = ["title", "description", "h1", "canonical"] for field in fields: if result.get(field, {}).get("status") not in ("PASS",): return False if result.get("broken_links", "").get("status") == "FAIL": return False return True def write_summary() -> None: entries = _load_report() passed = sum(1 for e in entries if _is_overall_pass(e)) lines = [] for entry in entries: overall = "PASS" if _is_overall_pass(entry) else "FAIL" failed_fields = [ f for f in ["title", "description", "h1", "canonical", "broken_links"] if entry.get(f, "").get("status") == "FAIL" ] suffix = f" [{', '.join(failed_fields)}]" if failed_fields else "" lines.append(f"{entry.get('url', 'unknown'):<60} | {overall}{suffix}") lines.append("") lines.append(f"{passed}/{len(entries)} 个 URL 通过了检测") with open(REPORTTXT, "w", encoding="utf-8") as f: f.write("\n".join(lines))
write_result()中的去重机制能够妥善处理重试操作。如果在人工审核并完成身份验证后再次尝试访问某个URL,系统会用新的审核结果替换原有的记录,而不会创建重复的条目。
模块7:主循环
index.py负责将所有组件连接在一起。它读取URL列表,加载状态信息,跳过已经完成审核的URL,然后启动审核流程。
import csv import os import sys import time import argparse from state import load_state, is_audited, mark_audited, add_to_needs_human from browser import fetch_page from extractor import extract from linkchecker import check_links from hitl import should_pause, pause_reason, pause_and_prompt from reporter import write_result, write_summary INPUT_csv = os.path.join(os.path.dirname(__file__), "input.csv") def read_urls(path: str) -> list[str]: with open(path, newline="", encoding="utf-8") as f: return .strip() for row in csv.DictReader(f) if row.get("url", "").strip()]def run(auto: bool = False): if not os.environ.get("ANTHROPIC_API_KEY"): print("错误:未设置ANTHROPIC_API KEY环境变量。") sys.exit(1) urls = read_urls(INPUT_csv) pending = [u for u in urls if not is_audited(u)] print(f"开始审核:还有{len(pending)}个URL待审核,已有{len(urls) - len(pending)}个URL完成审核。\n") total = lenurls) try: for i, url in enumerate(pending, start=1): position = urls.index(url) + 1 print(f"[{position}/{total}] {url}", end=" -> ", flush=True) # 打开浏览器访问该URL snapshot = fetch_page(url) # 进行人工审核 if should_pause(snapshot): reason = pause_reason(snapshot) if auto: print(f"自动跳过({reason})") add_to_needs_human(url) mark_audited(url) continue action = pause_and_prompt(url, reason) if action == "quit": print("退出审核流程。") break elif action == "skip": add_to_needs_human(url) mark_audited(url) continue # 如果选择“retry”,则重新尝试访问该URL snapshot = fetch_page(url) # 使用Claude工具提取信息 result = extract(snapshot) # 检查是否存在失效的链接 links = check_links(snapshot.get("raw_links", []), snapshot.get("final_url", url)) result["broken_links"] = links # 立即写入审核结果 write_result(result) mark_audited(url) overall = "PASS" if all( result.get(f, "").get("status") == "PASS" for f in ["title", "description", "h1", "canonical"] ) and links["status"] == "PASS" else "FAIL" print(overall) except KeyboardInterrupt: print("\n审核过程被中断。已保存进度,重新运行即可继续。") return write_summary() passed = sum( 1 for e in [r for r in []] if all(e.get(f, "").get("status") == "PASS" for f in ["title", "description", "h1", "canonical"]) ) print(f"\n审核完成。报告已保存至report.json和report-summary.txt文件中。") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--auto", action="store_true", help="自动跳过需要人工审核的URL") args = parser.parse_args() run(auto=args.auto)
KeyboardInterrupt处理程序就是用于恢复程序运行的机制。当你按下Ctrl+C时,该处理程序会输出一条消息并干净地退出程序。由于对于每个URL来说,在write_result()方法执行之后都会调用mark_audited()方法,因此下次运行程序时会跳过已经处理过的所有内容。
运行代理程序
交互模式(在遇到特殊情况时会暂停程序运行):
python index.py
自动模式(会跳过特殊情况,并将未处理的URL添加到needs_human[]列表中):
python index.py --auto
当程序运行时,你会看到每个URL对应的浏览器窗口会被打开,同时终端也会显示进度信息:
开始审计:还有7个URL未处理,0个已经完成。
[1/7] https://example.com -> 通过
[2/7] https://example.com/about -> 失败
[3/7] https://example.com/contact -> 被自动跳过(返回的状态码为404)
…
审计已完成。报告文件已保存为report.json和report-summary.txt
如果程序运行过程中被中断,可以重新运行它:
python index.py --auto
# 开始审计:还有4个URL未处理,3个已经完成。
为机构使用安排调度任务
对于需要每周定期执行的审计任务,可以创建一个批处理文件,然后通过Windows任务计划程序来安排执行。
创建run-audit.bat文件:
@echo off
set ANTHROPIC_API_KEY=你的API密钥
cd /d C:\Users\你的用户名\Desktop\seo-agent
python index.py --auto
在Windows任务计划程序中,按照以下步骤操作:
-
创建一个新的基本任务。
-
将触发条件设置为“每周一上午7点”。
-
将动作设置为“启动一个程序”。
-
选择
run-audit.bat文件作为要执行的程序。
周一早上查看report-summary.txt文件。其中那些被标记为needs_human[]的URL需要人工审核——比如那些需要输入用户名或密码才能访问的页面,或者返回了异常状态码的页面。
对于macOS/Linux系统,可以使用cron来安排任务:
# 每周一上午7点运行
0 7 * * 1 cd /path/to/seo-agent && ANTHROPIC_API_KEY=你的API密钥 python index.py --auto
审计结果是什么样的
我使用这个代理程序对我的在Hashnode、freeCodeCamp和DEV.to上发布的7个页面进行了检测,结果所有这些页面都失败了。
https://hashnode.com/@dannwaneri | 失败 [标题]
https://freecodecamp.org/news/claude-code-skill | 失败 [描述]
https://freecodecamp.org/news/stop-letting-ai-guess | 失败 [描述]
https://freecodecamp.org/news/rag-system-handbook | 失败 [标题, 描述]
https://freecodecamp.org/news/author/dannwaneri | 失败 [描述]
https://dev.to/dannwaneri/gatekeeping-panic | 失败 [标题]
https://dev.to/dannwaneri/production-rag-system | 失败 [标题]
0/7个URL通过审核
freeCodeCamp在描述文章时存在的问题,部分是由于平台本身的限制所致——freeCodeCamp使用的模板有时会截断或省略文章列表页面的元描述信息。而DEV.to在标题处理方面存在的问题则出在我自己身上,那些被用作标题的文章,其标签中的字符长度往往超过了60个。
需要说明的是,这个60个字符的长度限制其实只是一个显示上的限制,并不会影响文章的排名。谷歌会收录所有长度的标题。设定这一限制主要是为了确保在桌面端搜索结果中,标题能够完整显示而不会被截断。如果标题超过60个字符,虽然仍然会被展示出来,但可能会被截断,从而影响用户的点击率。系统标记这些标题只是为了提醒开发者注意显示上的问题,并不意味着这些标题违反了排名规则。
后续步骤
目前这个工具已经能够完成基本的SEO审计工作流程。不过还可以进行一些扩展功能,例如:
-
性能指标检测 — 可以为每个URL添加Lighthouse或PageSpeed Insights的API调用。
-
结构化数据验证 — 检查页面中是否使用了JSON-LD格式进行数据标记,并对其进行验证。
-
邮件发送功能 — 审计完成后,可以通过SMTP发送
report-summary.txt文件。 -
多客户支持 — 可以为不同的客户分别准备
input.csv文件,并生成不同的报告目录。
包含所有七个模块的完整代码可以在dannwaneri/seo-agent这个链接找到。你可以克隆这个代码,添加你自己的URL地址,然后运行它。
如果你觉得这篇文章有用,我还会在DEV.to/@dannwaneri这个平台上撰写更多关于如何为开发人员和机构配置AI工具的文章。DEV.to上还有关于这个工具设计理念的详细介绍,包括为什么使用HITL算法、为什么优先选择浏览器数据而不是爬虫数据,以及审计结果对你自己发布的文章意味着什么。