HyppÀÀ sisÀltöön

đŸ‘©â€đŸ”Ź Curium

TĂ€rpit

VĂ€liaikaiset tiedostot

SkriptejÀ kirjoittaessa sinulla voi hyvinkin olla tarve esimerkiksi ladata suuri zip tai tar.gz paketti verkosta ja purkaa se. NÀitÀ ei luonnollisesti kannata sÀilöÀ pitkÀaikaisesti. Pythonin tempfile-moduuli tarjoaa vÀliaikaisten tiedostojen luomisen ja hallinnan. Se soveltuu useisiin kÀyttötapauksiin, mutta tÀssÀ esittelemme kaksi.

KÀyttötapaus 1 on, ettÀ haluat kÀyttÀÀ internetistÀ löytyvÀÀ tiedostoa, mutta et halua ladata sitÀ joka kerta uusiksi. Voit kÀyttÀÀ tempfile.gettempdir()-funktiota saadaksesi jÀrjestelmÀn vÀliaikaisten tiedostojen hakemiston ja luoda tiedoston sinne. TÀmÀ on hyödyllistÀ esimerkiksi, jos haluat ladata tiedoston vain kerran ja kÀyttÀÀ sitÀ sen jÀlkeen paikallisesti. Huomaa, ettÀ kÀyttöjÀrjestelmÀ voi poistaa tmp-tiedostoja mielivaltaisesti esimerkiksi kÀynnistyksen yhteydessÀ tai levytilan kÀydessÀ vÀhiin. Lue toiminnallisuus koodista:

cache_internet_file.py
import tempfile
import requests

from pathlib import Path

URI = "https://www.example.com/index.html"

def cache_internet_file(uri: str) -> Path:
    temp_dir = tempfile.gettempdir() # (1)!
    temp_file_path = Path(temp_dir) / "etusivu.html" # (2)!

    if not temp_file_path.exists():
        print("[INFO] Downloading data from the internet...")
        response = requests.get(URI)
        with open(temp_file_path, 'wb') as temp_file:
            temp_file.write(response.content)

    return temp_file_path

local_file_path = cache_internet_file(URI)
print(f"[INFO] Downloaded data is available at: {local_file_path}")
  1. tempfile.gettempdir() palauttaa jÀrjestelmÀn vÀliaikaisten tiedostojen hakemiston. Windowsissa tÀmÀ voi olla esimerkiksi C:\Users\user\AppData\Local\Temp, Linuxissa /tmp ja macOS:ssÀ /var/folders/.../T/.
  2. Yksinkertaisuuden vuoksi tiedostonimi on kovakoodattu. Kenties haluaisit poimia tiedostonimen URI:sta urlib.parse.urlparse()-funktiolla? Tai antaa tiedostonimen argumenttina funktiolle? Tai kÀyttÀÀ sanitoitua urlia tiedostonimenÀ?

KÀyttötapaus 2 on hetkellisen hakemiston tai tiedoston luominen, jota tarvitaan vain ja ainoastaan skriptin ajon verran. TÀmÀ on hyödyllistÀ, jos skriptisi ajon sivutuotteena syntyy tiedosto, jota ei tarvita jatkossa. LöydÀt lisÀÀ esimerkkejÀ tempfile-moduulin dokumentaatiosta. Alla hyvin yksinkertainen esimerkki.

use_temporary_file.py
import tempfile

from pathlib import Path

with tempfile.NamedTemporaryFile(delete_on_close=True) as temp_file: # (1)!
    temp_file.write(b"Hello world!")
    temp_file.flush()
    temp_path = Path(temp_file.name)

if temp_path.exists():
    print(f"⛔ Oh no! Temporary file is still available at: {temp_path}")
else:
    print(f"✅ Temporary file was deleted! (rip {temp_path})")
  1. TÀssÀ kÀytetÀÀn context manageria eli with-lauseketta. Context manager kutsuu luokan __enter__- ja __exit__-metodeja automaattisesti, joten et tarvitse temp_file.close()-kutsua.

Kirjastot kontissa

Jos ajat yllÀ olevan cache_internet_file.py-esimerkin kontissa, saat alla nÀkyvÀn virheen. TÀmÀ johtuu siitÀ, ettÀ python:3.12-image sisÀltÀÀ vain ja ainoastaan Pythonin sisÀÀnrakennetut moduulit. Sen sijaan Ubuntussa requests-moduuli on asennettu jÀrjestelmÀtasolla - tÀhÀn dist-packages-hakemistoon olet tutustunut jo aiemmin.

đŸ–„ïž Bash
$ python runpy.py scripts/cache_internet_file.py 
Traceback (most recent call last):
  File "/app/scripts/cache_internet_file.py", line 2, in <module>
    import requests
