खेल भाप के लिए खुरचनी

Aug 17 2020

मैंने स्टीम के लिए एक स्क्रैपर बनाया जो कि स्टीम गेम के बारे में अलग-अलग जानकारी प्राप्त करता है, जैसे कि कीमत, चश्मा और समर्थित प्लेटफॉर्म। इसका कारण मैंने इसे बनाया क्योंकि मेरे पास एक सुपर स्लो लैपटॉप है, इसलिए कई गेमों को देखने में लंबा समय लगता है :)

कुछ चीजें जिन्हें मैं सुधारना चाहता हूं उनमें बेहतर त्रुटि से निपटने की आवश्यकता है, क्योंकि वेब एक गन्दा स्थान है और सभी पृष्ठ समान नहीं होंगे।

एक और बात जो मैं करने के बारे में सोच रहा था, वह बेहतर डेटा प्रबंधन कर रहा है, जैसे कि एक शब्दकोश में सभी मूल्यों को संग्रहीत करने के बजाय प्रत्येक गेम के लिए कक्षाओं और वस्तुओं का उपयोग करना, जो सरल और शायद कम कोड के लिए भी होगा।

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.firefox.options import Options
from sys import exit

games = {}
x = 0

# ask for games
while True:
    if x == 0:
        game = input('Type the game you want to find here: ')
    else:
        game = input('Type the game you want to find here (or enter nothing to continue): ')
    if not game:
        break
    games[game] = {}
    x += 1

# configure browser
print('Starting Browser')
firefox_options = Options()
firefox_options.headless = True
browser = webdriver.Firefox(options=firefox_options, service_log_path='/tmp/geckodriver.log')
print('Retrieving website')
browser.get('https://store.steampowered.com/')

for a_game in games:
    print('Finding info for "' + a_game + '"')

    # input & click
    print('Waiting for page to load')
    WebDriverWait(browser, 20).until(EC.element_to_be_clickable((By.CSS_SELECTOR, "input#store_nav_search_term"))).send_keys(a_game)
    WebDriverWait(browser, 20).until(EC.element_to_be_clickable((By.CSS_SELECTOR, "div#search_suggestion_contents>a"))).click()
    print('Navigating to game page')

    # if age-restricted:
    try:
        browser.find_element_by_css_selector('.agegate_birthday_selector')
        age_query = input('"' + a_game + '" is age-restricted, do you want to continue? y/n ')
        if age_query != 'y':
            print('Abort')
            exit()
        select = Select(browser.find_element_by_id('ageYear'))
        select.select_by_value('2000')
        browser.find_element_by_css_selector('a.btnv6_blue_hoverfade:nth-child(1)').click()
    except NoSuchElementException:
        pass

    print('Waiting for game page to load')
    # name of game
    games[a_game]['name'] = WebDriverWait(browser, 20).until(EC.element_to_be_clickable((By.CSS_SELECTOR, '.apphub_AppName'))).text

    # supported platforms
    print('Retrieving supported platforms')
    mac = False
    linux = False
    try:
        browser.find_element_by_css_selector('div.game_area_purchase_game_wrapper:nth-child(1) > div:nth-child(1) > div:nth-child(2) > '
                                             'span:nth-child(2)')
        mac = True
    except NoSuchElementException:
        pass

    try:
        browser.find_element_by_css_selector('div.game_area_purchase_game_wrapper:nth-child(1) > div:nth-child(1) > div:nth-child(2) > '
                                             'span:nth-child(3)')
        linux = True
    except NoSuchElementException:
        pass

    if mac and linux:
        games[a_game]['platform'] = 'all'
    elif mac:
        games[a_game]['platform'] = 'mac'
    elif linux:
        games[a_game]['platform'] = 'linux'
    else:
        games[a_game]['platform'] = 'windows'

    # price
    print('Retrieving price')
    discounted = False
    try:
        games[a_game]['price'] = browser.find_element_by_css_selector('div.game_purchase_action:nth-child(4) > div:nth-child(1) > div:nth-child(1)').text
    except NoSuchElementException:
        try:
            games[a_game]['before_price'] = browser.find_element_by_class_name('discount_original_price').text
            games[a_game]['after_price'] = browser.find_element_by_class_name('discount_final_price').text
        except NoSuchElementException:
            try:
                games[a_game]['price'] = 'FREE'
            except NoSuchElementException:
                games[a_game]['bundle_price'] = browser.find_element_by_css_selector('div.game_purchase_action_bg:nth-child(2) > div:nth-child(1)')
    except Exception:
        games[a_game]['price'] = 'Error: Unable to get price'

    # system requirements
    print('Retrieving system requirements')
    games[a_game]['specs'] = browser.find_element_by_css_selector('.game_area_sys_req').text

