tolke_fetch.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. #!/usr/bin/env python3
  2. """tolke_fetch.py — URL-i lugemine tõlkeboti jaoks.
  3. Ahel: curl (otsefetch) -> vajadusel FlareSolverr (Cloudflare) -> Trafilatura (DOM).
  4. Kasutus käsureal: echo '{"url":"..."}' | python3 tolke_fetch.py
  5. Raamatukoguna: from tolke_fetch import fetch; fetch(url)
  6. """
  7. import json
  8. import os
  9. import re
  10. import subprocess
  11. import sys
  12. UA = (
  13. "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
  14. "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
  15. )
  16. FLARE = os.environ.get("TOLKE_FLARE_URL", "http://100.87.1.24:8191/v1")
  17. # Tegelikud blokitunnused. NB: "challenge-platform" esineb KA edukal
  18. # Cloudflare-lehel (precursor-skript) -> seda üksi ei tohi blokiks lugeda.
  19. HARD_BLOCK_RE = re.compile(
  20. r"just a moment|Enable JavaScript and cookies|Attention Required|"
  21. r"_cf_chl_opt|cf-mitigated|Ray ID|cf-error-details|"
  22. r"checking your browser|Verifying you are human|DDoS protection by",
  23. re.I,
  24. )
  25. # 200-vastuse sees olev turvakontrolli tekst
  26. SOFT_BLOCK_RE = re.compile(
  27. r"security service to protect itself|triggered the security solution|"
  28. r"verify you are human|checking your browser",
  29. re.I,
  30. )
  31. _extract = None
  32. _IMPORT_ERR = ""
  33. def _load_extractor():
  34. global _extract, _IMPORT_ERR
  35. if _extract is not None:
  36. return _extract
  37. try:
  38. from trafilatura import extract as _e # noqa
  39. _extract = _e
  40. except Exception as e: # pragma: no cover
  41. _IMPORT_ERR = str(e)[:200]
  42. return _extract
  43. def run(cmd, timeout=180):
  44. try:
  45. p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
  46. return p.stdout
  47. except Exception as e:
  48. return "__ERR__" + str(e)[:200]
  49. def direct(url):
  50. return run(
  51. ["curl", "-sL", "--max-time", "60", "-A", UA,
  52. "-H", "Accept-Language: et,en;q=0.9", "--compressed", url],
  53. timeout=75,
  54. )
  55. def solverr(url):
  56. body = json.dumps({"cmd": "request.get", "url": url, "maxTimeout": 120000})
  57. out = run(
  58. ["curl", "-s", "--max-time", "145", "-X", "POST", FLARE,
  59. "-H", "Content-Type: application/json", "-d", body],
  60. timeout=160,
  61. )
  62. if out.startswith("__ERR__"):
  63. return ""
  64. try:
  65. return (json.loads(out).get("solution") or {}).get("response") or ""
  66. except Exception:
  67. return ""
  68. def is_blocked(html):
  69. if not html or len(html) < 2000:
  70. return True
  71. head = html[:60000]
  72. if HARD_BLOCK_RE.search(head):
  73. return True
  74. if len(html) < 20000 and SOFT_BLOCK_RE.search(head):
  75. return True
  76. return False
  77. def extract(html):
  78. """Trafilatura Python API. Tagastab (title, body_markdown)."""
  79. ex = _load_extractor()
  80. if not html or ex is None:
  81. return "", ""
  82. h = re.sub(r"<(script|style)[^>]*>.*?</\1>", "", html, flags=re.S | re.I)
  83. h = re.sub(r"<!--.*?-->", "", h, flags=re.S)
  84. try:
  85. body = ex(h, output_format="markdown", include_comments=False) or ""
  86. except Exception:
  87. return "", ""
  88. title = ""
  89. m = re.match(r"^#\s+(.+?)\s*$", body, re.M)
  90. if m:
  91. title = m.group(1).strip()
  92. return title, re.sub(r"\n{3,}", "\n\n", body).strip()
  93. def fetch(url, mode="auto"):
  94. """Tagastab dict: {ok, url, title, body, chars, via, reason}."""
  95. url = (url or "").strip()
  96. if not url:
  97. return {"ok": False, "url": "", "reason": "url puudub"}
  98. if _load_extractor() is None:
  99. return {"ok": False, "url": url,
  100. "reason": "trafilatura import: " + _IMPORT_ERR}
  101. via = "direct"
  102. html = "" if mode == "solverr" else direct(url)
  103. if mode == "auto" and is_blocked(html):
  104. solved = solverr(url)
  105. if solved and not is_blocked(solved):
  106. html, via = solved, "solverr"
  107. title, body = extract(html)
  108. # Ka 200-vastuse tekst võib olla blokiteade -> proovi Solverrit
  109. if (not body or len(body) < 120) and mode == "auto" and via == "direct":
  110. solved = solverr(url)
  111. if solved and not is_blocked(solved):
  112. html, via = solved, "solverr"
  113. title, body = extract(html)
  114. if body and len(body) > 120 and SOFT_BLOCK_RE.search(body[:800]):
  115. return {"ok": False, "url": url, "via": via,
  116. "reason": "leht nõuab turvakontrolli (Cloudflare)",
  117. "chars": len(body)}
  118. if not body or len(body) < 120:
  119. return {"ok": False, "url": url, "via": via,
  120. "reason": "sisu ei õnnestunud ekstraktida (tõke või tühi leht)",
  121. "chars": len(body or "")}
  122. return {"ok": True, "url": url, "title": title, "body": body,
  123. "chars": len(body), "via": via}
  124. def main():
  125. try:
  126. data = json.loads(sys.stdin.read() or "{}")
  127. except Exception:
  128. data = {}
  129. res = fetch(data.get("url") or "", data.get("mode") or "auto")
  130. print(json.dumps(res, ensure_ascii=False))
  131. return 0
  132. if __name__ == "__main__":
  133. sys.exit(main())