ModuleNotFoundError: No module named 'requests'

Voimme toki ajaa pip install requests-komennon kontin sisÀllÀ, mutta tÀmÀ pitÀisi ajaa joka kerta uudestaan, sillÀ kontit ovat ephemeral eli niiden tila ei pysy tallennettuna. Jos haluat pysyviÀ muutoksia, sinun tulee luoda oma Dockerfile ja sen pohjalta oma image. TehdÀÀn siis nÀin!

Luo tiedostot build-skroh-python.py ja skroh-python.Dockerfile haluamaasi lokaatioon. TÀssÀ esimerkissÀ ne on luotu projektikansion python/-alihakemistoon.

build-skroh-python.py
#!/usr/bin/env python3
import subprocess

def build_docker_image():
    command = [
        "docker", "buildx", "build",
        "-t", "skroh-python:3.12",
        "-f", "skroh-python.Dockerfile",
        "."
    ]
    subprocess.run(command)

if __name__ == "__main__":
    build_docker_image()
skroh-python.Dockerfile
FROM python:3.12

RUN pip install requests

Nyt voit ajaa build-skroh-python.py-skriptin, joka luo uuden Docker-imagen. Skripti on Python-skripti, joten sen pitÀisi toimia eri kÀyttöjÀrjestelmissÀ. Seuraa alla olevia komentoja ajatuksella. Komentojen tulostetta on lyhennetty luettavuuden parantamiseksi.

đŸ–„ïž Bash | CMD | PowerShell
$ python build-skroh-python.py
=> [internal] load build definition from skroh-python.Dockerfile
=> [1/2] FROM docker.io/library/python:3.12
=> [2/2] RUN pip install requests
=> => naming to docker.io/library/skroh-python:3.12

$ docker image ls
➜  python git:(main) ✗ docker image ls            
REPOSITORY                                TAG                   IMAGE ID       CREATED          SIZE
skroh-python                              3.12                  ad99a641fce3   28 minutes ago   1.03GB
python                                    3.12                  ba0500f08e94   12 days ago      1.02GB

$ python runpy.py --image skroh-python:3.12 scripts/cache_internet_file.py 
[INFO] Downloading data from the internet...
[INFO] Downloaded data is available at: /tmp/etusivu.html

Huomaa, ettÀ jos ajat alimman komennon uusiksi, tiedosto ladataan uudestaan. TÀmÀ johtuu siitÀ, ettÀ kontti on ephemeral ja sen tila ei pysy tallennettuna. Jos haluat hyötyÀ tÀmÀn sortin cachetuksesta, nopein tapa on kÀyttÀÀ Bash-istuntoa kontissa ja ajaa skripti useita kertoja saman istunnon aikana. Muista, ettÀ scripts/-hakemisto on mountattu kontin sisÀlle, joten voit muokata tiedostoja suoraan hostilla ja muutokset nÀkyvÀt kontissa vÀlittömÀsti.

đŸ–„ïž Bash | CMD | PowerShell
$ python runpy.py --image skroh-python:3.12 --bash

🐳 # python scripts/cache_internet_file.py 
[INFO] Downloading data from the internet...
[INFO] Downloaded data is available at: /tmp/etusivu.html

🐳 # python scripts/cache_internet_file.py 
[INFO] Downloaded data is available at: /tmp/etusivu.html

TehtÀvÀt

TehtÀvÀ: Pingviinien laskeminen

TÀmÀ on sinulle PowerShell-osiosta tuttua dataa. Lue skripti, joka lukee tiedoston penguins.csv ja laskee pingviinit lajeittain. Tulosta lajit ja niiden lukumÀÀrÀt.

Huomaa, ettĂ€ meidĂ€n kĂ€ytössĂ€ on jĂ€rjestelmĂ€tason Python ja siten vain Pythonin sisÀÀnrakennetut (ja ehkĂ€ debianin) kirjastot. ÄlĂ€ asenna datankĂ€sitelykirjastoja. KĂ€ytĂ€ sisÀÀnrakennettua csv-moduulia.

import csv

# Steps:
# 1. Read the file if exists
# 2. Download the file otherwise
# 3. Count the penguins
# 4. Print the results

Lopulta sen pitÀisi kÀyttÀytyÀ nÀin:

đŸ–„ïž Bash
$ python runpy.py --image skroh-python:3.12 scripts/penguins.py
[INFO] Downloading data from the internet...
Downloaded data is available at: /tmp/penguins.csv
Read 344 rows from the CSV file.
Adelie: 152
Chinstrap: 68
Gentoo: 124
CSV
import csv

