CoderCastrov logo
CoderCastrov
Питон

Парсинг веб-сайта поиска работы 'JobsDB' с использованием beautifulsoup (прохождение классной работы)

Парсинг веб-сайта поиска работы 'JobsDB' с использованием beautifulsoup (прохождение классной работы)
просмотров
7 мин чтение
#Питон

Я сейчас учусь в буткемпе по науке о данных, и нам требуется применить навыки парсинга веб-сайтов к локальному сайту поиска работы. Эта статья служит сводкой для моего обучения и справочником для начинающих. Мой подход и код предназначены только для ознакомления, наверняка есть много других лучших способов. Я объясню идею, полный код я размещу в конце этой статьи, а позже на GitHub. Удачи!

Цель: Спарсить локальный сайт поиска работы JobsDB

Мысленный штурм и трудности: Я хочу получить следующую информацию: название вакансии, компания, требуемые навыки, опыт работы и т.д., а затем узнать, какие навыки являются наиболее важными для работы аналитиком данных в Гонконге. Для начала я прочитал несколько статей, но их сайты поиска работы отличаются от JobsDB. Во-первых, JobsDB не показывает подробной информации, пока вы не щелкнете на объявление о вакансии вручную. Лучший способ решить эту проблему, возможно, использовать Selenium / scrapy, но я не знаком с ними. Во-вторых, информация скрыта в теге скрипта / xpath, что делает извлечение данных более сложным.

Шаг 1. Сначала изучите структуру.

Вы можете видеть, что каждая страница отображает максимум 30 результатов. Когда вы загружаете HTML с помощью запроса и проверяете код этой страницы, у вас не будет доступа к отдельным результатам. Однако в теге <script> вы можете найти все URL-ссылки на отдельные записи этой страницы.

Таким образом, мой подход заключается в следующем: получить все отдельные URL-адреса записей с КАЖДОЙ страницы. Затем парсить каждую вакансию по отдельности, используя цикл. Сначала нам нужно узнать, сколько раз мы должны выполнить цикл, чтобы получить все URL-адреса записей со всех страниц.

Шаг 2. Извлечение всех URL-адресов вакансий

Сначала нам нужно знать, сколько раз нам нужно выполнить цикл, чтобы получить все URL-адреса вакансий со всех страниц. Мы можем получить общее количество вакансий с помощью следующего кода:

total_num_jobs = re.findall('\d+', soup.find('meta', property="og:description")['content'])

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

Жизнь не так проста. Так называемые URL-адреса вакансий, которые мы собираем, полны примесей. Нам нужно дальше обработать их с помощью регулярных выражений. Нам нужна только последняя часть URL-адреса. (Мой одноклассник может использовать подходящий тег для нахождения "чистого" URL-адреса вакансии, но это его метод, и я не должен копировать его. Работа с структурой HTML и тегами может облегчить веб-парсинг.)

script_str = soup.find_all("script")[1].get_text()[28:]
pattern = re.compile(r'(https\W\Wu002F\Wu002Fhk\Wjobsdb\Wcom\Wu002Fhk\Wu002Fen\Wu002Fjob\Wu002F)([a-zA-Z0-9]+\W)*')
job_url_raw = []
for i in pattern.finditer(script_str):
    job_url_raw.append(i[0])
for i in job_url_raw:
    a = re.sub("u002F",'',i)
    b = re.sub('"','',a)
    c = b.split('\\')[-1]
    job_id.append(c)

Шаг 3. Извлечение информации из каждого поста

job_title = soup.find(class_='general-pos ad-y-auto-txt2').text.strip()
company = soup.find(class_='jobad-header-company').text.lstrip()
yr_exp = soup.find('b', class_='primary-meta-exp').text.strip()
yr_exp = yr_exp[0]
level = soup.find('b', class_='primary-meta-lv').text.strip()

Очень жаль, что я потратил столько времени, пытаясь извлечь точный текст «ответственности» и «требований» из объявления о вакансии. Однако, когда я попытался применить функцию цикла для всех объявлений о вакансиях, я обнаружил, что JobsDB использует разные методы тегов и атрибутов для хранения этой информации. Поэтому я не могу придумать краткий универсальный способ извлечения этих двух частей. В конце концов, я могу использовать <span>, который на самом деле возвращает так много ненужных данных.

role = [i.text.lower() for i in soup.find_all('span')]

Шаг 4. Запись в CSV файл (необязательно)

with open('jobsDBtest_csv_result.csv', 'a') as f:
    csv_writer = csv.writer(f)
    csv_writer.writerow([i, job_title, company, yr_exp, level, required_skill])

На самом деле это необязательно, потому что мы можем использовать pandas для обработки этой информации. У меня возникли проблемы с кодировкой в Excel и не все значения отображаются, но кому это важно.

Шаг 5. Извлечение требуемых навыков для работы

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

required_skill=[]
for i in skill_set:
    result = re.search(i, role2)
    if result:
        required_skill.append(i)
        skill_list.append(i)
    else:
        pass

Шаг 6. Подсчет наиболее востребованных навыков и экспорт в csv

frequency={}
for item in skill_list:
    frequency[item]=frequency.get(item,0)+1
import pandas as pd
skill_count=pd.Series(frequency)
skill_count.sort_values(ascending=False,inplace=True)
skill_countskill_count.to_csv('data_skills_count.csv')

Шаг 7. Результат и ограничения

Как видно из результатов, наиболее часто встречающимися навыками являются "bi" (сокращение от Business Intelligence) и "r". Я бы сказал, что их следует удалить из-за ограничений моего скрипта. Я использую регулярные выражения для поиска требуемых навыков в объявлениях о вакансиях, но "r" и "bi" являются слишком общими. Например, если исходная фраза - "are", использование "r" для поиска все равно вернет положительный результат, так же и с "bi".

На самом деле, Excel может быть потенциально ложным положительным результатом, потому что они всегда говорят "отличные навыки в Excel", но по моему опыту Excel важен в работе, связанной с данными, в Гонконге.

Я немного разочарован, потому что мой интерес больше связан с разработкой искусственного интеллекта с использованием глубокого обучения с помощью Tensorflow, Keras и PyTorch. Всего 739 объявлений о вакансиях, связанных с "data" на JobsDB (на данный момент), но глубокое обучение, Keras, Tensorflow, PyTorch - это САМЫЕ редко требуемые навыки....

Дальнейшие идеи для работы: (извините, у меня сейчас нет времени, просто бросаю идеи)

  1. Мы можем сравнить различные веб-сайты поиска работы (Glassdoor HK и JobsDB и т. д.) по разнообразию вакансий и "качеству". В следующий раз, когда вы будете искать работу, вы будете знать, какой из них лучше. Если вы ищете аналитика данных на JobsDB и Glassdoor в Гонконге, вы поймете, о чем я говорю.

  2. Вы также можете извлечь информацию о зарплате, чтобы сравнить среднюю зарплату разных должностей в разных странах. Затем вы можете сравнить с ВВП/средним доходом/индексом доступности... и увидеть, в какой стране находится рай для вашей желаемой работы.

Полный код: Используйте его, если вам нужно. Код является нерабочим, потому что я спешил вчера вечером. Если у вас есть лучший подход, пожалуйста, оставьте комментарий, чтобы мы могли вместе улучшить! Большое спасибо. (извините, что вставка кода в Medium может вызвать проблемы с отступами.)

from bs4 import BeautifulSoup
import requests
import re
import pandas as pd
import csv 
import timeglobal job_title_list
job_title_list=[]
global company_list
company_list=[]
global yr_exp_list
yr_exp_list=[]
global level_list
level_list=[]
global role_list
role_list=[]
role=[]
global skill_list
skill_list=[]def get_job_id(job_name):
    t1=time.time()    baseurl='[https://hk.jobsdb.com/hk/search-jobs/'+re.sub('](https://hk.jobsdb.com/hk/search-jobs/'+re.sub(') ','-  ',job_name)+'/1'
    r=requests.get(baseurl,headers={"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"}).text
    soup=BeautifulSoup(r,'lxml')
    
    pattern=re.compile(r'(\d+)')
    total_num_jobs=re.findall('\d+', soup.find('meta', property="og:description")['content'])
    
    float(total_num_jobs[0])  #на самом деле только одно значение
    num_page=int(total_num_jobs[0]) //30 +1   #если действительно не нужно? как в этом случае
    
    global job_id 
    job_id=[]
    
    skills = ['R', 'RStudio', 'Markdown', 'Latex', 'SparkR', 'D3', 'D3.js','Microsoft Office','Excel',
    'Unix', 'Linux', 'MySQL', 'Microsoft SQL server', 'SQL','VBA','Qlik'
    'Python', 'SPSS', 'SAS', 'C#','Matlab','Java', 'keras',
    'JavaScript', 'HTML', 'HTML5', 'CSS', 'CSS3','PHP', 'Excel', 'Tableau',
    'AWS', 'Amazon Web Services ','Google Cloud Platform', 'GCP','theano'
    'Microsoft Azure', 'Azure', 'Hadoop', 'Spark','python'
    'MapReduce', 'Map Reduce','Shark', 'Cassandra',
    'NoSQL', 'MongoDB', 'GIS', 'Haskell', 'Scala', 'Ruby','Perl',
    'Mahout', 'Stata','Deep Learning','Machine Learning',  'Pytorch', "Tensorflow",'Caffe','API','seo','Business Intelligence'
    , 'BI', ]
    skill_lower=[i.lower() for i in skills]
    skill_set=list(set(skill_lower))
    skill_set
    
    
    for i in range(1,num_page+1):
        #time.sleep(1)
        print(f'Now start to craw job search result P.{i}')
        newurl=baseurl[:-1]+str(i)
        r=requests.get(newurl,headers={"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"}).text
        soup=BeautifulSoup(r,'lxml')
        
        script_str=soup.find_all("script")[1].get_text()[28:]pattern=re.compile(r'(https\W\Wu002F\Wu002Fhk\Wjobsdb\Wcom\Wu002Fhk\Wu002Fen\Wu002Fjob\Wu002F)([a-zA-Z0-9]+\W)*') 
        job_url_raw=[]
        job_url_raw2=[]
        for i in pattern.finditer(script_str):
            job_url_raw.append(i[0])
for i in job_url_raw:
            a=re.sub("u002F",'',i)
            b=re.sub('"','',a)
            c=b.split('\\')[-1]
            print(c)
            job_id.append(c)
        
    for i in job_id:
        try: #есть пустой id?
            print(f'Now start to craw individual job id {i}')
 
            joburl='[https://hk.jobsdb.com/hk/en/job/'+str(i)](https://hk.jobsdb.com/hk/en/job/'+str(i))
            r=requests.get(joburl,headers={"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"}).text
            soup=BeautifulSoup(r,'lxml')try:
                job_title= soup.find(class_='general-pos ad-y-auto-txt2').text.strip()
            except:
                job_title= soup.find(class_='general-pos ad-y-auto-txt').text.strip()
            print(f'Job title is {job_title}')try:
                company= soup.find(class_='jobad-header-company').text.lstrip()
            except:
                company= soup.find(class_='jobad-header-company ad-y-auto-txt1').text.lstrip()
            print(f'company {job_title}')role=[i.text.lower() for i in soup.find_all('span')]

            role2=str(role)
            
            print(f' Got job role for this post {job_title}')
            try:
                yr_exp=soup.find('b', class_='primary-meta-exp').text.strip()
                yr_exp=yr_exp[0]
            except:
                yr_exp='Didnt specify'
            print(f' Exp is {yr_exp} ')try:
                level=soup.find('b', class_='primary-meta-lv').text.strip()
            except:
                level='Didnt specify'
            print(f' level is {yr_exp} ')job_title_list.append(job_title)        
            company_list.append(company) 
            yr_exp_list.append(yr_exp)
            role_list.append(role)
            
            print('now to matching')
            required_skill=[]
            for i in skill_set:
                result=re.search(i,role2)
                if result:
                    required_skill.append(i)
                    skill_list.append(i)
                else:
                    pass
                
            with open ('jobsDBtest_csv_result.csv','a') as f:
                csv_writer= csv.writer(f)
                csv_writer.writerow([i,job_title,company,yr_exp,level,required_skill])    

        except:
            print('Cant crawd some id')
            pass
    t2= time.time()
    print(f'It takes {t2-t1} to crawd all the jobs info')
    print(f'There are total {len(job_id)} jobs in this category')    frequency={}
    for item in skill_list:
        frequency[item]=frequency.get(item,0)+1
    skill_count=pd.Series(frequency)
    skill_count.sort_values(ascending=False,inplace=True)
    
    return skill_countget_job_id('data analyst')