Gdy ostrzyże się owieczkę, będę z wełny mieć czapeczkę

Zatem wracam, niniejszym drugi sezon bloga uważam za otwarty!:)

Pierwsza praca w IT czyli o tym, żeby choć w potrzebie nie wchodzić w nie swoje buty

Sporo się w ciagu tego roku zawodowo wydarzyło, na czele z największym przełomem, czyli przebranżowieniem. Zgodnie z planem znalazłam pracę i zdecydowałam się na zmianę, przy czym przy całej trudności i złożoności tego tematu, sama decyzja związana ze złożeniem wypowiedzenia “staremu życiu” wiązała się chyba z większym stresem niż długi i wymagający etap nauki i poszukiwania pracy. Ale uff, to już za mną, odważyłam się, skoczyłam. No i… nie do końca trafiłam. Choć Python, choć firma, wydawałoby się, marzenie, choć skomplikowany i piętrowy proces rekrutacji, to w efekcie… pudło:) Szczegóły pominę, ale do tej historii jeszcze wrócę, bo jest bardzo pouczająca. Przynajmniej dla mnie:)

Druga praca w IT czyli o tym, że warto trzymać się swoich planów

Doświadczenie to skłoniło mnie do ponownego przemyślenia swoich priorytetów. Zgodnie z jednym z lepszych swoich obyczajów postanowiłam potraktować tę obiektywnie niełatwą sytuację jako okazję na kolejny start. I tym razem był to strzał w dziesiątkę. W pierwszym przypadku, przy wielkim wysiłku i zaangażowaniu z mojej strony żeby dostać tę pracę, ostateczny wybór czym będę się zajmować nie należał do mnie. Poszłam za wydawałoby się świetną okazją, możliwością, z jakiej po prostu nie można nie skorzystać. Za drugim razem zdecydowałam się postawić na swoim i trzymać się swoich pierwotnych planów. Poświęciłam spoooro czasu na naukę i realizację projektów związanych z analizą i wizualizacją danych. Co złożyło się na następujący zestaw: SQL, bazy danych, Python, Numpy, Pandas, pythonowe bibioteki do wizualizacji danych (seaborn!) oraz Tableau i… ech, Excel:)

I o jednym z takich szybkich projektów, służących nie tylko nauce ale i ludzkości:) chcę dziś napisać.

Stack: Beautiful Soup, Pandas i… Excel;) Całość spina Jupyter Notebook.

Moja przyjaciółka dzierga piękne rzeczy na drutach. W sezonie zimowym nie może się więc opędzić od zamówień, najczęściej składanych przez znajomych i realizowanych pro bono. Trudno jest się wbić do długaśniej kolejki, a że miałyśmy z dziewczynami pilną i gwałtowną potrzebę posiadania nowych czapek, musiałyśmy zastosować skuteczny argument ustawiający nas w kolejce “dla uprawnionych”:) I tu z pomocą przyszedł wyżej wymieniony stack, a właściwie jego cyniczne wykorzystanie:)

Moja dziergająca przyjaciółka kupuje włóczki korzystając z dwóch sklepów internetowych. Obydwie strony miewają ten sam asortyment ale różne ceny, czasowe promocje i inaczej ustawione filtry ułatwiające wyszukanie potrzebnych materiałów. Znalezienie odpowiedniej włóczki zabiera więc czas, bywa irytujące i nieefektywne.

Zawarłyśmy więc następujący uklad: za trzy piękne, kolorowe czapki na zimowe mrozy ja ułatwię jej życie:) Nową jakość życia miało jej zapewnieć excelowe (bo w tym czuje się dobrze)  narzędzie umożliwające szybkie przefiltowanie aktualnej ofety po kolorach, cenie za 100 g,  ewentualnych promocjach, składzie, przezaczeniu (szalik, sweter, ubranko dla malucha, spódnica etc) oraz producencie.

Zatem, do dzieła!

Scraper

Konstrukcja url poszczególnych produktów pozwoliła na zastosowanie następującego podejścia przy zbieraniu adresów: scraper wchodzi na kolejne podstrony i zapisuje nazwy produktów. Następnie, po przeprasowaniu, nazwy te są konkatenowane z głównym adresem.

def get_names(basic_url):

i = 2
names = []

while i <= 190:
page = requests.get(basic_url + str(i))
soup = BeautifulSoup(page.content, 'html.parser')

for name in soup.find_all('a', class_='product-name'):
names.append(name.text)
sleep(randint(1, 2))
i += 1

parsed_names = []
for raw in names:
parsed_names.append((raw.replace(' ', '-')).replace('(', '').replace(')', '').lower())

names_final = []
for item in parsed_names:
names_final.append((unicodedata.normalize('NFD', item).encode('ascii', 'ignore')).decode("utf-8"))

return names_final

W efekcie powstaje rodzaj listingu, dzięki któremu możemy uzyskać wymagane informacje o poszczególnych produktach. Dane te zapisujemy do pandas.

def get_details(source):

    links = []
    names = []
    prices = []
    descriptions = []
    categories = []
    products = []
    all_attributes = []
    weights = []
    information = []
    

    for item in source:        
        try:            
            page = requests.get('https://www...pl/produkt/' + item.replace('-mix', ''))
            soup = BeautifulSoup(page.content, 'html.parser')
        except Exception as e:
            pass

        try:            
            link = page.url
        except Exception as e:
            link = None
        links.append(link)

        try:
            name = soup.find('h1', class_='product_title entry-title').text
        except Exception as e:
            name = None
        names.append(name)

        try:
            price = soup.find('span', class_='amount')
            price_num = ((str(price.text))[:4]).replace(',', '.')
        except Exception as e:
            price_num = None
        prices.append(price_num)

        try:
            raw_description = soup.find('div', class_='product-single-short-description').text
            description = raw_description.replace('\n', ' ').replace('\xa0', '').replace('_', '')
        except Exception as e:
            description = None
        descriptions.append(description)

        product_meta = soup.find('div', class_='product_meta')

        try:
            raw_category = product_meta.find('span', class_='posted_in').text
            category = raw_category.replace('Kategorie: ', '').replace('Kategoria: ', '').replace('.', '')
        except Exception as e:
            category = None
        categories.append(category)

        try:
            raw_product = product_meta.find('span', class_='tagged_as').text
            product = raw_product.replace('Produkt: ', '').replace('.', '')
        except Exception as e:
            product = None
        products.append(product)

        try:
            raw_attributes = soup.find('table', class_='shop_attributes').text
            attributes = raw_attributes.replace('\n', '')
        except Exception as e:
            attributes = None
        all_attributes.append(attributes)
        
        try:
            raw_weight = soup.find('div', class_='yith-wcbm-badge yith-wcbm-badge-custom yith-wcbm-badge-40549').text
            weight = raw_weight.replace('\n ', '')
        except Exception as e:
            weight = None
        weights.append(weight)
        
        try:
            raw_info = soup.find('div', class_='panel entry-content wc-tab').text
            info = raw_info
        except Exception as e:
            ifo = None
        information.append(info)

        sleep(randint(1, 2))

    columns = ['url', 'name', 'price', 'description', 'category', 'product', 'attributes', 'weight', 'info']

    wool_df = pd.DataFrame({'url': links,
                            'name': names,
                            'price': prices,
                            'description': descriptions,
                            'category': categories,
                            'product': products,
                            'attributes': all_attributes,
                            'weight': weights,
                            'info': information})[columns]

    return wool_df

Transformacja/parsowanie

Pandas pozwala na szybką transformację danych:

def price_per_100g(row):
if row['weight_num'] == 100.0:
return row['price_num']
elif row['weight_num'] == 50.0:
return row['price_num'] * 2
else:
None

def take_dest(col):
items = str(col).split(',')[:-2]
return ','.join(items)

def change_to_yes_no(boolean):
if boolean == True:
return 'Tak'
else:
return 'Nie'

def parse_df(df):
df['color'] = df['name'].str.split(')').str.get(1).str.strip().str.lower()
df['description'] = df['description'].str.strip()
df['destination'] = df['attributes'].str.split('m').str.get(1).str.replace('Przeznaczenie', '')
df['destination'] = df['destination'].map(take_dest)
df['length'] = df['attributes'].str.split('m').str.get(0)
df['length'] = df['length'] + 'm'
df['length'] = df['length'].str.replace('Długość w ', '')
df['temp1'] = df['length'].str.split(' g').str.get(0)
df['temp1'] = df['temp1'] + ' g: '
df['temp2'] = df['length'].str.split(' g').str.get(1)
df['length'] = df['temp1'] + df['temp2']
del wool['attributes']
del df['temp1']
del df['temp2']
df['price'] = df['price'].str.strip()
df['weight_num'] = df['weight'].str.replace('g', '')
df['weight_num'] = pd.to_numeric(df['weight_num'])
df['price_num'] = pd.to_numeric(df['price'])
df['price_per_100g'] = df[ ['price_num', 'weight_num'] ].apply(price_per_100g, axis=1)
df['price_per_100g'] = pd.to_numeric(df['price_per_100g'])
del df['weight_num']
del df['price_num']
del wool['price']
wool['composition'] = wool['info'].str.split('\n').str.get(2).str.replace('\n', '')\
.str.replace('\xa0', '').str.replace('Skład:', '')
df['alpaca_in_composition'] = df['composition'].str.contains('alpaka')
df['alpaca_in_composition'] = df['alpaca_in_composition'].map(change_to_yes_no)
df['wool_in_composition'] = df['composition'].str.contains('wełna')
df['wool_in_composition'] = df['wool_in_composition'].map(change_to_yes_no)
df['merino_in_composition'] = df['composition'].str.contains('merynos')
df['merino_in_composition'] = df['merino_in_composition'].map(change_to_yes_no)
wool['size_needles_crochet'] = wool['info'].str.split('\n').str.get(5).str.replace('\n', '')\
.str.replace('\xa0', '').str.replace('Zalecany przez producenta rozmiar drutów/szydełka:', '')\
.str.replace('Zalecany przez producenta rozmiar drutów:', '')
wool['sample'] = wool['info'].str.split('\n').str.get(6).str.replace('\n', '').str.replace('\xa0', '')\
.str.replace('Próbka wg producenta:', '').str.replace('Próbka', '')
wool['recommendations'] = wool['info'].str.split('\n').str.get(7).str.replace('\n', '').str.replace('\xa0', '')\
.str.replace(' /', ',')
del wool['info']
return df

Następnie tak przygotowany DataFrame wrzucamy do Excela:

def write_to_excel(df, path):
    excel_file = df.to_excel(path)
    return excel_file

write_to_excel(wool_parsed, 'wool.xls')

Et voila! Stoimy w kolejce dla uprawnionych!:)