greeta commited on
Commit
447b885
·
verified ·
1 Parent(s): 87112c1

Upload scraper.py

Browse files
Files changed (1) hide show
  1. scraper.py +218 -0
scraper.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Скрапер для сайта ФИПИ (fipi.ru)
3
+ Извлекает задания по русскому языку для ЕГЭ (задание 27)
4
+ """
5
+
6
+ import httpx
7
+ from bs4 import BeautifulSoup
8
+ from typing import List, Dict, Optional
9
+ from datetime import datetime
10
+ import re
11
+ import logging
12
+
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class FIPIScraper:
18
+ """Парсер для сайта ФИПИ"""
19
+
20
+ def __init__(self, base_url: str = "https://fipi.ru"):
21
+ self.base_url = base_url
22
+ self.headers = {
23
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
24
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
25
+ "Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7",
26
+ }
27
+
28
+ async def fetch_page(self, url: str) -> Optional[str]:
29
+ """Получение HTML страницы"""
30
+ # Создаем клиент с отключенной проверкой SSL (для fipi.ru поддоменов)
31
+ import ssl
32
+ ssl_context = ssl.create_default_context()
33
+ ssl_context.check_hostname = False
34
+ ssl_context.verify_mode = ssl.CERT_NONE
35
+
36
+ async with httpx.AsyncClient(
37
+ headers=self.headers,
38
+ timeout=30.0,
39
+ verify=ssl_context
40
+ ) as client:
41
+ try:
42
+ response = await client.get(url)
43
+ response.raise_for_status()
44
+ return response.text
45
+ except httpx.HTTPError as e:
46
+ logger.error(f"Ошибка при получении {url}: {e}")
47
+ return None
48
+
49
+ def parse_task_page(self, html: str, url: str) -> Optional[Dict]:
50
+ """Парсинг страницы с заданием"""
51
+ soup = BeautifulSoup(html, 'lxml')
52
+
53
+ # Извлечение заголовка - приоритет h1 в .content
54
+ title_tag = soup.select_one('.content h1') or soup.find('h1')
55
+ title = title_tag.get_text(strip=True) if title_tag else "Без названия"
56
+
57
+ # Если заголовок пустой, пробуем извлечь из title документа
58
+ if not title or title == "Без названия":
59
+ title_doc = soup.find('title')
60
+ if title_doc:
61
+ title = title_doc.get_text(strip=True)
62
+
63
+ # Извлечение основного контента - приоритет .content
64
+ content_div = soup.select_one('.content') or soup.find('div', class_='field--name-body')
65
+ if not content_div:
66
+ content_div = soup.find('main') or soup.find('body')
67
+
68
+ # Очистка текста - удаляем скрипты и стили
69
+ for element in content_div.find_all(['script', 'style', 'nav', 'header', 'footer']):
70
+ element.decompose()
71
+
72
+ content = content_div.get_text(separator='\n', strip=True) if content_div else ""
73
+
74
+ # Извлечение изображения (если есть)
75
+ images = []
76
+ for img in content_div.find_all('img'):
77
+ src = img.get('src') or img.get('data-src')
78
+ if src:
79
+ if not src.startswith('http'):
80
+ src = self.base_url + src
81
+ images.append(src)
82
+
83
+ # Извлечение ссылок на задания
84
+ task_links = []
85
+ for link in content_div.find_all('a', href=True):
86
+ href = link['href']
87
+ link_text = link.get_text(strip=True)
88
+ if any(pattern in href for pattern in ['/ege/', '/oge/', '/task/', '/demo/', '/bank/']):
89
+ if not href.startswith('http'):
90
+ href = self.base_url + href
91
+ task_links.append({"text": link_text, "url": href})
92
+
93
+ # Определение типа задания
94
+ task_type = self._detect_task_type(title, content)
95
+
96
+ # Извлечение вариантов (если есть)
97
+ variants = self._extract_variants(content)
98
+
99
+ return {
100
+ "title": title,
101
+ "content": content,
102
+ "source_url": url,
103
+ "task_type": task_type,
104
+ "images": images,
105
+ "variants": variants,
106
+ "task_links": task_links,
107
+ "scraped_at": datetime.utcnow().isoformat(),
108
+ }
109
+
110
+ def _detect_task_type(self, title: str, content: str) -> str:
111
+ """Определение типа задания"""
112
+ text = (title + " " + content).lower()
113
+
114
+ if any(word in text for word in ["сочинение", "эссе", "напишит"]):
115
+ return "writing"
116
+ elif any(word in text for word in ["тест", "выбер", "вариант"]):
117
+ return "test"
118
+ elif any(word in text for word in ["ауди", "слуш"]):
119
+ return "listening"
120
+ elif any(word in text for word in ["чит", "текст"]):
121
+ return "reading"
122
+ else:
123
+ return "other"
124
+
125
+ def _extract_variants(self, content: str) -> List[str]:
126
+ """Извлечение вариантов ответов"""
127
+ variants = []
128
+
129
+ # Паттерн для вариантов типа "1) ... 2) ..."
130
+ pattern = r'(\d+)[\.\)]\s*([^\n\d]+)'
131
+ matches = re.findall(pattern, content)
132
+
133
+ for _, variant in matches:
134
+ variants.append(variant.strip())
135
+
136
+ return variants[:10] # Ограничение на 10 вариантов
137
+
138
+ async def scrape_tasks(self, subject: str = "russian") -> List[Dict]:
139
+ """
140
+ Скрапинг заданий по предмету
141
+
142
+ Args:
143
+ subject: Код предмета (по умолчанию russian)
144
+
145
+ Returns:
146
+ Список заданий
147
+ """
148
+ tasks = []
149
+
150
+ # Актуальные URLs для скрапинга (fipi.ru) - только работающие
151
+ urls_to_scrape = [
152
+ f"{self.base_url}/ege/otkrytyy-bank-zadaniy-ege",
153
+ f"{self.base_url}/oge/otkrytyy-bank-zadaniy-oge",
154
+ ]
155
+
156
+ for url in urls_to_scrape:
157
+ logger.info(f"Скрапинг {url}")
158
+ html = await self.fetch_page(url)
159
+
160
+ if html:
161
+ task = self.parse_task_page(html, url)
162
+ if task:
163
+ tasks.append(task)
164
+
165
+ # Если есть ссылки на задания, скачиваем их
166
+ for link_info in task.get('task_links', [])[:5]: # Ограничиваем количество
167
+ link_url = link_info.get('url')
168
+ if link_url:
169
+ logger.info(f" -> Скачиваем задание: {link_url}")
170
+ link_html = await self.fetch_page(link_url)
171
+ if link_html:
172
+ subtask = self.parse_task_page(link_html, link_url)
173
+ if subtask:
174
+ tasks.append(subtask)
175
+
176
+ logger.info(f"Найдено {len(tasks)} заданий")
177
+ return tasks
178
+
179
+ async def scrape_task_by_id(self, task_id: str) -> Optional[Dict]:
180
+ """Скрапинг конкретного задания по ID"""
181
+ url = f"{self.base_url}/task/{task_id}"
182
+ logger.info(f"Скрапинг задания {task_id}")
183
+
184
+ html = await self.fetch_page(url)
185
+ if html:
186
+ return self.parse_task_page(html, url)
187
+
188
+ return None
189
+
190
+ async def search_tasks(self, query: str) -> List[Dict]:
191
+ """Поиск заданий по ключевому слову"""
192
+ tasks = []
193
+ # Используем правильный URL для поиска на fipi.ru
194
+ search_url = f"{self.base_url}/search?q={query}"
195
+
196
+ html = await self.fetch_page(search_url)
197
+ if not html:
198
+ # Пробуем альтернативный поиск через банк заданий
199
+ logger.info("Поиск не доступен, пробуем парсинг банка заданий")
200
+ return await self.scrape_tasks()
201
+
202
+ soup = BeautifulSoup(html, 'lxml')
203
+
204
+ # Поиск ссылок на задания с правильными паттернами
205
+ for link in soup.find_all('a', href=True):
206
+ href = link['href']
207
+ # Проверяем на наличие валидных URL заданий
208
+ if any(pattern in href for pattern in ['/ege/', '/oge/', '/task/', '/demo/', '/bank/']):
209
+ if not href.startswith('http'):
210
+ href = self.base_url + href
211
+
212
+ task_html = await self.fetch_page(href)
213
+ if task_html:
214
+ task = self.parse_task_page(task_html, href)
215
+ if task:
216
+ tasks.append(task)
217
+
218
+ return tasks