import asyncio import aiohttp from typing import List, Dict, Optional from dataclasses import dataclass from datetime import datetime @dataclass class ArticleData: """文章数据结构""" title: str content: str url: str publishtime: Optional[str] = None
class AsyncCrawler: """异步爬虫类 - 高效采集工具"""
def init(self, maxconcurrent: int = 5, timeout: int = 30, retrytimes: int = 3): self.maxconcurrent = maxconcurrent self.timeout = aiohttp.ClientTimeout(total=timeout) self.retrytimes = retrytimes self.results: List[ArticleData] = [] self.failedurls: List[str] = []
async def fetchsingle(self, session: aiohttp.ClientSession, url: str) -> Optional[Dict]: """抓取单个页面""" for attempt in range(self.retrytimes): try: async with session.get(url, timeout=self.timeout) as resp: if resp.status == 200: html = await resp.text() return {{"url": url, "html": html}} elif resp.status == 429: wait = 2 ** attempt print(f"限流中,等待{{wait}}秒...") await asyncio.sleep(wait) else: print(f"HTTP {{resp.status}}: {{url}}") break except Exception as e: print(f"尝试 {{attempt + 1}} 失败: {{e}}")
self.failedurls.append(url) return None
async def parsearticle(self, rawdata: Dict) -> Optional[ArticleData]: """解析文章内容(需根据目标网站定制)""" from bs4 import BeautifulSoup html = rawdata["html"] soup = BeautifulSoup(html, "html.parser")
titletag = soup.find("h1") or soup.find("title") title = titletag.gettext(strip=True) if titletag else "未知标题"
contenttag = soup.find("div", class="content") or soup.find("article") content = contenttag.gettext(strip=True) if contenttag else ""
return ArticleData( title=title, content=content[:500], url=rawdata["url"] )
async def crawl(self, urls: List[str]) -> List[ArticleData]: """并发抓取多个URL""" semaphore = asyncio.Semaphore(self.maxconcurrent)
async def limitedfetch(session, url): async with semaphore: return await self.fetchsingle(session, url)
connector = aiohttp.TCPConnector(limit=self.maxconcurrent) async with aiohttp.ClientSession(connector=connector) as session: tasks = [limitedfetch(session, url) for url in urls] rawresults = await asyncio.gather(*tasks)
for raw in rawresults: if raw: article = await self.parsearticle(raw) if article: self.results.append(article)
return self.results
def exporttojson(self, filepath: str): """导出结果为JSON""" import json data = [ {{"title": a.title, "url": a.url, "time": a.publishtime}} for a in self.results ] with open(filepath, "w", encoding="utf-8") as f: json.dump(data, f, ensureascii=False, indent=2)
async def main(): """使用示例""" crawler = AsyncCrawler(maxconcurrent=3) urls = ["https://example.com/page/{{i}}" for i in range(1, 6)]
articles = await crawler.crawl(urls)
print(f"成功采集: {{len(articles)}} 篇") print(f"失败 URL: {{len(crawler.failedurls)}} 个")
crawler.exporttojson("articles_{{datetime.now():%Y%m%d}}.json")
if name == "main": asyncio.run(main())