Научный форум dxdy

Математика, Физика, Computer Science, Machine Learning, LaTeX, Механика и Техника, Химия,
Биология и Медицина, Экономика и Финансовая Математика, Гуманитарные науки




На страницу Пред.  1 ... 40, 41, 42, 43, 44
 Re: Любимые пирожки, порошки, двустишия и близкие формы
Изображение

 Re: Любимые пирожки, порошки, двустишия и близкие формы
Аватара пользователя
- приникнув глазом к окуляру
олег увидел в микроскоп
обсерваторию простейших
и телескопа объектив

- теперь на улицу из дома
олег не может выходить
он понимает звезды смотрят
а про галактику молчу

 Re: Любимые пирожки, порошки, двустишия и близкие формы

(Оффтоп)

Евгений Машеров в сообщении #1737560 писал(а):
... он понимает звезды смотрят...

Когда-то (давно, подробности уже забыл) я наткнулся в интернете на смешной стишок. Помнится, вроде, текст был без рифмы, и, кажется, его автор — косящий под алкаша и матерщинника (а может он и вправду такой :) А. Левченко. Но это не точно. Позже я попытался снова отыскать тот как бы стих, но не нашёл.

Пересказываю сюжет своими словами. В общем, персонаж, от лица которого автор ведёт повествование, вышел ночью из избы во двор по нужде. Сидя на корточках, он взглянул на небо. Видит: оно всё звёздами усыпано. Ему и подумалось: "если я могу смотреть туда, то и на меня могут смотреть оттуда". Впечатлённый этой мыслью, он, поплевав на ладони, поправил причёску, и сел поровнее.

Никому такое не попадалось? :)

 Re: Любимые пирожки, порошки, двустишия и близкие формы
Аватара пользователя
возьмём историю народов
возьмём историю камней
есть ощущение что камни
умней

наверно хорошо в гробу то
вот так наряженным лежать
когда нибудь я тоже так же
о боже только бы дожить

в театре дурова поставил
аркадий басню слон и глист
о том как путь к свободе долог
и мглист

семён глядит в трюмо сражённый
невероятной красотой
взгляд переводит на невесту
не пара ты мне зульфия

дайте мне на время
кто нибудь жену
я попрактикуюсь
и назад верну

 Скрипт: скачать все пирожки и порошки с poetory.ru (1/2)
Аватара пользователя

(Скрипт для скачивания стихов с poetory.ru)

Код:
# Скачивает стихи с poetory.ru через его открытый API и сохраняет в
# poems.db — ту самую базу, которую читает main.pyw. Вместе с текстом
# хранятся метаданные: номер на сайте, автор, дата публикации, лайки,
# рейтинг и источники публикации («Пирожковая», «Порошки», ...) в поле section.
#
# Внешних зависимостей НЕТ — только стандартная библиотека Python
# (BeautifulSoup прежней версии больше не нужна).
#
# API (проверено try_api.py): GET https://api.poetory.ru/api/content/
# отдаёт список стихов пачками (page, page_size) и поле count — точное
# число стихов на сайте (на момент проверки — 134 770).
#
# Запуск:
#     py scrape_poetory.py                    # первые 100 стихов
#     py scrape_poetory.py 2000               # первые 2000
#     py scrape_poetory.py --all              # ВСЁ, ~1400 запросов, около часа
#     py scrape_poetory.py --all --start 50000  # с номера 50000
#     py scrape_poetory.py --refresh          # пройти весь список с начала:
#                                             #   обновить лайки/рейтинг у
#                                             #   скачанных и добавить новые
#
# Повторный запуск безопасен И БЫСТР: скрипт сам находит первый
# нескачанный номер и продолжает с его страницы (--start больше не нужен
# для продолжения). Уже скачанные записи, встретившиеся по пути,
# обновляются (лайки, рейтинг). Ctrl+C безопасен в любой момент.

import argparse
import gzip
import json
import os
import random
import re
import sqlite3
import time
from collections import namedtuple
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(BASE_DIR, "poems.db")

API = "https://api.poetory.ru"
LIST_URL = f"{API}/api/content/"

# Стих №1 на сайте — «когда играют кастаньеты». По нему проверяем,
# что номера в API совпадают с номерами в адресах сайта.
FIRST_POEM_SNIPPET = "кастаньеты"

HEADERS = {
    "User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                   "AppleWebKit/537.36 (KHTML, like Gecko) "
                   "Chrome/126.0 Safari/537.36"),
    "Accept": "application/json, */*;q=0.8",
    "Accept-Language": "ru,en;q=0.8",
    "Accept-Encoding": "identity",
}

# Всё, что нужно знать об одном стихе
Poem = namedtuple("Poem", "text author date section likes rating")

NAME_RE = re.compile(r'"name":"((?:[^"\\]|\\.)*)"')   # запасной поиск имени


class NotFound(Exception):
    """Сервер ответил 404 (страница списка кончилась / стих не существует)."""


def ensure_schema(conn):
    """Создаёт таблицу poems; если база старая — дополняет полями.

    main.pyw читает столбцы content, author, section — они называются
    именно так. site_id, likes, rating и date — метаданные.
    """
    conn.execute(
        """
        CREATE TABLE IF NOT EXISTS poems (
            id      INTEGER PRIMARY KEY AUTOINCREMENT,  -- локальный номер
            site_id INTEGER,                            -- номер на сайте
            content TEXT NOT NULL,                      -- текст стихотворения
            author  TEXT,                               -- автор
            section TEXT,                               -- источники публикации
            date    TEXT,                               -- дата публикации YYYY-MM-DD
            likes   INTEGER,                            -- суммарные лайки
            rating  REAL                                -- рейтинг сайта
        )
        """
    )
    cols = {row[1] for row in conn.execute("PRAGMA table_info(poems)")}
    for name, decl in (("site_id", "INTEGER"), ("date", "TEXT"),
                       ("likes", "INTEGER"), ("rating", "REAL")):
        if name not in cols:
            conn.execute(f"ALTER TABLE poems ADD COLUMN {name} {decl}")
    conn.execute("CREATE INDEX IF NOT EXISTS idx_poems_site_id ON poems(site_id)")
    conn.commit()


def fetch(url, timeout=20, attempts=4, base_wait=10):
    """Скачивает URL; при сетевом сбое, 429 и 5xx повторяет с паузой.

    Остальные HTTP-ошибки (404 и т.п.) отдаём вызывающему коду.
    """
    request = Request(url, headers=HEADERS)
    for attempt in range(attempts):
        try:
            with urlopen(request, timeout=timeout) as response:
                data = response.read()
                if (response.headers.get("Content-Encoding") or "").lower() == "gzip":
                    data = gzip.decompress(data)
                return data
        except HTTPError as error:
            # 429 «слишком много запросов» и ошибки сервера — временные
            if error.code != 429 and error.code < 500:
                raise
            if attempt == attempts - 1:
                raise
            wait = base_wait * (attempt + 1)
            print(f"    сервер ответил {error.code}: жду {wait} с и повторяю...")
            time.sleep(wait)
        except (URLError, TimeoutError, OSError) as error:
            if attempt == attempts - 1:
                raise
            wait = base_wait * (attempt + 1)
            print(f"    сбой: {error}\n    жду {wait} с и повторяю...")
            time.sleep(wait)


def fetch_json(url):
    """Скачивает URL и разбирает JSON; 404 превращает в NotFound."""
    try:
        raw = fetch(url)
    except HTTPError as error:
        if error.code == 404:
            raise NotFound(url) from error
        raise
    try:
        return json.loads(raw.decode("utf-8"))
    except ValueError as error:
        path = os.path.join(BASE_DIR, "debug_api.json")
        try:
            with open(path, "wb") as file:
                file.write(raw)
            where = f" (сохранён в {path})"
        except OSError:
            where = ""
        raise RuntimeError(f"API прислал не JSON{where}: {error}") from error


