Как ускорить очистку больших объемов данных с помощью Python

У меня такой код:

items = set()  # use a set to not have duplicate items
a = requests.get(f"{BASE_URL}?{BASE_QUERIES}&cursor=*")

amount = a.json()["totalResults"]  # in the range of 30 million

items.update(item["guid"].split("?")[0] for item in a.json()["items"])  # we only want the url before the query strings

cursor = a.json()["nextCursor"]  # we have a cursor as each request only returns 100 items, the cursor shows use where to start

while len(items) < amount:  # ensure we get all the items
    try:
        a = requests.get(f"{BASE_URL}?{BASE_QUERIES}&cursor={cursor}")
        items.update(item["guid"].split("?")[0] for item in a.json()["items"])
    except:
        continue
    try:
        cursor = urllib.parse.quote(a.json()["nextCursor"])
    except KeyError:
        if len(items) == amount:  # when we reach the final iteration the cursor will not be there
            break

headers = {
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36"
}

for link in items:  # iterate over each item
    for _ in range(3):
        response = requests.get(link, headers=headers, stream=True)
        response.raw.decode_content = True
        parser = etree.HTMLParser()
        tree = etree.parse(response.raw, parser)

        setting = tree.xpath(
            "/html/body/div[1]/div/div/main/div/div[2]/div/div[2]/div/div/div[1]/a/span[1]/text()"
        )[0].strip()
        try:
            image = tree.xpath(
                "/html/body/div[1]/div/div/main/div/div[2]/div/div[1]/div[1]/div/div/a/@href"
            )[0]  # most of the time this works
        except:
            try:
                image = tree.xpath(
                    "/html/body/div[1]/div/div/main/div/div[2]/div/div[1]/div[1]/div[1]/a/@href"
                )[0]  # about 1 in 10 times the above fails and this works
            except:
                print(f"Image not found for link: {link}")
                break

        title = tree.findtext(".//title")
        title_for_file = (
            fr"{os.getcwd()}IMAGESIMAGE - " + "".join(c for c in title if c in valid_chars) + ".jpeg"
        )  # sometimes the file contains characters which aren't allowed in file names

        description = "".join(
            tree.xpath(
                "/html/body/div[1]/div/div/main/div/div[3]/div[1]/div/div/div/div/p/text()"
            )
        )

        if setting not in [x, y]:  # we only want items which have a specific setting
            break

        try:
            image_req = requests.get(image)
            with open(title_for_file, "wb") as f:
                f.write(image_req.content)  # write the image to the directory

            img = pyexiv2.Image(title_for_file)
            metadata = {"Xmp.xmp.Title": title, "Xmp.xmp.Description": description}  # edit the metadata
            img.modify_xmp(metadata)
            img.close()
            break
        except Exception as e:
            print(
                f"ERROR parsing image: {title_for_file}, it is most likely corrupt, "
                f"retrying ({link}), "
                f"error: {e}"
            )
            os.remove(title_for_file)
    else:
        print("ERROR more than 3 times moving to next")

В коде есть довольно подробные комментарии, объясняющие, что он делает, моя проблема в том, что на выполнение всех запросов на получение ссылок в коде требуется около 7 часов. items, затем еще один (я не уверен на 100%, но, думаю, около 1500 часов), чтобы проверить каждое изображение и добавить метаданные. Есть что-нибудь в этом коде, который можно ускорить / улучшить, чтобы ускорить этот процесс?

Точно так же можно где-нибудь уменьшить использование памяти, если я подозреваемый это будет использовать много памяти с объемом данных, которые он анализирует.

РЕДАКТИРОВАТЬ

Возможным рассмотрением было бы использование Threading чтобы загрузить сразу несколько изображений, как найти оптимальное количество потоков для одновременного запуска? Или, возможно, я бы начал каждый Thread с небольшой задержкой, возможно 0,5 секунды?

1 ответ
1

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

def get_items(response):
    # we only want the url before the query strings
    # use a set to not have duplicate items
    return {item["guid"].split("?")[0] for item in response["items"]}

def get_all_items():
    url = f"{BASE_URL}?{BASE_QUERIES}"
    response = requests.get(url, params={"cursor": "*"}).json()
    amount = response["totalResults"]  # in the range of 30 million
    items = get_items(response)        
    while cursor := response.get("nextCursor"):
        try:
            response = requests.get(url, params={"cursor": cursor}).json()
            items.update(get_items(response))
        except Exception:
            continue
    if len(items) != amount:
        raise ValueError("Did not get all items")
    return items

Обратите внимание, что я изменил твою голую except чтобы except Exception, иначе пользователь нажмет Ctrl+C также будет проигнорирован.

Я также использовал новый оператор моржа, чтобы продолжить итерацию, пока nextCursor дается в ответе.

HEADERS = {
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36"
}

ALLOWED_SETTINGS = {x, y}

def handle_link(link, retries=3):
    for _ in range(retries):
        if parse(link, ALLOWED_SETTINGS):
            break

def parse(link, allowed_settings):
    response = requests.get(link, headers=HEADERS, stream=True)
    response.raw.decode_content = True
    parser = etree.HTMLParser()
    tree = etree.parse(response.raw, parser)
    setting = get_setting(tree)
    if setting not in allowed_settings:  # we only want items which have a specific setting
        return False
    image = get_image(tree)
    if image is None:
        return False
    title = ǵet_title(tree)
    file_name = get_file_name(title)
    description = get_description(tree)
    return save_image(image, file_name,
                      {"Xmp.xmp.Title": title, "Xmp.xmp.Description": description})

Обратите внимание, что я прервал как можно скорее, нет необходимости продолжать синтаксический анализ, если setting уже говорит вам, что вы выбросите результат.

def get_setting(tree):
    return tree.xpath(
        "/html/body/div[1]/div/div/main/div/div[2]/div/div[2]/div/div/div[1]/a/span[1]/text()"
    )[0].strip()

def get_image(tree):
    xpaths = ["/html/body/div[1]/div/div/main/div/div[2]/div/div[1]/div[1]/div/div/a/@href",
              "/html/body/div[1]/div/div/main/div/div[2]/div/div[1]/div[1]/div[1]/a/@href"]
    for xpath in xpaths:
        try:
            return tree.xpath(xpath)[0]
        except Exception:                
            continue
    print(f"Image not found for link: {link}")

def get_title(tree):
    return tree.findtext(".//title")

def get_file_name(title):
    return (
        fr"{os.getcwd()}IMAGESIMAGE - " + "".join(c for c in title if c in valid_chars) + ".jpeg"
    )  # sometimes the file contains characters which aren't allowed in file names

def get_description(tree):
    return "".join(
        tree.xpath(
            "/html/body/div[1]/div/div/main/div/div[3]/div[1]/div/div/div/div/p/text()"
        )
    )

def save_image(url, file_name, metadata):
    try:
        image_req = requests.get(url)
        with open(file_name, "wb") as f:
            f.write(image_req.content)  # write the image to the directory
        img = pyexiv2.Image(file_name)
        img.modify_xmp(metadata)
        img.close()
        return True
    except Exception as e:
        print(
            f"ERROR parsing image: {file_name}, it is most likely corrupt, "
            f"error: {e}"
        )
        os.remove(file_name)
        return False

Теперь вы можете начать думать о распараллеливании, поскольку на самом деле у вас есть инкапсулированная целевая функция для выполнения.

Поскольку теперь есть отдельные функции для получения некоторой информации, вы можете подумать об изменении способа анализа ссылок. Я лично предпочитаю использовать BeautifulSoup над xpath, поскольку использование селекторов CSS для классов или идентификаторов является IMO более читабельным, чем ваши длинные пути.

Обратите внимание, что в конце концов вы всегда будете ограничены доступным интернет-соединением. Если вам нужно загрузить 30 миллионов изображений размером 1 МБ при подключении к Интернету со скоростью 100 Мбит / с, он будет занять не менее 27 дней и если у вас соединение со скоростью 1 Гбит / с, это все равно займет почти три дня, независимо от того, как вы улучшите свой код. Если вы загружаете два изображения параллельно, каждое будет загружаться с половинной скоростью.

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *