netseecxld commited on
Commit
096e682
Β·
verified Β·
1 Parent(s): a5adbb4

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +265 -0
app.py ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import asyncio
3
+ import aiohttp
4
+ import json
5
+ import os
6
+ import time
7
+ import threading
8
+ from bs4 import BeautifulSoup
9
+ from fastapi import FastAPI
10
+ from huggingface_hub import HfApi, hf_hub_download
11
+ import uvicorn
12
+
13
+ # ─── ΠΠΠ‘Π’Π ΠžΠ™ΠšΠ˜ ────────────────────────────────────────────────
14
+ DB_PATH = '/app/linkm_posts.db'
15
+ PROGRESS_FILE = '/app/linkm_progress.json'
16
+ BASE_URL = 'https://linkm.me/posts/{}'
17
+ START_ID = 213008536
18
+ END_ID = 0
19
+ WORKERS = 150
20
+ BATCH_SIZE = 500
21
+ REQUEST_TIMEOUT = 10
22
+ HF_TOKEN = os.environ.get('HF_TOKEN')
23
+ HF_DATASET = os.environ.get('HF_DATASET')
24
+ PUSH_EVERY = 10000 # ΠΏΡƒΡˆΠΈΡ‚ΡŒ ΠΊΠ°ΠΆΠ΄Ρ‹Π΅ 10k Π½Π°ΠΉΠ΄Π΅Π½Π½Ρ‹Ρ…
25
+
26
+ HEADERS = {
27
+ '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',
28
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
29
+ 'Accept-Language': 'en-US,en;q=0.5',
30
+ }
31
+
32
+ # ─── Π‘ΠžΠ‘Π’ΠžΠ―ΠΠ˜Π• ────────────────────────────────────────────────
33
+ state = {
34
+ 'status': 'starting',
35
+ 'current_id': START_ID,
36
+ 'found': 0,
37
+ 'processed': 0,
38
+ 'speed': 0,
39
+ 'last_push': 0,
40
+ 'started_at': time.time(),
41
+ }
42
+
43
+ # ─── PULL Π‘ HF ПРИ БВАРВЕ ─────────────────────────────────────
44
+ def pull_from_hf():
45
+ if not HF_TOKEN or not HF_DATASET:
46
+ return
47
+ try:
48
+ hf_hub_download(
49
+ repo_id=HF_DATASET,
50
+ filename='linkm_posts.db',
51
+ repo_type='dataset',
52
+ local_dir='/app',
53
+ token=HF_TOKEN,
54
+ )
55
+ print('βœ… Π‘Π°Π·Π° скачана с HF')
56
+ except Exception as e:
57
+ print(f'⚠️ НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΡΠΊΠ°Ρ‡Π°Ρ‚ΡŒ Π±Π°Π·Ρƒ: {e}')
58
+
59
+ try:
60
+ hf_hub_download(
61
+ repo_id=HF_DATASET,
62
+ filename='linkm_progress.json',
63
+ repo_type='dataset',
64
+ local_dir='/app',
65
+ token=HF_TOKEN,
66
+ )
67
+ print('βœ… ΠŸΡ€ΠΎΠ³Ρ€Π΅ΡΡ скачан с HF')
68
+ except Exception as e:
69
+ print(f'⚠️ ΠŸΡ€ΠΎΠ³Ρ€Π΅ΡΡ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ Π½Π° HF: {e}')
70
+
71
+ # ─── БАЗА ДАННЫΠ₯ ──────────────────────────────────────────────
72
+ def init_db():
73
+ conn = sqlite3.connect(DB_PATH)
74
+ c = conn.cursor()
75
+ c.execute('''
76
+ CREATE TABLE IF NOT EXISTS posts (
77
+ post_id INTEGER PRIMARY KEY,
78
+ text TEXT NOT NULL,
79
+ parsed_at TEXT DEFAULT (datetime('now'))
80
+ )
81
+ ''')
82
+ conn.commit()
83
+ conn.close()
84
+
85
+ # ─── ΠŸΠ ΠžΠ“Π Π•Π‘Π‘ ─────────────────────────────────────────────────
86
+ def load_progress():
87
+ if os.path.exists(PROGRESS_FILE):
88
+ try:
89
+ with open(PROGRESS_FILE, 'r') as f:
90
+ data = json.load(f)
91
+ print(f'▢️ ΠŸΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠ°Π΅ΠΌ с ID: {data["next_id"]} (Π½Π°ΠΉΠ΄Π΅Π½ΠΎ: {data["found"]:,})')
92
+ return data
93
+ except Exception:
94
+ pass
95
+ return {'next_id': START_ID, 'processed': 0, 'found': 0}
96
+
97
+ def save_progress(data):
98
+ with open(PROGRESS_FILE, 'w') as f:
99
+ json.dump(data, f)
100
+
101
+ # ─── ΠŸΠΠ Π‘Π˜ΠΠ“ ──────────────────────────────────────────────────
102
+ def extract_text(html: str):
103
+ soup = BeautifulSoup(html, 'html.parser')
104
+
105
+ main = soup.find('main', class_='deeplink_main')
106
+ if main:
107
+ p = main.find('p')
108
+ if p:
109
+ parts = [node.strip() for node in p.descendants if isinstance(node, str) and node.strip()]
110
+ result = ' '.join(parts).strip()
111
+ if ' on Link. ' in result:
112
+ result = result.split(' on Link. ', 1)[1].strip()
113
+ elif result.endswith(' on Link.'):
114
+ return None
115
+ if result:
116
+ return result
117
+
118
+ og = soup.find('meta', property='og:description')
119
+ if og:
120
+ content = og.get('content', '').strip()
121
+ if ' on Link. ' in content:
122
+ content = content.split(' on Link. ', 1)[1].strip()
123
+ elif content.endswith(' on Link.'):
124
+ return None
125
+ if content:
126
+ return content
127
+
128
+ return None
129
+
130
+ async def fetch_post(session, post_id):
131
+ try:
132
+ async with session.get(
133
+ BASE_URL.format(post_id),
134
+ timeout=aiohttp.ClientTimeout(total=REQUEST_TIMEOUT)
135
+ ) as resp:
136
+ if resp.status != 200:
137
+ return post_id, None
138
+ html = await resp.text(encoding='utf-8', errors='replace')
139
+ return post_id, extract_text(html)
140
+ except Exception:
141
+ return post_id, None
142
+
143
+ def save_batch(rows):
144
+ if not rows:
145
+ return
146
+ conn = sqlite3.connect(DB_PATH)
147
+ conn.executemany('INSERT OR IGNORE INTO posts (post_id, text) VALUES (?, ?)', rows)
148
+ conn.commit()
149
+ conn.close()
150
+
151
+ # ─── PUSH Π’ HUGGINGFACE ───────────────────────────────────────
152
+ def push_to_hf(progress):
153
+ if not HF_TOKEN or not HF_DATASET:
154
+ return
155
+ try:
156
+ api = HfApi(token=HF_TOKEN)
157
+ api.upload_file(
158
+ path_or_fileobj=DB_PATH,
159
+ path_in_repo='linkm_posts.db',
160
+ repo_id=HF_DATASET,
161
+ repo_type='dataset',
162
+ )
163
+ api.upload_file(
164
+ path_or_fileobj=PROGRESS_FILE,
165
+ path_in_repo='linkm_progress.json',
166
+ repo_id=HF_DATASET,
167
+ repo_type='dataset',
168
+ )
169
+ print(f'βœ… Π—Π°ΠΏΡƒΡˆΠ΅Π½ΠΎ Π² HF (Π½Π°ΠΉΠ΄Π΅Π½ΠΎ: {progress["found"]:,})')
170
+ state['last_push'] = progress['found']
171
+ except Exception as e:
172
+ print(f'❌ Ошибка ΠΏΡƒΡˆΠ°: {e}')
173
+
174
+ # ─── ГЛАВНЫЙ Π¦Π˜ΠšΠ› ─────────────────────────────────────────────
175
+ async def run_parser():
176
+ pull_from_hf()
177
+ init_db()
178
+
179
+ progress = load_progress()
180
+ current_id = progress['next_id']
181
+ start_time = time.time()
182
+
183
+ state['status'] = 'running'
184
+ state['current_id'] = current_id
185
+ state['found'] = progress['found']
186
+ state['processed'] = progress['processed']
187
+ state['last_push'] = progress['found']
188
+
189
+ connector = aiohttp.TCPConnector(limit=WORKERS, ssl=False)
190
+
191
+ async with aiohttp.ClientSession(connector=connector, headers=HEADERS) as session:
192
+ while current_id > END_ID:
193
+ batch_start = max(current_id - BATCH_SIZE, END_ID)
194
+ batch_ids = list(range(current_id, batch_start, -1))
195
+ results = await asyncio.gather(*[fetch_post(session, pid) for pid in batch_ids])
196
+
197
+ found_rows = [(pid, txt) for pid, txt in results if txt]
198
+ save_batch(found_rows)
199
+
200
+ progress['processed'] += len(batch_ids)
201
+ progress['found'] += len(found_rows)
202
+ progress['next_id'] = batch_start
203
+ save_progress(progress)
204
+
205
+ elapsed = time.time() - start_time
206
+ speed = progress['processed'] / max(elapsed, 1)
207
+
208
+ state['current_id'] = current_id
209
+ state['found'] = progress['found']
210
+ state['processed'] = progress['processed']
211
+ state['speed'] = round(speed)
212
+
213
+ print(f'ID: {current_id:,} | НайдСно: {progress["found"]:,} | {speed:.0f} ID/сСк')
214
+
215
+ if progress['found'] - state['last_push'] >= PUSH_EVERY:
216
+ push_to_hf(progress)
217
+
218
+ current_id = batch_start
219
+
220
+ state['status'] = 'done'
221
+ push_to_hf(progress)
222
+
223
+ def start_parser():
224
+ asyncio.run(run_parser())
225
+
226
+ # ─── FASTAPI ──────────────────────────────────────────────────
227
+ app = FastAPI()
228
+
229
+ @app.get('/status')
230
+ def get_status():
231
+ elapsed = time.time() - state['started_at']
232
+ remaining_ids = state['current_id'] - END_ID
233
+ remaining_sec = remaining_ids / max(state['speed'], 1)
234
+ return {
235
+ 'status': state['status'],
236
+ 'current_id': state['current_id'],
237
+ 'end_id': END_ID,
238
+ 'percent': round((START_ID - state['current_id']) / START_ID * 100, 2),
239
+ 'found': state['found'],
240
+ 'processed': state['processed'],
241
+ 'speed_per_sec': state['speed'],
242
+ 'elapsed_min': round(elapsed / 60, 1),
243
+ 'remaining_min': round(remaining_sec / 60, 1),
244
+ 'last_push_at': state['last_push'],
245
+ }
246
+
247
+ @app.get('/search')
248
+ def search(q: str, limit: int = 50):
249
+ conn = sqlite3.connect(DB_PATH)
250
+ c = conn.cursor()
251
+ c.execute('SELECT post_id, text FROM posts WHERE text LIKE ? LIMIT ?', (f'%{q}%', limit))
252
+ rows = c.fetchall()
253
+ conn.close()
254
+ return {
255
+ 'query': q,
256
+ 'count': len(rows),
257
+ 'results': [{'post_id': r[0], 'text': r[1]} for r in rows]
258
+ }
259
+
260
+ # ─── БВАРВ ────────────────────────────────────────────────────
261
+ if __name__ == '__main__':
262
+ t = threading.Thread(target=start_parser, daemon=True)
263
+ t.start()
264
+ uvicorn.run(app, host='0.0.0.0', port=7860)
265
+