def clean_text(raw):
    """Приводит текст стиха в порядок.

    Убирает подпись «© …» (автор хранится отдельным полем) и лишние
    пробелы, но сохраняет пустые строки между строфами.
    """
    lines = []
    for line in raw.replace("\r\n", "\n").replace("\r", "\n").split("\n"):
        line = line.replace("\xa0", " ").strip()
        if line.startswith("©"):
            continue
        if line:
            lines.append(re.sub(r"\s{2,}", " ", line))
        elif lines and lines[-1]:
            lines.append("")   # одна пустая строка = граница строфы
    while lines and not lines[-1]:
        lines.pop()
    return "\n".join(lines)


def parse_date(raw):
    """'2012-11-29T12:00:00Z' -> '2012-11-29'."""
    if not raw:
        return None
    match = re.match(r"\d{4}-\d{2}-\d{2}", str(raw))
    return match.group() if match else None


def author_from_copyright(raw_text):
    """Автор из подписи «© …» в тексте стиха."""
    for line in raw_text.split("\n"):
        line = line.strip()
        if line.startswith("©"):
            return line.lstrip("©").strip() or None
    return None


def authors_from_item(item, raw_text):
    """Автор(ы): поле nicknames, в крайнем случае подпись «© …».

    Структуру nicknames заранее не знаем (это может быть список строк,
    словарей с разными именами полей), поэтому пробуем варианты.
    """
    raw = item.get("nicknames")
    if isinstance(raw, dict):
        raw = [raw]
    names = []
    for entry in raw or []:
        name = None
        if isinstance(entry, str):
            name = entry
        elif isinstance(entry, dict):
            for key in ("nickname", "name", "nick", "title", "author"):
                value = entry.get(key)
                if isinstance(value, str) and value.strip():
                    name = value
                    break
            if name is None:
                value = entry.get("path")   # слаг «al-cogol» -> «al cogol»
                if isinstance(value, str) and value.strip():
                    name = value.replace("-", " ")
        name = (name or "").strip()
        if name and name not in names:
            names.append(name)
    return ", ".join(names) if names else author_from_copyright(raw_text)


def source_names_api(pub_set):
    """Имена источников публикации из publication_set (тут это настоящий JSON)."""
    names = []
    for entry in pub_set or []:
        if not isinstance(entry, dict):
            continue
        source = entry.get("content_source")
        name = source.get("name") if isinstance(source, dict) else None
        if not name:   # запасной вариант: ищем "name":"..." в тексте записи
            match = NAME_RE.search(json.dumps(entry, ensure_ascii=False))
            if match:
                name = match.group(1)
        name = str(name or "").strip()
        if name and name not in names:
            names.append(name)
    return ", ".join(names) or None


def item_to_poem(item):
    """JSON-запись API -> Poem; None, если текстового стиха в записи нет."""
    raw_text = str(item.get("item_content") or "")
    if not raw_text.strip():
        return None
    text = clean_text(raw_text)
    if not text:
        return None

    likes = item.get("likes")
    likes = int(likes) if isinstance(likes, (int, float)) else None
    rating = item.get("rating")
    try:
        rating = float(rating) if rating is not None else None
    except (TypeError, ValueError):
        rating = None

    return Poem(
        text,
        authors_from_item(item, raw_text),
        parse_date(item.get("first_pub")),
        source_names_api(item.get("publication_set")),
        likes,
        rating,
    )


def normalize(text):
    """Текст без пустых строк, подписи «© …» и лишних пробелов.

    Так сравниваем стихи со старыми записями базы, у которых нет site_id.
    """
    lines = []
    for line in text.split("\n"):
        line = line.replace("\xa0", " ").strip()
        if line and not line.startswith("©"):
            lines.append(re.sub(r"\s{2,}", " ", line))
    return "\n".join(lines)


def check_id_mapping(conn):
    """Проверяем, что id в API совпадает с номером в адресе сайта.

    Без проверки есть риск записать стихи не под теми номерами.
    При несовпадении останавливаемся (обход — флагом --trust-ids).
    """
    try:
        item = fetch_json(f"{LIST_URL}1")
    except NotFound:
        print("ПРОВЕРКА: /api/content/1 не найден — id в API не совпадает "
              "с номерами сайта. Останавливаюсь, пришлите этот вывод.")
        raise SystemExit(1)
    text = normalize(str(item.get("item_content") or "")).lower()
    if FIRST_POEM_SNIPPET not in text:
        print("ПРОВЕРКА НЕ ПРОЙДЕНА: /api/content/1 не похож на стих "
              "https://poetory.ru/1")
        print(f"  начало текста: {text[:80]!r}")
        print("Останавливаюсь, чтобы не записать стихи под чужими номерами.")
        print("Если это ложная тревога — перезапустите с --trust-ids.")
        raise SystemExit(1)
    row = conn.execute(
        "SELECT content FROM poems WHERE site_id = 1").fetchone()
    if row and normalize(row[0]).lower() != text:
        print("  внимание: стих №1 в базе отличается от сайта — "
              "его текст на сайте редактировали")
    print("Проверка id: /api/content/1 — тот же стих, что poetory.ru/1 — OK")



 Скрипт: скачать все пирожки и порошки с poetory.ru (2/2)
Аватара пользователя

(продолжение)

Код:
def fetch_first_page():
    """Подбирает размер страницы (page_size) и возвращает (размер, страницу 1).

    Пробуем 200/100/50/30: сервер либо отдаёт столько, сколько попросили,
    либо молча режет до своего максимума — берём фактически полученное.
    """
    for size in (200, 100, 50, 30):
        try:
            data = fetch_json(f"{LIST_URL}?page=1&page_size={size}")
        except NotFound:
            continue
        except (HTTPError, URLError, TimeoutError, OSError) as error:
            print(f"  page_size={size}: не вышел ({error}), пробую меньше")
            continue
        if not isinstance(data, dict):
            continue
        results = data.get("results")
        if isinstance(results, list) and results:
            actual = len(results)
            if actual < size:
                print(f"  сервер ограничил выдачу: просили {size}, "
                      f"получилось {actual}")
            return actual, data
    raise RuntimeError("API не отдал даже первую страницу списка")


def ordering_is_ascending(page_data):
    """Идут ли номера в списке по возрастанию с единицы.

    Если да — умеем вычислять страницу продолжения.
    """
    results = (page_data.get("results") or []) if isinstance(page_data, dict) else []
    ids = [item.get("id") for item in results if isinstance(item, dict)]
    if not ids or not all(isinstance(i, int) for i in ids):
        return False
    return ids[0] == 1 and all(a < b for a, b in zip(ids, ids[1:]))


def first_missing_id(known_ids):
    """Первый номер, которого нет в базе (номера идут с 1)."""
    candidate = 1
    while candidate in known_ids:
        candidate += 1
    return candidate


def find_start_page(min_missing, page_size):
    """Страница списка, с которой продолжать скачивание.

    Список идёт по возрастанию id (проверено), поэтому номер страницы
    можно оценить формулой и уточнить запросами: из-за дыр в нумерации
    (стихи удаляли) оценка уезжает вперёд — сдвигаемся, пока не накроем
    min_missing. Возвращает (страница, данные страницы) — страницу не
    придётся качать повторно. Особые случаи: ("end", None) — ничего
    нового нет; (None, None) — не получилось, качаем с первой страницы.
    """
    page = max(1, (min_missing - 1) // page_size + 1)
    for _ in range(30):
        try:
            data = fetch_json(f"{LIST_URL}?page={page}&page_size={page_size}")
        except NotFound:
            return "end", None
        ids = [item.get("id") for item in (data.get("results") or [])
               if isinstance(item, dict)]
        if not ids:
            return None, None
        if max(ids) < min_missing:
            page += 1                     # ещё не дошли до нужного номера
        elif min(ids) > min_missing and page > 1:
            page -= 1                     # перескочили из-за дыр в номерах
        else:
            return page, data
    return None, None


def do_crawl(conn, args, known_ids, legacy, limit):
    """Обход списка стихов через API: новые добавляем, скачанные обновляем."""
    print("Подключаюсь к API...")
    if args.page_size:
        page_size = args.page_size
        pending = fetch_json(f"{LIST_URL}?page=1&page_size={page_size}")
        print(f"Размер страницы (задан вручную): {page_size}")
    else:
        page_size, pending = fetch_first_page()

    count = pending.get("count") if isinstance(pending, dict) else None
    total_pages = None
    if isinstance(count, int) and count > 0:
        total_pages = (count + page_size - 1) // page_size
        print(f"На сайте {count} стихов; страниц списка: {total_pages} "
              f"(по {page_size} шт.)")
    else:
        print(f"Сервер не сообщил общее число стихов; страница: {page_size} шт.")

    if not args.trust_ids:
        check_id_mapping(conn)

    start_page = 1
    if args.refresh:
        print("Режим --refresh: прохожу список с первой страницы — "
              "скачанное обновится, новое добавится")
    elif known_ids or args.start > 1:
        min_missing = args.start if args.start > 1 else first_missing_id(known_ids)
        if ordering_is_ascending(pending):
            page, data = find_start_page(min_missing, page_size)
            if page == "end":
                print("Все стихи до конца списка уже в базе — нового нет")
                return 0, 0, 0, 0
            if page is not None:
                start_page, pending = page, data
                print(f"Продолжаю со страницы {start_page} "
                      f"(первый отсутствующий номер: {min_missing})")
            else:
                print("Не определил страницу продолжения — качаю с первой, "
                      "скачанные номера будут обновляться на месте")
        else:
            print("Список в API не по возрастанию номеров — качаю с первой "
                  "страницы, скачанные номера будут обновляться на месте")

    page = start_page
    processed = added = enriched = refreshed = empty = 0
    pages_done = 0
    started = time.monotonic()

    try:
        while processed < limit:
            if pending is not None:
                data, pending = pending, None
            else:
                data = fetch_json(f"{LIST_URL}?page={page}&page_size={page_size}")
            results = data.get("results") or []
            if not results:
                print(f"Страница {page}: пустая — список кончился")
                break

            page_added = page_refreshed = 0
            for item in results:
                if processed >= limit:
                    break
                try:
                    site_id = int(item["id"])
                except (KeyError, TypeError, ValueError):
                    continue
                if args.start > 1 and site_id < args.start:
                    continue

                poem = item_to_poem(item)
                if poem is None:
                    empty += 1
                    continue
                processed += 1

                text, author, date, section, likes, rating = poem
                key = normalize(text)
                if site_id in known_ids:
                    # Уже в базе: обновляем лайки/рейтинг, дозаполняем остальное
                    conn.execute(
                        "UPDATE poems SET likes = COALESCE(?, likes), "
                        "rating = COALESCE(?, rating), "
                        "section = COALESCE(section, ?), "
                        "date = COALESCE(date, ?), "
                        "author = COALESCE(NULLIF(author, ''), ?) "
                        "WHERE site_id = ?",
                        (likes, rating, section, date, author, site_id),
                    )
                    known_ids.add(site_id)
                    refreshed += 1
                    page_refreshed += 1
                elif key in legacy:
                    # Такой текст уже лежал в базе без номера — дополняем
                    conn.execute(
                        "UPDATE poems SET site_id = ?, "
                        "author = COALESCE(NULLIF(author, ''), ?), "
                        "date = COALESCE(date, ?), section = COALESCE(section, ?), "
                        "likes = COALESCE(likes, ?), rating = COALESCE(rating, ?) "
                        "WHERE rowid = ?",
                        (site_id, author, date, section, likes, rating,
                         legacy.pop(key)),
                    )
                    known_ids.add(site_id)
                    enriched += 1
                    page_added += 1
                else:
                    conn.execute(
                        "INSERT INTO poems (site_id, content, author, section, "
                        "date, likes, rating) VALUES (?, ?, ?, ?, ?, ?, ?)",
                        (site_id, text, author, section, date, likes, rating),
                    )
                    known_ids.add(site_id)
                    added += 1
                    page_added += 1
            conn.commit()
            pages_done += 1

            line = f"Страница {page:>6}"
            if total_pages:
                line += f"/{total_pages}"
            print(f"{line}: новых {page_added}, обновлено {page_refreshed}")

            if pages_done % 10 == 0:
                elapsed = time.monotonic() - started
                if elapsed > 0:
                    speed = pages_done / elapsed
                    eta = f", осталось ~{(total_pages - page) / speed / 60:.0f} мин" \
                          if total_pages else ""
                    print(f"  … {pages_done} стр., {speed * 60:.0f} стр/мин{eta}")

            if not data.get("next"):
                print("Поле next пустое — дошли до конца списка")
                break
            page += 1
            # Случайная пауза, чтобы запросы не шли как по линейке
            time.sleep(random.uniform(args.delay, args.delay * 2))
    except KeyboardInterrupt:
        print("\nОстановлено вручную (Ctrl+C) — всё скачанное уже сохранено в базе")
    except NotFound:
        print("\nСервер ответил 404 — страницы списка кончились (штатный конец)")
    except (HTTPError, URLError, TimeoutError, OSError, RuntimeError) as error:
        print(f"\nСбой: {error}")
        print("Продолжить: py scrape_poetory.py --all   "
              "(скрипт сам найдёт, где остановились)")

    return added, enriched, refreshed, empty


def main():
    parser = argparse.ArgumentParser(
        description="Скачивает стихи poetory.ru через API в poems.db")
    parser.add_argument("count", nargs="?", type=int, default=None,
                        help="сколько стихов обойти (по умолчанию 100; "
                             "с --all или --refresh — все)")
    parser.add_argument("--all", action="store_true",
                        help="скачать ВСЁ (по списку API)")
    parser.add_argument("--refresh", action="store_true",
                        help="пройти весь список с начала: обновить лайки/"
                             "рейтинг у скачанных и добавить новые")
    parser.add_argument("--start", type=int, default=1,
                        help="номер первого стиха (по умолчанию 1; "
                             "для продолжения не нужен — определяется сам)")
    parser.add_argument("--page-size", type=int, default=None,
                        help="стихов в одном запросе (по умолчанию "
                             "подбирается сам)")
    parser.add_argument("--delay", type=float, default=1.0,
                        help="минимальная пауза между запросами, сек "
                             "(по умолчанию 1.0)")
    parser.add_argument("--trust-ids", action="store_true",
                        help="не проверять совпадение id API с номерами сайта")
    args = parser.parse_args()

    if args.count is not None:
        limit = args.count
    elif args.all or args.refresh:
        limit = 10 ** 9
    else:
        limit = 100

    conn = sqlite3.connect(DB_PATH)
    ensure_schema(conn)

    known_ids = {row[0] for row in conn.execute(
        "SELECT site_id FROM poems WHERE site_id IS NOT NULL")}
    legacy = {normalize(text): rowid for text, rowid in conn.execute(
        "SELECT content, rowid FROM poems WHERE site_id IS NULL AND content IS NOT NULL")}
    print(f"В базе уже {len(known_ids) + len(legacy)} стихов")

    added, enriched, refreshed, empty = do_crawl(
        conn, args, known_ids, legacy, limit)

    total, with_author, with_date, with_likes, with_rating = conn.execute(
        "SELECT COUNT(*), COUNT(author), COUNT(date), COUNT(likes), "
        "COUNT(rating) FROM poems").fetchone()
    conn.close()

    print()
    print(f"Новых: {added}, дополнено старых: {enriched}, обновлено: {refreshed}")
    if empty:
        print(f"Записей без текста пропущено: {empty}")
    print(f"Всего в базе: {total}, с автором: {with_author}, "
          f"с датой: {with_date}, с лайками: {with_likes}, "
          f"с рейтингом: {with_rating}")


if __name__ == "__main__":
    main()

 [ Сообщений: 651 ]  На страницу Пред.  1 ... 40, 41, 42, 43, 44


Соглашение о конфиденциальности | Общие правила

Powered by phpBB © 2000, 2002, 2005, 2007 phpBB Group