# ...
csv_reader = csv.DictReader(csv_file)
list(csv_reader)
Counting

Lajien laskemisen voi tehdÀ usealla eri tavalla. Voit esimerkiksi kÀyttÀÀ collections.Counter-luokkaa, saman kirjaston defaultdictiÀ, sqliteÀ, tai ihan tavallista dictionaryÀ. Counterilla se hoituu nÀin:

from collections import Counter

# ...
c = Counter(entry["species"] for entry in penguin_dict)
dict(c)
TehtÀvÀ: Suurimmat ohjelmat

Luo skripti, joka tulostaa n kappaletta suurimpia binÀÀritiedostoja /usr/bin-hakemistossa. Vakio n = 5, mutta kÀyttÀjÀ voi syöttÀÀ sen. Voit lÀhestyÀ ongelmaa kahdella tavalla:

  • subprocess.run(["du", "-a", "/usr/bin"])
  • Path("/usr/bin/some_executable").stat()

JÀlkimmÀinen tapa sÀÀstÀÀ sinut du-komennon tuloksen parsimiselta ja on muutenkin more pythonic. Se palauttaa itemin, joka sisÀltÀÀ muun muassa seuraavat tiedot:

os.stat_result(
    st_mode=33261,    # oct(33261) == 0o100755 eli 755 eli rwxr-xr-x
    st_uid=0,         # user id
    st_gid=0,         # group id
    st_size=1346480,  # size in bytes
    ...
)

Huomaa, ettÀ arvot st_uid ja st_gid tulee muuttaa kÀyttÀjÀnimeksi ja ryhmÀnimeksi. TÀmÀ onnistuu helposti pwd ja grp-moduuleilla. EntÀ kuinka muuttaisit st_size-arvon ihmisluettavaan muotoon? Kenties StackOverFlow-palvelussa tÀtÀ on pohtinut joku muukin?

LisÀhaaste

Voit lisÀtÀ tehtÀvÀn haastavuutta seuraamalla symbolisia linkkejÀ. Jos teet tÀmÀn, tulet saamaan kohtalaisen mÀÀrÀn duplikaatteja. Keksi tapa poistaa duplikaatit listasta.

LisÀhaaste 2

LisÀhaaste on hyödyntÀÀ st_modea. Voit parsia siitÀ muun muassa moden numeroina (esim. 755), kÀÀntÀÀ sen merkkijonoksi (esim. rwxrxrx)

Esimerkki kÀytöstÀ alla (lisÀhaasteet mukana):

đŸ–„ïž Bash
$ ./runpy.py scripts/largest_binaries.py -n 8 
/usr/bin/x86_64-linux-gnu-lto-dump-12      30.5 MiB    root:root rwxr-xr-x
/usr/bin/sq                                 9.6 MiB    root:root rwxr-xr-x
/usr/bin/python3.11                         6.5 MiB    root:root rwxr-xr-x
/usr/bin/perl5.36.0                         3.6 MiB    root:root rwxr-xr-x
/usr/bin/git                                3.5 MiB    root:root rwxr-xr-x
/usr/bin/x86_64-linux-gnu-ld.gold           3.0 MiB    root:root rwxr-xr-x
/usr/bin/scalar                             2.1 MiB    root:root rwxr-xr-x
/usr/bin/git-shell                          2.0 MiB    root:root rwxr-xr-x

TĂ€mĂ€ harjoitus ei juuri saavuta mitÀÀn, mitĂ€ ls-komento ei tee, mutta se on hyvĂ€ harjoitus tiedostojen kĂ€sittelyyn ja tiedostojen metatietojen lukemiseen. Voisit kĂ€yttÀÀ nĂ€itĂ€ taitoja esimerkiksi tiedostojen analysointiin, jĂ€rjestĂ€miseen, tai vaikkapa tiedostojen poistamiseen – kenties sisĂ€llyttĂ€en merkittĂ€vĂ€sti enemmĂ€n logiikkaa.

TehtÀvÀ: Duplikaattitiedostojen luominen

TÀmÀ tehtÀvÀ toimii esiasteena seuraavalle tehtÀvÀlle. Luo skripti, joka kirjoittaa tiedostoihin sisÀltöÀ siten, ettÀ osa tiedostoista on tarkoituksella toistensa kopioita. Osa tiedostoista tulee sen sijaan olla uniikkeja. Voit kÀyttÀÀ apuna seuraavanlaista jakoa:

add_duplicates.py
duplicate_files = [
    tmpdir / "foo.txt",
    tmpdir / "foo_copy.txt",
    tmpdir / "nested" / "foo_copy_nested.txt",
]

unique_files = [
    tmpdir / "unicorn_a.txt",
    tmpdir / "nested" / "unicorn_b.txt",
]

Lopulta ohjelmaa tulisi voida kÀyttÀÀ seuraavanlaisesti:

🐳 Bash
$ python scripts/add_duplicates.py
Temporary directory created at: /tmp/tmpinhu9_m1
Created file: /tmp/tmpinhu9_m1/foo.txt
Created file: /tmp/tmpinhu9_m1/foo_copy.txt
Created file: /tmp/tmpinhu9_m1/nested/foo_copy_nested.txt
Created file: /tmp/tmpinhu9_m1/unicorn_a.txt
Created file: /tmp/tmpinhu9_m1/nested/unicorn_b.txt
Navigate to /tmp/tmpinhu9_m1 to see the files! 👀

Tip

Rautakoodauksen sijasta voit kÀyttÀÀ tempfile.gettempdir(), jotta sama skripti toimisi eri alustoilla.

TehtÀvÀ: Duplikaattien tunnistaminen

Luo skripti, joka tunnistaa duplikaatit annetussa hakemistossa. MikÀli -recurse flag on annettu, sen tulisi kÀydÀ myös alihakemistot lÀpi. Duplikaatit tulisi tunnistaa tiedoston MD5-hashin perusteella. Voit kÀyttÀÀ samoja vaiheita kuin aiemmin PowerShellin kanssa, mutta puuttuvat cmdletit saattavat aiheuttaa pÀÀnvaivaa.

PÀÀnvaiva 1: Tulet mahdollisesti huomaamaan, ettÀ Group-Object ja Where-Object Count -komentojen puute tekee tehtÀvÀstÀ hieman vaikeamman Pythonissa kuin PowerShellissÀ, jossa olet toteuttanut vastaavan operaation aiemmin. Datan kÀsittelyyn tarkoitetut kirjastot, kuten Pandas, tarjoavat nÀitÀ ominaisuuksia, mutta se lisÀisi meidÀn skriptille ylimÀÀrÀisiÀ riippuvuuksia. Ratkaise tÀmÀ ongelma Pythonin sisÀÀnrakennetuilla kirjastoilla. Kenties collections.defaultdict tai collections.Counter voisi olla hyödyllinen?

PÀÀnvaiva 2: Sinun saattaa tulla ikÀvÀ myös Get-FileHash -cmdletia, joka laskee tiedoston hashin. Pythonissa voit kÀyttÀÀ hashlib-moduulia. Voit chunkata tiedoston ja laskea hashin osissa, kuten StackOverFlow:n postauksissa neuvotaan, jotta suurten tiedostojen kÀsittely onnistuu. Vaihtoehtona on kÀyttÀÀ valmisratkaisua: hashlib.file_digest(f, algorithm). Kyseinen funktio on Python 3.11:ssÀ lisÀtty.

Alla nÀkyy esimerkkitoteutuksen kÀyttö:

🐳 Bash
$ python scripts/find_duplicates.py /tmp/tmpinhu9_m1/
🚹 WARNING: Duplicate files found:

Full path                      MD5 checksum
---------------------------------------------------------------
/tmp/tmpinhu9_m1/foo.txt       746308829575e17c3331bbcb00c0898b
/tmp/tmpinhu9_m1/foo_copy.txt  746308829575e17c3331bbcb00c0898b

$ python scripts/find_duplicates.py /tmp/tmpinhu9_m1/ --recurse
🚹 WARNING: Duplicate files found:

Full path                                    MD5 checksum
-----------------------------------------------------------------------------
/tmp/tmpinhu9_m1/foo.txt                     746308829575e17c3331bbcb00c0898b
/tmp/tmpinhu9_m1/foo_copy.txt                746308829575e17c3331bbcb00c0898b
/tmp/tmpinhu9_m1/nested/foo_copy_nested.txt  746308829575e17c3331bbcb00c0898b
TehtÀvÀ: Tulosta PATH-muuttujan hakemistot

Skriptiohjelmoinnin tÀrkeÀ osa on ympÀristömuuttujien kÀsittely. Yksi tÀrkeimmistÀ ympÀristömuuttujista on PATH, joka sisÀltÀÀ hakemistot, joista kÀyttöjÀrjestelmÀ etsii suoritettavia tiedostoja. Toteuta skripti, joka tulostaa PATH-muuttujan hakemistot riveittÀin. Voit kÀyttÀÀ os.environ["PATH"]-muuttujaa, joka palauttaa PATH-muuttujan arvon.

TÀmÀ on Curium-osion helppo loppukevennys! Tulosteen tulisi nÀyttÀÀ jotakuinkin tÀltÀ:

🐳 Bash
python scripts/print_env.py 
/usr/local/bin
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin