Shanraq.org Shanraq.org
Кесте: аргументтер, журнал, құлып және қайтару коды
IT

Python: деректен өз есебіңізге дейін 23-сабақ (барлығы 56)

Кесте: аргументтер, журнал, құлып және қайтару коды

Python курсының жиырма үшінші сабағы. Түнде сізсіз жұмыс істейтін бағдарлама: кодты түзетудің орнына аргументтер, `print` орнына деңгейлері бар журнал, қатар жүретін екі іске қосуға қарсы құлып және кестенің жалғыз оқитын нәрсесі — қайтару коды. Плюс cron, launchd пен systemd.

Не үшін керек

Шолу деректі алуды, дерекқорға салуды және сұраныспен жауап беруді үйренді. Соңғысы қалды: бұның бәрі сізсіз болып тұруы керек.

Жоспарлағыш бойынша іске қосылған бағдарлама басқа дүниеде тұрады. Оның шығуын ешкім көрмейді. Күнді кодта ешкім түзетпейді. Ол алдыңғысы әлі жұмыс істеп тұрғанда екінші рет іске қосылуы мүмкін. Ал жүйе ол туралы білетін жалғыз нәрсе — оның қайтарған саны.

Сондықтан сабақ cron туралы емес. Ол cron-ды пайдасыз ететін төрт нәрсе туралы.

Бірден тұтас

kesteme.py файлы. Іске қосу: ортадан python kesteme.py.

Міндеттісі — алғашқы үш блок: аргументтер, журнал және құлып. Төртіншісі кестенің бағдарламаға мүлде қарау себебі болатын қайтару кодын көрсетеді.

"""23-сабақ: сізсіз жұмыс істейтін бағдарлама.

Шолуды кесте бойынша іске қосатын кез келді. Онсыз бұл жұмыс емес, үміт болатын
үш нәрсе: кодты түзетудің орнына аргументтер, print орнына журнал және екеудің
орнына бір дана.
"""

import argparse
import logging
import subprocess
import sys
from pathlib import Path

HERE = Path(__file__).resolve().parent
LOG = HERE / "svodka.log"
LOCK = HERE / "svodka.lock"

print("== кодты түзетудің орнына аргументтер")
parser = argparse.ArgumentParser(description="Бір күндік шолу")
parser.add_argument("--day", default="бүгін", help="күн ЖЖЖЖ-АА-КК түрінде")
parser.add_argument("--dry-run", action="store_true", help="санау, бірақ жазбау")
parser.add_argument("--verbose", action="store_true", help="егжей-тегжейлі журнал")
# Аргументтер тізімі анық берілген: сонда сабақ бәрінде бірдей нәрсе басады.
args = parser.parse_args(["--day", "2026-01-15", "--verbose"])
print("күн:", args.day, "| жазбай:", args.dry_run, "| егжей-тегжейлі:", args.verbose)
print("әдепкілері:", parser.parse_args([]).day, parser.parse_args([]).dry_run)

print()
print("== print орнына журнал")
logging.basicConfig(
    filename=LOG, filemode="w", encoding="utf-8",
    level=logging.DEBUG if args.verbose else logging.INFO,
    format="%(asctime)s | %(levelname)s | %(message)s",
)
log = logging.getLogger("svodka")
log.debug("егжей-тегжей: %s талдап жатырмын", args.day)
log.info("%s күні: 3 мән алынды", args.day)
log.warning("2026-01-14 бағамы табылмады, аттап өтемін")
try:
    1 / 0
except ZeroDivisionError:
    log.exception("орташаны есептегенде нөлге бөлу")

# Журналдағы уақыт әр іске қосуда өзгеше, сондықтан сабақ одан кейінгісін
# басады, ал log.exception қосқан стек жолдарын мүлде баспайды.
for line in LOG.read_text(encoding="utf-8").splitlines():
    if " | " in line:
        print(line.split(" | ", 1)[1])
print("ал одан кейін файлда қатенің стегі жатыр — ол әркімде өзгеше")

print()
print("== екеудің орнына бір дана")


def start():
    """Құлыпты алады немесе жұмыс істеуден бас тартады."""
    try:
        LOCK.touch(exist_ok=False)
        return True
    except FileExistsError:
        return False


print("бірінші іске қосу құлыпты алды:", start())
print("құлып бос емес кездегі екінші іске қосу:", start())
LOCK.unlink()
print("босатқаннан кейін:", start())
LOCK.unlink()

print()
print("== кестеге қайтару коды маңызды")
worker = HERE / "rabota.py"
worker.write_text(
    "import sys\n"
    "sys.stderr.write('дереккөз жауап бермеді\\n')\n"
    "raise SystemExit(2)\n", encoding="utf-8")
done = subprocess.run([sys.executable, worker], capture_output=True, text=True)
print("қайтару коды:", done.returncode, "| stderr-де:", done.stderr.strip())
print("нөл «бәрі жақсы» дегенді білдіреді:", subprocess.run([sys.executable, "-c", "pass"]).returncode)

worker.unlink()
LOG.unlink()

Шығатыны:

== кодты түзетудің орнына аргументтер
күн: 2026-01-15 | жазбай: False | егжей-тегжейлі: True
әдепкілері: бүгін False

== print орнына журнал
DEBUG | егжей-тегжей: 2026-01-15 талдап жатырмын
INFO | 2026-01-15 күні: 3 мән алынды
WARNING | 2026-01-14 бағамы табылмады, аттап өтемін
ERROR | орташаны есептегенде нөлге бөлу
ал одан кейін файлда қатенің стегі жатыр — ол әркімде өзгеше

== екеудің орнына бір дана
бірінші іске қосу құлыпты алды: True
құлып бос емес кездегі екінші іске қосу: False
босатқаннан кейін: True

== кестеге қайтару коды маңызды
қайтару коды: 2 | stderr-де: дереккөз жауап бермеді
нөл «бәрі жақсы» дегенді білдіреді: 0

Талдау

Кодты түзетудің орнына аргументтер

parser.add_argument("--day", default="бүгін", help="күн ЖЖЖЖ-АА-КК түрінде")
args = parser.parse_args(["--day", "2026-01-15", "--verbose"])

argparse бағдарламаны құралға айналдырады: күнді, режимді және егжей-тегжейлілікті іске қосу кезінде таңдайды, файлды өңдеп емес. Жоспарлағыш үшін бұл ыңғайлылық емес, қажеттілік — cron жолында кодты түзете алмайсыз.

Ол тегін беретін үш нәрсе: барлық сипаттамасы бар --help, белгісіз аргументке түсінікті мәтінмен қате және бір жерде көрініп тұратын әдепкілер. action="store_true" — бұл жалауша: ол не бар, не жоқ.

Әдетте parser.parse_args() деп жазады — аргументсіз, сонда нағыз командалық жол талданады. Сабақта тізім анық берілген, шығу бәрінде бірдей болсын деп; сіздің бағдарламаңызда жақша бос болады.

Елестетіп көріңіз. Көлік кілті. Аргументтері бар бағдарлама — оталдыруға болатын көлік; ішінде саны жазылған бағдарлама — рөлі бір күйге дәнекерленген көлік.

print орнына журнал

DEBUG | егжей-тегжей: 2026-01-15 талдап жатырмын
INFO | 2026-01-15 күні: 3 мән алынды
WARNING | 2026-01-14 бағамы табылмады, аттап өтемін
ERROR | орташаны есептегенде нөлге бөлу

print еш жерге жазады: түнде терминалға ешкім қарамайды, ал cron ең жақсы жағдайда шығуды хатпен жібереді. logging файлға жазады, әрі әр жолдың деңгейі мен уақыты болады.

Деңгейлер — әшекей емес. DEBUG-ты бірдеңені тексергенде қосады; INFO — істің әдеттегі барысы; WARNING — «әрі қарай жұмыс істеп жатырмыз, бірақ байқаңыз»; ERROR — «болмады». Деңгейді бір рет белгілейді, әрі бірінші блоктағы --verbose дәл соны істейді: INFO-ның орнына level=DEBUG.

except ішіндегі log.exception(...) файлға қатенің стегін қосып жазады — оныншы сабақта оқыған сол стек. Журнал жүргізудің басты себебі осы: таңертең сізде «бағдарлама құлады» емес, оның қай жолда құлағаны тұрады.

Әрі кішкентай, бірақ маңызды нәрсе: log.info("%s күні: %d алынды", day, n) — f-жолмен емес, пайызбен. Сонда мәтін тек осы деңгей қосулы болса ғана жиналады.

Құлып: екеудің орнына бір дана

бірінші іске қосу құлыпты алды: True
құлып бос емес кездегі екінші іске қосу: False

Түнгі іске қосу созылып кетті, келесісінің уақыты жетті — енді олар екеу. Екеуі де сол дереккөзді оқиды, екеуі де сол кестеге жазады; ең жақсы жағдайда қос трафикпен төлейсіз, ең жаманында біреуінен жарты, екіншісінен жарты дерек аласыз.

LOCK.touch(exist_ok=False) файл жасайды да, ол бұрыннан бар болса құлайды. Құлып деген осы: «бос емес пе» деген тексеру мен алудың бөлінбейтін бірлігі. if LOCK.exists() деп бөлек тексеруге болмайды — тексеру мен жасаудың арасына екінші іске қосу үлгеріп кіреді.

Құлыпты міндетті түрде finally ішінде шешеді: құлағаннан кейін құлыпты қалдырған бағдарлама енді ешқашан іске қосылмайды, әрі оны қолмен жөндеуге тура келеді.

Қайтару коды: кестенің оқитын жалғыз нәрсесі

қайтару коды: 2 | stderr-де: дереккөз жауап бермеді
нөл «бәрі жақсы» дегенді білдіреді: 0

Жоспарлағыш сіздің журналыңызды оқымайды. Ол бағдарлама қайтарған санға қарайды: 0 — болды, кез келген басқасы — жоқ.

Ол санмен әрі қарай не болатыны жоспарлағышқа байланысты, әрі айырмасы елеулі. systemd статусты шынымен талдайды: қызметте Restart=on-failure бар, ал сәтсіздік systemctl status-та көрініп тұрады. Кәдімгі cron өздігінен ештеңені қайта іске қоспайды: ол бағдарламаның басып шығарғанын хатпен жібере алады (MAILTO), оның қатысуы сонымен бітеді. Қайта іске қосу, мессенджерге хабарлама және мониторинг — бөлек бапталатын нәрсе; қайтару коды оларға тек себеп береді.

Сондықтан түнгі бағдарламаның келісімі бар: жұмыс істелмесе — raise SystemExit(1), істелсе — нөл. Қатені бұл кезде stderr-ге басып шығарған дұрыс: cron бір ағынды екіншісінен бөледі.

Жоспарлағыштың өзі

Бір жол. Linux пен macOS-та — crontab -e:

# минут сағат күн ай апта-күні  пәрмен
30 7 * * *  /home/user/digest/.venv/bin/python /home/user/digest/main.py --day today >> /home/user/digest/cron.log 2>&1

Бәрі сүрінетін үш ереже:

  • Жолдар тек толық. cron-да сіздің PATH-ыңыз да, ағымдағы қалтаңыз да жоқ — бағдарламадағы Path(__file__).resolve().parent те, ортадағы Python-ға толық жол да содан.
  • Уақыт белдеуі — жүйенікі, сіздікі емес. UTC-дегі серверде 30 7 — бұл Алматыдағы 12:30.
  • Шығуды бағыттайды. >> ... 2>&1 болмаса, cron оны поштамен жіберуге тырысады, ал ол әдетте серверде жоқ.

macOS-та сол нәрсені launchd істейді (~/Library/LaunchAgents ішіндегі .plist файлы), қазіргі Linux-та — systemd таймері (.service пен .timer қосағы), Windows-та — «Тапсырмалар жоспарлағышы». Олардың айырмасы синтаксисте; осы сабақта айтылғанның бәрі кез келгенінде бірдей.

Сабақ картасы

Сабақ картасы: аргументтер, журнал және құлып

Өз сөзіңізбен айтыңыз

Қарамай, дауыстап немесе қағазға жауап беріңіз. Жауаптары — сабақтың соңында.

  1. Күнді неге кодтағы жолмен емес, аргументпен таңдайды?
  2. Түнде жұмыс істейтін бағдарлама үшін logging print-тен немен өзгеше?
  3. Құлып не үшін керек, әрі if LOCK.exists() оны неге алмастырмайды?

Жаттығу

Тапсырмаға дейінгі үш қысқа қадам: болжау, толықтыру, жөндеу. Жауаптары — сабақтың соңында, бірақ алдымен өзіңіз жауап беріңіз.

1. Болжаңыз. Бұл бағдарлама не басып шығарады?

import logging
import sys

logging.basicConfig(stream=sys.stdout, level=logging.INFO,
                    format="%(levelname)s: %(message)s")
log = logging.getLogger("demo")
log.debug("егжей-тегжей")
log.info("істелді")
log.warning("бірдеңе дұрыс емес")

2. Бос орынды толтырыңыз. ... орнына бос командалық жолда басылатын әдепкіні жазыңыз.

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--day", ...)
print(parser.parse_args([]).day)

3. Жөндеңіз. Жоспарлағыш бағдарламаны өз қалтасынан іске қосады, әрі оның қасындағы файл табылмай қалады. Жолды бұл маңызды болмайтындай етіңіз.

# бағдарлама файлды өз қасына қояды да, бөтен қалтаға кетеді — жоспарлағыш оны
# дәл солай іске қосады
import os
from pathlib import Path

(Path(__file__).resolve().parent / "dannye.csv").write_text("2026;8.0\n", encoding="utf-8")
os.chdir("/")                      # жоспарлағыш өз қалтасынан іске қосады
print("файл табылды:", Path("dannye.csv").exists())

Тапсырма

Міндетті. Шолудың түнгі іске қосылуын жазыңыз. Керегі: --day (әдепкісімен) және --dry-run аргументтері; деңгейлері мен уақыты бар файлдағы журнал; құлыпты алатын, не істеп жатқанын журналға жазатын, құлыпты finally ішінде шешетін және сәтті болса 0, құлып бос емес болса 1 қайтаратын run(day, dry) функциясы.

Оны үш рет шақырыңыз: кәдімгі іске қосу, құлып бос емес кездегі іске қосу (құлыпты қолмен жасаңыз) және --dry-run арқылы іске қосу. Әрқайсысының қайтару кодын, ал соңында уақытсыз журналды басып шығарыңыз.

Күтілетін шығу:

бірінші іске қосу, коды: 0
құлып бос емес кездегі іске қосу, коды: 1
жазбай іске қосу, коды: 0
журнал:
  INFO | 2026-01-16 күні: бастадық
  INFO | 2026-01-16 күні: 2 жол жазылды
  WARNING | 2026-01-16 күні: басқа іске қосу жүріп жатыр, шығамыз
  INFO | 2026-01-16 күні: бастадық
  INFO | 2026-01-16 күні: жазбай санаймыз

Дайын болғаны: шығу жол-жолмен сәйкес келеді; құлып сәтті тармақтан кейін емес, finally ішінде шешіледі; бос емес құлып құлауды емес, WARNING пен 1 кодын береді; барлық жол Path(__file__).resolve().parent-тен құралған.

Өз деректеріңізде. Өз бағдарламаңызды өз машинаңыздағы кестеге — cron, launchd немесе «Тапсырмалар жоспарлағышы» — жақын бес минутқа қойыңыз. Іске қосылуын күтіп, журналды оқыңыз. Python-ға жолды дұрыс жазғаныңызды білудің жалғыз тәсілі осы.

Қалауыңызша.

  • --log-level қосып, DEBUG-ты командалық жолдан қосуды үйреніңіз.
  • Бағдарламаны әдейі құлатып, log.exception-нан журналға не түскенін қараңыз.
  • Екі данасын қатар іске қосып (python main.py & python main.py), екіншісінің 1 кодымен кеткеніне көз жеткізіңіз.

Жобада бұл қайда тұрады

Шолу қызметке айналады. Күніне бір рет ол бағамды өзі алады, дерекқорға салады, не істегенін журналға жазады және нөл қайтарады — ал сіз ол туралы нөл қайтармай қалғанда ғана білесіз.

Қарыздар. Құлпымыз файлдық, әрі онымен болатын бәле көрінгеніне қарама-қарсы: ол апаттық аяқталудан аман қалады. Бағдарламаны өлтірді, қуат өшті — файл орнында қалады да, келесі іске қосу құлып бос емес деп көреді, ал оны ұстап тұрған ешкім жоқ. Таңертең оны қолмен шешеді. Мұны құлыпқа процесс нөмірі мен құрылған уақытын жазып қою арқылы емдейді: ондай нөмірлі процесс болмаса, құлып ескірген, оны өзі шешуге болады. Әрі журнал шексіз өседі — ротацияға серверге жеткен жерде жетеміз.

Жауаптар

Жауаптарды көрсету

Сұрақтарға

  1. Өйткені кодтағы жолды кестеден өзгерте алмайсыз: cron команданы іске қоса алады, файлды өңдей алмайды. Аргумент бағдарламаны режимдері бар құралға айналдырады, ал --help — сол режимдер сипатталған орынға.
  2. print түнде ешкім жоқ жерге жазады. logging файлға жазады, маңыздылығын деңгеймен, сәтін уақытпен белгілейді, ал log.exception қасына қатенің стегін қояды. Таңертең бұл «құлады» мен «дәл мына жерде құлады» дегеннің айырмасы.

Бағдарламадан жұмысқа көшпейтін бір ұсақ нәрсе бар: filemode="w" әр іске қосуда журналды өшіреді, әрі ол мұнда сабақтың шығысы бәрінде бірдей болсын деп қана тұр. Нағыз бағдарламада режим — "a", яғни қосып жазу (әдепкісі де сол), ал файл шексіз өспеуі үшін RotatingFileHandler — өлшемі бойынша — немесе TimedRotatingFileHandler — күні бойынша — алынады.
3. Құлып екі іске қосудың бір жұмысты екі рет істеуіне жол бермейді. if LOCK.exists() оны алмастырмайды, өйткені тексеру мен файл жасаудың арасына екінші іске қосу үлгеріп кіреді: бөлінбейтін бір амал керек, ал touch(exist_ok=False) — сол.

Жаттығуға

  1. Екі жол: INFO: істелді және WARNING: бірдеңе дұрыс емес. DEBUG белгіленген деңгейден төмен, сондықтан көрінбейді — деңгей дегеніміз егжей-тегжейдің ажыратқышы.
INFO: істелді
WARNING: бірдеңе дұрыс емес
  1. default="бүгін". Әдепкі — «бермесе ше?» деген сұрақтың жауабы, әрі ол аргумент жарияланған жерде тұруға тиіс.
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--day", default="бүгін")
print(parser.parse_args([]).day)
бүгін
  1. Path("dannye.csv") жолы ағымдағы қалтаға қатысты, ал оны жоспарлағыш белгілейді. Бағдарламаның қалтасынан бастап құрады — сонда оны қайдан шақырғанының маңызы болмайды:
import os
from pathlib import Path

HERE = Path(__file__).resolve().parent
(HERE / "dannye.csv").write_text("2026;8.0\n", encoding="utf-8")
os.chdir("/")                      # жоспарлағыш өз қалтасынан іске қосады
print("файл табылды:", (HERE / "dannye.csv").exists())
(HERE / "dannye.csv").unlink()
файл табылды: True

Дереккөздер

Мәтінде қате не теру қатесі кездессе, бізге айтыңыз

Тапсырманы тексеру

Алдымен VS Code-та шешіп, іске қосыңыз — редактор қатені сол жерде көрсетеді. Дайын шешімді осында қойыңыз. Тексеретін — модель: ол қатені атап көрсетеді, бірақ дайын жауапты бермейді.

Тексеру үшін кіру керек. Кіру

Пікірлер (0)

Әзірге пікір жоқ. Бірінші болыңыз.