You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript开发带限流与分页的Semrush和Ahrefs API客户端需求

解决方案:Semrush/Ahrefs API类型化客户端实现(限流+自动分页)

核心思路

  • 封装通用限流工具统一处理API请求频率限制
  • 抽象自动分页逻辑避免重复代码
  • 为每个API实现类型化客户端,处理各自的认证和数据解析
  • 内置错误处理,捕获429限流、请求失败等异常

1. 通用限流工具

实现一个RateLimiter类,控制单位时间内的请求次数,避免触发429错误:

class RateLimiter {
  private requestQueue: (() => Promise<void>)[] = [];
  private activeRequests = 0;
  private readonly maxRequestsPerWindow: number;
  private readonly windowMs: number;
  private windowStart = Date.now();
  private requestsInWindow = 0;

  constructor(maxRequestsPerWindow: number, windowMs: number) {
    this.maxRequestsPerWindow = maxRequestsPerWindow;
    this.windowMs = windowMs;
  }

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      const task = async () => {
        try {
          // 等待窗口重置或获取可用额度
          while (true) {
            const now = Date.now();
            if (now - this.windowStart >= this.windowMs) {
              this.windowStart = now;
              this.requestsInWindow = 0;
            }
            if (this.requestsInWindow < this.maxRequestsPerWindow) {
              this.requestsInWindow++;
              break;
            }
            await new Promise(resolve => setTimeout(resolve, 100));
          }

          const result = await fn();
          resolve(result);
        } catch (err) {
          reject(err);
        } finally {
          this.activeRequests--;
          this.processQueue();
        }
      };

      this.requestQueue.push(task);
      this.processQueue();
    });
  }

  private processQueue() {
    while (this.requestQueue.length > 0 && this.activeRequests < this.maxRequestsPerWindow) {
      const task = this.requestQueue.shift();
      if (task) {
        this.activeRequests++;
        task();
      }
    }
  }
}

2. 通用自动分页逻辑

封装分页处理函数,自动跟踪偏移量,直到获取完整数据:

async function handlePagination<T>(
  fetchPage: (offset: number) => Promise<T[]>,
  pageSize: number,
  maxTotal?: number
): Promise<T[]> {
  let offset = 0;
  const allResults: T[] = [];

  while (true) {
    const pageResults = await fetchPage(offset);
    allResults.push(...pageResults);

    // 终止条件:返回结果不足一页,或达到最大限制
    if (pageResults.length < pageSize || (maxTotal && allResults.length >= maxTotal)) {
      break;
    }

    offset += pageSize;
  }

  return allResults;
}

3. Semrush类型化客户端

处理查询参数认证、CSV解析、类型映射,集成限流和自动分页:

// Semrush关键词数据类型定义
export interface SemrushKeywordData {
  keyword: string;
  volume: number;
  difficulty: number;
  cpc: number;
  // 根据API返回字段扩展
}

class SemrushClient {
  private readonly apiKey: string;
  private readonly rateLimiter: RateLimiter;
  private readonly baseUrl = 'https://api.semrush.com';

  constructor(apiKey: string, maxRequestsPerMinute: number = 10) {
    this.apiKey = apiKey;
    this.rateLimiter = new RateLimiter(maxRequestsPerMinute, 60000);
  }

  // 解析Semrush CSV响应为类型化数据
  private parseCsv<T>(csv: string): T[] {
    const lines = csv.split('\n').filter(line => line.trim() !== '');
    if (lines.length === 0) return [];
    
    const headers = lines[0].split(';').map(h => h.toLowerCase());
    return lines.slice(1).map(line => {
      const values = line.split(';');
      return headers.reduce((obj, header, index) => {
        // 根据字段类型做转换
        switch (header) {
          case 'volume':
          case 'difficulty':
            obj[header] = parseInt(values[index]) || 0;
            break;
          case 'cpc':
            obj[header] = parseFloat(values[index]) || 0;
            break;
          default:
            obj[header] = values[index] || '';
        }
        return obj;
      }, {} as T);
    });
  }

  // 获取域名有机关键词数据(自动分页)
  async getKeywordData(
    domain: string,
    database: string = 'us',
    pageSize: number = 100
  ): Promise<SemrushKeywordData[]> {
    const fetchPage = async (offset: number) => {
      const params = new URLSearchParams({
        key: this.apiKey,
        type: 'domain_organic',
        domain,
        database,
        export_columns: 'Ph,Po,Nq,Cp', // 对应关键词、难度、搜索量、CPC
        limit: pageSize.toString(),
        offset: offset.toString(),
        export_format: 'csv'
      });

      const response = await this.rateLimiter.execute(() => fetch(`${this.baseUrl}?${params}`));

      if (!response.ok) {
        if (response.status === 429) {
          throw new Error('Semrush API限流触发,请降低请求频率');
        }
        throw new Error(`Semrush API请求失败: ${response.status} ${response.statusText}`);
      }

      const csv = await response.text();
      return this.parseCsv<SemrushKeywordData>(csv);
    };

    return handlePagination(fetchPage, pageSize);
  }
}

4. Ahrefs类型化客户端

处理Bearer Token认证、JSON解析,同样集成限流和自动分页:

// Ahrefs反向链接数据类型定义
export interface AhrefsBacklinkData {
  url: string;
  domain: string;
  anchor: string;
  ahrefs_rank: number;
  // 根据API返回字段扩展
}

class AhrefsClient {
  private readonly apiToken: string;
  private readonly rateLimiter: RateLimiter;
  private readonly baseUrl = 'https://apiv2.ahrefs.com';

  constructor(apiToken: string, maxRequestsPerMinute: number = 10) {
    this.apiToken = apiToken;
    this.rateLimiter = new RateLimiter(maxRequestsPerMinute, 60000);
  }

  // 获取域名反向链接数据(自动分页)
  async getBacklinks(
    domain: string,
    pageSize: number = 100
  ): Promise<AhrefsBacklinkData[]> {
    const fetchPage = async (offset: number) => {
      const params = new URLSearchParams({
        token: this.apiToken,
        from: 'backlinks',
        target: domain,
        limit: pageSize.toString(),
        offset: offset.toString(),
        output: 'json'
      });

      const response = await this.rateLimiter.execute(() => fetch(`${this.baseUrl}?${params}`, {
        headers: {
          'Authorization': `Bearer ${this.apiToken}`
        }
      }));

      if (!response.ok) {
        if (response.status === 429) {
          throw new Error('Ahrefs API限流触发,请降低请求频率');
        }
        throw new Error(`Ahrefs API请求失败: ${response.status} ${response.statusText}`);
      }

      const data = await response.json();
      return data.backlinks as AhrefsBacklinkData[];
    };

    return handlePagination(fetchPage, pageSize);
  }
}

5. 错误处理与使用示例

在业务代码中统一捕获异常,支持限流重试等扩展:

async function fetchSeoData() {
  const semrushClient = new SemrushClient('your-semrush-api-key');
  const ahrefsClient = new AhrefsClient('your-ahrefs-api-token');

  try {
    const keywords = await semrushClient.getKeywordData('example.com');
    console.log('获取到Semrush关键词数量:', keywords.length);

    const backlinks = await ahrefsClient.getBacklinks('example.com');
    console.log('获取到Ahrefs反向链接数量:', backlinks.length);
  } catch (err) {
    if (err instanceof Error) {
      console.error('SEO数据拉取失败:', err.message);
      // 针对限流错误实现重试逻辑
      if (err.message.includes('限流')) {
        console.log('5秒后重试...');
        setTimeout(fetchSeoData, 5000);
      }
    }
  }
}

fetchSeoData();

内容的提问来源于stack exchange,提问作者Al Amin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 17:24:52