netseecxld commited on
Commit
07250d5
Β·
verified Β·
1 Parent(s): 426fe31

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +260 -4
app.py CHANGED
@@ -189,6 +189,262 @@ async def run_parser():
189
 
190
  connector = aiohttp.TCPConnector(limit=WORKERS, ssl=False)
191
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  async with aiohttp.ClientSession(connector=connector, headers=HEADERS) as session:
193
  while current_id > END_ID:
194
  batch_start = max(current_id - BATCH_SIZE, END_ID)
@@ -247,10 +503,11 @@ def get_status():
247
 
248
  @app.get('/search')
249
  def search(q: str, limit: int = 50):
250
- conn = sqlite3.connect(DB_PATH, timeout=30) # ← Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ timeout=30
251
- conn.execute('PRAGMA journal_mode=WAL') # ← Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ
252
  c = conn.cursor()
253
- c.execute('SELECT post_id, text FROM posts WHERE text LIKE ? LIMIT ?', (f'%{q}%', limit))
 
254
  rows = c.fetchall()
255
  conn.close()
256
  return {
@@ -264,4 +521,3 @@ if __name__ == '__main__':
264
  t = threading.Thread(target=start_parser, daemon=True)
265
  t.start()
266
  uvicorn.run(app, host='0.0.0.0', port=7860)
267
-
 
189
 
190
  connector = aiohttp.TCPConnector(limit=WORKERS, ssl=False)
191
 
192
+ async with aiohttp.ClientSession(connector=connector, headers=HEADERS) as session:
193
+ while current_id > END_ID:
194
+ batch_start = max(current_id - BATCH_SIZE, END_ID)
195
+ batch_ids = list(range(current_id, batch_start, -1))
196
+ results = await asyncio.gather(*[fetch_post(session, pid) for pid in batch_ids])
197
+
198
+ found_rows = [(pid, txt) for pid, txt in results if txt]
199
+ save_batch(found_rows)
200
+
201
+ progress['processed'] += len(batch_ids)
202
+ progress['found'] += len(found_rows)
203
+ progress['next_id'] = batch_start
204
+ save_progress(progress)
205
+
206
+ elapsed = time.time() - start_time
207
+ speed = progress['processed'] / max(elapsed, 1)
208
+
209
+ state['current_id'] = current_id
210
+ state['found'] = progress['found']
211
+ state['processed'] = progress['processed']
212
+ state['speed'] = round(speed)
213
+
214
+ print(f'ID: {current_id:,} | НайдСно: {progress["found"]:,} | {speed:.0f} ID/сСк')
215
+
216
+ if progress['found'] - state['last_push'] >= PUSH_EVERY:
217
+ push_to_hf(progress)
218
+
219
+ current_id = batch_start
220
+ import sqlite3
221
+ import asyncio
222
+ import aiohttp
223
+ import json
224
+ import os
225
+ import time
226
+ import threading
227
+ from bs4 import BeautifulSoup
228
+ from fastapi import FastAPI
229
+ from huggingface_hub import HfApi, hf_hub_download
230
+ import uvicorn
231
+
232
+ # ─── ΠΠΠ‘Π’Π ΠžΠ™ΠšΠ˜ ────────────────────────────────────────────────
233
+ DB_PATH = '/app/linkm_posts.db'
234
+ PROGRESS_FILE = '/app/linkm_progress.json'
235
+ BASE_URL = 'https://linkm.me/posts/{}'
236
+ START_ID = 213470273
237
+ END_ID = START_ID
238
+ WORKERS = 150
239
+ BATCH_SIZE = 500
240
+ REQUEST_TIMEOUT = 10
241
+ HF_TOKEN = os.environ.get('HF_TOKEN')
242
+ HF_DATASET = os.environ.get('HF_DATASET')
243
+ PUSH_EVERY = 10000 # ΠΏΡƒΡˆΠΈΡ‚ΡŒ ΠΊΠ°ΠΆΠ΄Ρ‹Π΅ 10k Π½Π°ΠΉΠ΄Π΅Π½Π½Ρ‹Ρ…
244
+
245
+ HEADERS = {
246
+ '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',
247
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
248
+ 'Accept-Language': 'en-US,en;q=0.5',
249
+ }
250
+
251
+ # ─── Π‘ΠžΠ‘Π’ΠžΠ―ΠΠ˜Π• ────────────────────────────────────────────────
252
+ state = {
253
+ 'status': 'starting',
254
+ 'current_id': START_ID,
255
+ 'found': 0,
256
+ 'processed': 0,
257
+ 'speed': 0,
258
+ 'last_push': 0,
259
+ 'started_at': time.time(),
260
+ }
261
+
262
+ # ─── PULL Π‘ HF ПРИ БВАРВЕ ─────────────────────────────────────
263
+ def pull_from_hf():
264
+ if not HF_TOKEN or not HF_DATASET:
265
+ return
266
+ try:
267
+ hf_hub_download(
268
+ repo_id=HF_DATASET,
269
+ filename='linkm_posts.db',
270
+ repo_type='dataset',
271
+ local_dir='/app',
272
+ token=HF_TOKEN,
273
+ )
274
+ print('βœ… Π‘Π°Π·Π° скачана с HF')
275
+ except Exception as e:
276
+ print(f'⚠️ НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΡΠΊΠ°Ρ‡Π°Ρ‚ΡŒ Π±Π°Π·Ρƒ: {e}')
277
+
278
+ try:
279
+ hf_hub_download(
280
+ repo_id=HF_DATASET,
281
+ filename='linkm_progress.json',
282
+ repo_type='dataset',
283
+ local_dir='/app',
284
+ token=HF_TOKEN,
285
+ )
286
+ print('βœ… ΠŸΡ€ΠΎΠ³Ρ€Π΅ΡΡ скачан с HF')
287
+ except Exception as e:
288
+ print(f'⚠️ ΠŸΡ€ΠΎΠ³Ρ€Π΅ΡΡ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ Π½Π° HF: {e}')
289
+
290
+ # ─── БАЗА ДАННЫΠ₯ ──────────────────────────────────────────────
291
+ def init_db():
292
+ conn = sqlite3.connect(DB_PATH)
293
+ conn.execute('PRAGMA journal_mode=WAL')
294
+ c = conn.cursor()
295
+ c.execute('''
296
+ CREATE TABLE IF NOT EXISTS posts (
297
+ post_id INTEGER PRIMARY KEY,
298
+ text TEXT NOT NULL,
299
+ parsed_at TEXT DEFAULT (datetime('now'))
300
+ )
301
+ ''')
302
+ c.execute('''
303
+ CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
304
+ text,
305
+ content='posts',
306
+ content_rowid='post_id',
307
+ tokenize='trigram'
308
+ )
309
+ ''')
310
+ c.execute('''
311
+ CREATE TRIGGER IF NOT EXISTS posts_ai AFTER INSERT ON posts BEGIN
312
+ INSERT INTO posts_fts(rowid, text) VALUES (new.post_id, new.text);
313
+ END
314
+ ''')
315
+ conn.commit()
316
+ conn.close()
317
+
318
+ # ─── ΠžΠ”ΠΠžΠ ΠΠ—ΠžΠ’ΠΠ― ΠœΠ˜Π“Π ΠΠ¦Π˜Π― Π’ FTS5 ───────────────────────────────
319
+ def migrate_to_fts():
320
+ conn = sqlite3.connect(DB_PATH, timeout=120)
321
+ conn.execute('PRAGMA journal_mode=WAL')
322
+ c = conn.cursor()
323
+ c.execute('SELECT COUNT(*) FROM posts_fts')
324
+ fts_count = c.fetchone()[0]
325
+ c.execute('SELECT COUNT(*) FROM posts')
326
+ posts_count = c.fetchone()[0]
327
+
328
+ if fts_count < posts_count:
329
+ print(f'πŸ”¨ Π‘Ρ‚Ρ€ΠΎΡŽ FTS индСкс: {fts_count:,} / {posts_count:,}, заполняю...')
330
+ c.execute('''
331
+ INSERT INTO posts_fts(rowid, text)
332
+ SELECT post_id, text FROM posts
333
+ WHERE post_id NOT IN (SELECT rowid FROM posts_fts)
334
+ ''')
335
+ conn.commit()
336
+ print('βœ… FTS индСкс построСн')
337
+ else:
338
+ print('βœ… FTS индСкс ΡƒΠΆΠ΅ Π°ΠΊΡ‚ΡƒΠ°Π»Π΅Π½')
339
+ conn.close()
340
+
341
+ # ─── ΠŸΠ ΠžΠ“Π Π•Π‘Π‘ ─────────────────────────────────────────────────
342
+ def load_progress():
343
+ if os.path.exists(PROGRESS_FILE):
344
+ try:
345
+ with open(PROGRESS_FILE, 'r') as f:
346
+ data = json.load(f)
347
+ print(f'▢️ ΠŸΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠ°Π΅ΠΌ с ID: {data["next_id"]} (Π½Π°ΠΉΠ΄Π΅Π½ΠΎ: {data["found"]:,})')
348
+ return data
349
+ except Exception:
350
+ pass
351
+ return {'next_id': START_ID, 'processed': 0, 'found': 0}
352
+
353
+ def save_progress(data):
354
+ with open(PROGRESS_FILE, 'w') as f:
355
+ json.dump(data, f)
356
+
357
+ # ─── ΠŸΠΠ Π‘Π˜ΠΠ“ ──────────────────────────────────────────────────
358
+ def extract_text(html: str):
359
+ soup = BeautifulSoup(html, 'html.parser')
360
+
361
+ main = soup.find('main', class_='deeplink_main')
362
+ if main:
363
+ p = main.find('p')
364
+ if p:
365
+ parts = [node.strip() for node in p.descendants if isinstance(node, str) and node.strip()]
366
+ result = ' '.join(parts).strip()
367
+ if ' on Link. ' in result:
368
+ result = result.split(' on Link. ', 1)[1].strip()
369
+ elif result.endswith(' on Link.'):
370
+ return None
371
+ if result:
372
+ return result
373
+
374
+ og = soup.find('meta', property='og:description')
375
+ if og:
376
+ content = og.get('content', '').strip()
377
+ if ' on Link. ' in content:
378
+ content = content.split(' on Link. ', 1)[1].strip()
379
+ elif content.endswith(' on Link.'):
380
+ return None
381
+ if content:
382
+ return content
383
+
384
+ return None
385
+
386
+ async def fetch_post(session, post_id):
387
+ try:
388
+ async with session.get(
389
+ BASE_URL.format(post_id),
390
+ timeout=aiohttp.ClientTimeout(total=REQUEST_TIMEOUT)
391
+ ) as resp:
392
+ if resp.status != 200:
393
+ return post_id, None
394
+ html = await resp.text(encoding='utf-8', errors='replace')
395
+ return post_id, extract_text(html)
396
+ except Exception:
397
+ return post_id, None
398
+
399
+ def save_batch(rows):
400
+ if not rows:
401
+ return
402
+ conn = sqlite3.connect(DB_PATH)
403
+ conn.executemany('INSERT OR IGNORE INTO posts (post_id, text) VALUES (?, ?)', rows)
404
+ conn.commit()
405
+ conn.close()
406
+
407
+ # ─── PUSH Π’ HUGGINGFACE ───────────────────────────────────────
408
+ def push_to_hf(progress):
409
+ if not HF_TOKEN or not HF_DATASET:
410
+ return
411
+ try:
412
+ api = HfApi(token=HF_TOKEN)
413
+ api.upload_file(
414
+ path_or_fileobj=DB_PATH,
415
+ path_in_repo='linkm_posts.db',
416
+ repo_id=HF_DATASET,
417
+ repo_type='dataset',
418
+ )
419
+ api.upload_file(
420
+ path_or_fileobj=PROGRESS_FILE,
421
+ path_in_repo='linkm_progress.json',
422
+ repo_id=HF_DATASET,
423
+ repo_type='dataset',
424
+ )
425
+ print(f'βœ… Π—Π°ΠΏΡƒΡˆΠ΅Π½ΠΎ Π² HF (Π½Π°ΠΉΠ΄Π΅Π½ΠΎ: {progress["found"]:,})')
426
+ state['last_push'] = progress['found']
427
+ except Exception as e:
428
+ print(f'❌ Ошибка ΠΏΡƒΡˆΠ°: {e}')
429
+
430
+ # ─── ГЛАВНЫЙ Π¦Π˜ΠšΠ› ─────────────────────────────────────────────
431
+ async def run_parser():
432
+ pull_from_hf()
433
+ init_db()
434
+ migrate_to_fts()
435
+
436
+ progress = load_progress()
437
+ current_id = progress['next_id']
438
+ start_time = time.time()
439
+
440
+ state['status'] = 'running'
441
+ state['current_id'] = current_id
442
+ state['found'] = progress['found']
443
+ state['processed'] = progress['processed']
444
+ state['last_push'] = progress['found']
445
+
446
+ connector = aiohttp.TCPConnector(limit=WORKERS, ssl=False)
447
+
448
  async with aiohttp.ClientSession(connector=connector, headers=HEADERS) as session:
449
  while current_id > END_ID:
450
  batch_start = max(current_id - BATCH_SIZE, END_ID)
 
503
 
504
  @app.get('/search')
505
  def search(q: str, limit: int = 50):
506
+ conn = sqlite3.connect(DB_PATH, timeout=30)
507
+ conn.execute('PRAGMA journal_mode=WAL')
508
  c = conn.cursor()
509
+ safe_q = q.replace('"', '""')
510
+ c.execute('SELECT post_id, text FROM posts_fts WHERE posts_fts MATCH ? LIMIT ?', (f'"{safe_q}"', limit))
511
  rows = c.fetchall()
512
  conn.close()
513
  return {
 
521
  t = threading.Thread(target=start_parser, daemon=True)
522
  t.start()
523
  uvicorn.run(app, host='0.0.0.0', port=7860)