# close browser
print('Finished Retrieving data, closing browser \n')
print('********************************************')
browser.close()

for each_game in games.keys():
    print('GAME: ' + games[each_game]['name'].upper())

    # printing supported platforms
    if games[each_game]['platform'] == 'all':
        print('Supported Platforms: Windows, Mac and Linux')
    elif games[each_game]['platform'] == 'mac':
        print('Supported Platforms: Windows and Mac')
    elif games[each_game]['platform'] == 'linux':
        print('Supported Platforms: Windows and Linux')
    else:
        print('Supported Platforms: Windows Only')
    print('\n')

    # printing price
    try:
        print('Price: Discounted ' + games[each_game]['after_price'] + ' from ' + games[each_game]['before_price'])
    except KeyError:
        print('Price: ' + games[each_game]['price'])
    except Exception:
        print('Bundled Price: ' + games[each_game]['bundle_price'])
    print('\n')

    # printing system requirements
    print('System Requirements: \n')
    print('-------------------------------- \n')
    print(games[each_game]['specs'])
    print('--------------------------------')
    input('Press enter to continue ')

print('Finished Successfully')

जवाब

4 Setris Aug 29 2020 at 09:34

कार्यों में तर्क को तोड़ो

निम्न में से प्रत्येक चरण के लिए अलग-अलग फ़ंक्शन होने से कोड को पढ़ना आसान हो जाएगा।

  • खेल के नाम प्राप्त करें
  • खुरचनी खेल की जानकारी
  • खेल की जानकारी प्रदर्शित करें

अपनी स्क्रिप्ट के प्रवेश बिंदु पर पहरा दें

मैं एक if __name__ == "__main__":गार्ड के तहत स्क्रिप्ट निष्पादन प्रवाह को स्थानांतरित करने की सलाह दूंगा। ऐसा करने से आप स्क्रिप्ट चलाए बिना इस फ़ाइल से कार्यों को अन्य फ़ाइलों में आयात कर सकते हैं।

sys.exit()नियंत्रण प्रवाह के लिए उपयोग करने से बचें

कॉलिंग sys.exit()पायथन इंटरप्रेटर को बंद कर देता है, जो किसी भी कोड को बनाता है जो इसे परीक्षण करना मुश्किल बनाता है। आपको इसके बजाय स्क्रिप्ट को रिफलेक्टर करना चाहिए ताकि यह सभी रिकवरी योग्य मामलों के लिए सामान्य रूप से समाप्त हो जाए।

उदाहरण के लिए, यदि उपयोगकर्ता आयु-प्रतिबंधित गेम के लिए जानकारी नहीं चाहता है, तो उसे छोड़ दें और सूची में अगले गेम पर जाएं। मुझे लगता है कि यह एक बेहतर उपयोगकर्ता अनुभव के लिए वैसे भी बना होगा, क्योंकि अगर हमें exit()सूची में अन्य शेष खेलों को संसाधित करने की आवश्यकता नहीं है।

समर्थित प्लेटफ़ॉर्म एक सूची होनी चाहिए

निर्धारित करने और एक खेल के लिए समर्थित प्लेटफार्मों मुद्रण में, आप बूलियन्स है macऔर linuxजो अंततः के एक स्ट्रिंग लेने से एक के लिए अनुवाद किया जाता है all, mac, linux, windows:

if mac and linux:
    games[a_game]['platform'] = 'all'      # windows, mac, linux
elif mac:
    games[a_game]['platform'] = 'mac'      # windows, mac
elif linux:
    games[a_game]['platform'] = 'linux'    # windows, linux
else:
    games[a_game]['platform'] = 'windows'  # windows

मुझे लगता है कि यह एक सूची के रूप में मॉडल करने के लिए अधिक समझ में आता है, उदाहरण के लिए ["windows", "mac"]इसलिए यह अधिक स्पष्ट है कि क्या प्लेटफॉर्म समर्थित हैं। यह आपको अतिरिक्त लिखने से भी बचाएगा यदि इन को प्रिंट करते समय / elif / अन्यथा तर्क।

फ्लैट नेस्टेड से बेहतर है

मूल्य पुनर्प्राप्ति चरण में ब्लॉक किए गए नेस्टेड प्रयास को छोड़कर पढ़ना बहुत मुश्किल है।

यदि आप किसी फ़ंक्शन में मूल्य पुनर्प्राप्ति को सौंपते हैं, तो आप तर्क को संरचना कर सकते हैं, इसलिए यह नेस्टेड के बजाय समतल है, जैसे कि निम्नलिखित छद्मकोड में:

def get_price() -> str:
    element = find_element_for_bundle_price()
    if element:
        return element.text

    element = find_element_for_non_discounted_price()
    if element:
        return element.text

    element = find_element_for_discounted_price()
    if element:
        return element.text

    # If we don't find a price on the page, it's free?
    # Actually this is not always true, but for this example
    # we'll assume this is the case.
    return 'FREE'

अपवाद से निपटने का दुरुपयोग

स्क्रिप्ट पकड़ रहा है KeyErrorऔर Exceptionतीन अलग-अलग प्रकार की कीमतों को प्रिंट करने के लिए संभाल रहा है: बंडल, रियायती और मानक। यह निश्चित रूप से अपवाद से निपटने का दुरुपयोग है, खासकर जब से सामान्य को पकड़ना Exceptionशायद ही कभी एक अच्छा विचार है क्योंकि यह अन्य त्रुटियों को छिपा सकता है जो आप उम्मीद कर रहे थे। इसके अलावा यह यहाँ की जरूरत नहीं है; हम बस का उपयोग कर सकते हैं अगर / elif / और:

game_dict = games[each_game]
if 'bundle_price' in game_dict:
    # print bundle price
elif 'before_price' in game_dict and 'after_price' in game_dict:
    # print discounted price
else:
    # print standard price

डाटा प्रबंधन

आपने उल्लेख किया कि आप एक शब्दकोश का उपयोग करने के बजाय प्रत्येक खेल के लिए कक्षाएं या ऑब्जेक्ट होने के बारे में सोच रहे थे। मुझे लगता है कि यह एक अच्छा विचार है। यह कोड को कम नहीं कर सकता है, लेकिन यह निश्चित रूप से कोड की पठनीयता में सुधार करेगा।

इसके लिए एक अच्छा उम्मीदवार एक साधारण डेटा कंटेनर होगा typing.NamedTuple। जैसा कि @ MaartenFabré ने टिप्पणियों में सुझाव दिया है, dataclasses.dataclassइस तरह के डेटा कंटेनर के लिए एक और अच्छा विकल्प है।

बिना धार के मामले

निम्नलिखित खोज प्रश्नों का परिणाम स्क्रिप्ट टाइमिंग में होगा:

  • कोई भी खोज क्वेरी जो स्टीम पर कोई सुझाव नहीं देती है। यह एक गेम की तरह कुछ हो सकता है जो अस्तित्व में नहीं है (अभी तक), जैसे "funkytown", या एक गैर-रिक्त स्ट्रिंग जिसमें केवल व्हाट्सएप शामिल है, उदा " "

  • कोई भी खोज क्वेरी जहां पहला सुझाव स्टीम पैकेज है, उदा "the witcher 3 wild hunt game of the year"

  • कोई भी खोज क्वेरी जहां पहला सुझाव स्टीम बंडल है, उदा "monkey island collection"

मैंने पहले भी इसका उल्लेख किया था, लेकिन स्क्रिप्ट गलत तरीके से मानती है कि यदि पृष्ठ पर कोई मूल्य प्रदर्शित नहीं किया जाता है, तो गेम मुफ्त है। लेकिन स्टीम पर अप्रबंधित गेम हैं जहां डेवलपर ने कोई कीमत निर्धारित नहीं की है, और साथ ही उन्होंने इसे "फ्री" या "फ्री-टू-प्ले" गेम ("स्पिरिट ऑफ ग्लॉस" के रूप में वर्गीकृत नहीं किया है। ठोस उदाहरण मुझे मिला)। इस मामले में प्रदर्शित करने की कीमत "टीबीडी" या "अज्ञात" जैसी होनी चाहिए।

सौभाग्य से, और अगले खंड को शुरू करने के एक तरीके के रूप में, एक एपीआई है जिसका उपयोग हम मुफ्त और मुफ्त में अंतर करने के लिए कर सकते हैं। यह API एक ऐसे is_freeक्षेत्र को उजागर करता है, trueजब कोई गेम फ्री या फ्री-टू-प्ले होता है। यदि आप इस समीक्षा के अंत में कूद जाते हैं, तो आप देख सकते हैं कि यह मेरी उदाहरण स्क्रिप्ट में कैसे पुनः प्राप्त हो रहा है।

एपीआई को स्क्रैपिंग के लिए प्राथमिकता दें

एपीआई के साथ, डेटा पुनर्प्राप्ति तेजी से होती है - और अक्सर सेलेनियम के साथ स्क्रैपिंग की तुलना में तेजी के आदेश। API के साथ, डेटा निष्कर्षण आसान है क्योंकि प्रतिक्रिया का प्रारूप अक्सर JSON है।

मैं हमेशा इस बात का उल्लेख करता हूं कि जब भी स्क्रैपिंग आएगी क्योंकि संभावित समय और प्रयास की बचत बहुत बड़ी हो सकती है। एक आधिकारिक एपीआई, या एक अनौपचारिक एपीआई जिसे दस्तावेज किया गया है, खोजने के लिए कुछ समय बिताएं। यदि कुछ भी नहीं बदलता है, तो एक HTTP / S ट्रैफिक इंस्पेक्टर जैसे Fiddler या Chrome DevTools के साथ घूमें और देखें कि क्या आप किसी भी आशाजनक अनौपचारिक API पा सकते हैं। अगर अंत में आप कुछ भी नहीं पा सकते हैं, तो अंतिम उपाय के रूप में स्क्रैपिंग पर वापस जाएं।

इस मामले में, वास्तव में एक अनौपचारिक स्टीम स्टोर एपीआई है जो उपलब्ध है। इसका इस्तेमाल करने के लिए हम स्टीम एप्लिकेशन आईडी की जरूरत है या भाप आइटम हम रुचि में कर रहे हैं के पैकेज ID है, लेकिन हम एपीआई से कि प्राप्त कर सकते हैं कि शक्तियों खोज सुझाव ड्रॉप-डाउन मेनू, https://store.steampowered.com/search/suggest

API का उपयोग करके उदाहरण स्क्रिप्ट

निम्नलिखित अनधिकृत स्टीम स्टोर एपीआई का उपयोग करके एक उदाहरण स्क्रिप्ट है।

#!/usr/bin/env python3

import re
import json
import requests
from enum import Enum
from bs4 import BeautifulSoup  # type: ignore
from typing import Any, Dict, List, Optional, NamedTuple, Union

SEARCH_SUGGEST_API = "https://store.steampowered.com/search/suggest"
APP_DETAILS_API = "https://store.steampowered.com/api/appdetails"
PACKAGE_DETAILS_API = "https://store.steampowered.com/api/packagedetails"


class Platform(Enum):
    WINDOWS = "windows"
    MAC = "mac"
    LINUX = "linux"

    def __str__(self) -> str:
        return str(self.value)


class Price(NamedTuple):
    initial: int  # price in cents
    final: int  # price in cents


class SteamGame(NamedTuple):
    app_id: int
    name: str
    platforms: List[Platform]
    is_released: bool
    is_free: bool
    price: Optional[Price]
    pc_requirements: str

    def __str__(self) -> str:
        if self.is_free:
            price = "Free"
        elif self.price:
            final = f"${self.price.final / 100}" if self.price.initial == self.price.final: price = final else: price = f"{final} (previously ${self.price.initial / 100})"
        else:
            price = "TBD"

        platforms = ", ".join(str(p) for p in self.platforms)
        is_released = "Yes" if self.is_released else "No"

        return "\n".join(
            (
                f"Name: {self.name}",
                f"Released: {is_released}",
                f"Supported Platforms: {platforms}",
                f"Price: {price}",
                "",
                "PC Requirements:",
                self.pc_requirements,
            )
        )


class SteamBundle(NamedTuple):
    bundle_id: int
    name: str
    price: Price
    application_names: List[str]

    def __str__(self) -> str:
        final = f"${self.price.final / 100}" if self.price.initial == self.price.final: price = final else: price = f"{final} (without bundle: ${self.price.initial / 100})"

        return "\n".join(
            (
                f"Name: {self.name}",
                f"Price: {price}",
                "",
                "Items included in this bundle:",
                *(f"  - {name}" for name in self.application_names),
            )
        )


class SteamPackage(NamedTuple):
    package_id: int
    name: str
    platforms: List[Platform]
    is_released: bool
    price: Optional[Price]
    application_names: List[str]

    def __str__(self) -> str:
        if self.price:
            final = f"${self.price.final / 100}" if self.price.initial == self.price.final: price = final else: initial = f"${self.price.initial / 100}"
                price = f"{final} (without package: {initial})"
        else:
            price = "TBD"

        platforms = ", ".join(str(p) for p in self.platforms)
        is_released = "Yes" if self.is_released else "No"

        return "\n".join(
            (
                f"Name: {self.name}",
                f"Released: {is_released}",
                f"Supported Platforms: {platforms}",
                f"Price: {price}",
                "",
                "Items included in this package:",
                *(f"  - {name}" for name in self.application_names),
            )
        )


SteamItem = Union[SteamGame, SteamBundle, SteamPackage]


def deserialize_bundle_data(encoded_bundle_json: str) -> Any:
    return json.loads(re.sub(r""", '"', encoded_bundle_json))


def extract_app_ids(bundle_data: Dict[str, Any]) -> List[int]:
    return [
        app_id
        for item in bundle_data["m_rgItems"]
        for app_id in item["m_rgIncludedAppIDs"]
    ]


def lookup_app_names(
    session: requests.Session, app_ids: List[int]
) -> List[str]:
    app_names = []
    for app_id in app_ids:
        params = {"appids": app_id, "filters": "basic"}
        response = session.get(APP_DETAILS_API, params=params)
        response.raise_for_status()

        app_names.append(response.json()[str(app_id)]["data"]["name"])

    return app_names


def extract_bundle_price(bundle_data: Dict[str, Any]) -> Price:
    total_price = sum(
        item["m_nFinalPriceInCents"] for item in bundle_data["m_rgItems"]
    )
    total_price_with_bundle_discount = sum(
        item["m_nFinalPriceWithBundleDiscount"]
        for item in bundle_data["m_rgItems"]
    )

    return Price(total_price, total_price_with_bundle_discount)


def extract_package_information(
    package_id: int, package_data: Dict[str, Any]
) -> SteamPackage:
    return SteamPackage(
        package_id=package_id,
        name=package_data["name"],
        platforms=[p for p in Platform if package_data["platforms"][str(p)]],
        is_released=not package_data["release_date"]["coming_soon"],
        price=Price(
            package_data["price"]["initial"], package_data["price"]["final"]
        ),
        application_names=[app["name"] for app in package_data["apps"]],
    )


def get_package(session: requests.Session, package_id: str) -> SteamPackage:
    params = {"packageids": package_id}
    response = session.get(PACKAGE_DETAILS_API, params=params)
    response.raise_for_status()

    return extract_package_information(
        int(package_id), response.json()[package_id]["data"]
    )


def extract_requirements_text(requirements_html: str) -> str:
    soup = BeautifulSoup(requirements_html, "html.parser")
    return "\n".join(tag.get_text() for tag in soup.find_all("li"))


def extract_game_information(game_data: Dict[str, Any]) -> SteamGame:
    price_overview = game_data.get("price_overview")
    price = (
        Price(price_overview["initial"], price_overview["final"])
        if price_overview
        else None
    )

    requirements = game_data["pc_requirements"]
    minimum = extract_requirements_text(requirements["minimum"])
    recommended_html = requirements.get("recommended")
    recommended = (
        extract_requirements_text(recommended_html)
        if recommended_html
        else None
    )

    minimum_requirements = f"[Minimum]\n{minimum}"
    if recommended:
        recommended_requirements = f"[Recommended]\n{recommended}"
        pc_requirements = (
            minimum_requirements + "\n\n" + recommended_requirements
        )
    else:
        pc_requirements = minimum_requirements

    return SteamGame(
        app_id=game_data["steam_appid"],
        name=game_data["name"],
        platforms=[p for p in Platform if game_data["platforms"][str(p)]],
        is_released=not game_data["release_date"]["coming_soon"],
        is_free=game_data["is_free"],
        price=price,
        pc_requirements=pc_requirements,
    )


def get_game(session: requests.Session, app_id: str) -> SteamGame:
    params = {"appids": app_id}
    response = session.get(APP_DETAILS_API, params=params)
    response.raise_for_status()

    return extract_game_information(response.json()[app_id]["data"])


def get_game_information(games: List[str]) -> Dict[str, Optional[SteamItem]]:
    game_to_info = {}

    with requests.Session() as session:
        for game in games:
            params = {"term": game, "f": "games", "cc": "US", "l": "english"}
            response = session.get(SEARCH_SUGGEST_API, params=params)
            response.raise_for_status()

            # get first search suggestion
            result = BeautifulSoup(response.text, "html.parser").find("a")

            if result:
                bundle_id = result.get("data-ds-bundleid")
                package_id = result.get("data-ds-packageid")
                app_id = result.get("data-ds-appid")

                if bundle_id:
                    name = result.find("div", class_="match_name").get_text()
                    bundle_data = deserialize_bundle_data(
                        result["data-ds-bundle-data"]
                    )
                    app_ids = extract_app_ids(bundle_data)
                    app_names = lookup_app_names(session, app_ids)
                    price = extract_bundle_price(bundle_data)

                    info: Optional[SteamItem] = SteamBundle(
                        bundle_id=int(bundle_id),
                        name=name,
                        price=price,
                        application_names=app_names,
                    )
                elif package_id:
                    info = get_package(session, package_id)
                elif app_id:
                    info = get_game(session, app_id)
                else:
                    info = None
            else:
                info = None

            game_to_info[game] = info

    return game_to_info


def display_game_information(
    game_information: Dict[str, Optional[SteamItem]]
) -> None:
    arrow = " =>"
    for game_query, game_info in game_information.items():
        result_header = f"{game_query}{arrow}"
        query_result = (
            game_info if game_info else f"No results found for {game_query!r}."
        )
        result = "\n".join(
            (
                result_header,
                "-" * (len(result_header) - len(arrow)),
                "",
                str(query_result),
                "\n",
            )
        )

        print(result)


if __name__ == "__main__":
    games = [
        "slay the spire",
        "civ 6",
        "funkytown",
        "path of exile",
        "bless unleashed",
        "the witcher 3 wild hunt game of the year",
        "divinity source",
        "monkey island collection",
        "star wars squadrons",
        "spirit of glace",
    ]
    game_info = get_game_information(games)
    display_game_information(game_info)