[PYTHON] Gratter et manger des bûches - je veux trouver un bon restaurant! ~ (Travail)

1. Motivation

2. À propos du journal de consommation

3. Zone de grattage et acquisition d'URL

En regardant l'URL de chaque boutique, dans le cas de Tokyo, elle se présente sous la forme "//tabelog.com/tokyo/A .... / A ...... / ........ /" Il est devenu. Par exemple, dans le cas d'une boutique à Shibuya, l'URL est "//tabelog.com/tokyo/A1303/A130301/......../" Il peut être interprété comme "Shibuya / boutique spécifique /". En regardant la page supérieure du journal des aliments, il y a des données jusqu'à "// tabelog.com/tokyo/A .... /" à la place de "Recherche par zone", donc je vais les obtenir en premier. Comme prévu, nous n'avons pas besoin de toutes les données pour l'ensemble du pays, donc après avoir réduit dans une certaine mesure une grande zone, nous n'obtiendrons que les données que nous voulons.

import numpy as np
import pandas as pd
import requests
from bs4 import BeautifulSoup
import time

root_url = 'https://tabelog.com/'
res = requests.get(root_url)
soup = BeautifulSoup(res.content, 'html.parser')

Je l'ai analysé avec Beautiful Soup comme ça, et quand j'ai regardé à l'intérieur,

<h2 class="rsttop-heading1 rsttop-search__title">
Recherche par zone
                  </h2>
</div>
<ul class="rsttop-area-search__list">
<li class="rsttop-area-search__item">
<a class="rsttop-area-search__target js-area-swicher-target" data-swicher-area-list='[{"areaName":"Ginza / Shimbashi / Yurakucho","url":"/tokyo/A1301/"},{"areaName":"Tokyo / Nihonbashi","url":"/tokyo/A1302/"},{"areaName":"Shibuya / Ebisu / Daikanyama","url":"/tokyo/A1303/"},...

↑ Les données que vous voulez ici! !!

a = soup.find_all('a', class_='rsttop-area-search__target js-area-swicher-target')
a[0].get('data-swicher-area-list')

Si tu le fais comme ça

'[{"areaName":"Ginza / Shimbashi / Yurakucho","url":"/tokyo/A1301/"},{"areaName":"Tokyo / Nihonbashi","url":"/tokyo/A1302/"},{"areaName":"Shibuya / Ebisu / Daikanyama","url":"/tokyo/A1303/"},...

Etc. Cependant, je pensais que c'était une belle liste de dictionnaires, mais c'était complètement une chaîne de caractères. .. .. J'ai donc essayé de trouver comment le faire, mais je n'ai pas pu le trouver, alors je vais le fixer de force à la forme nécessaire bien que ce ne soit pas bon. Si vous savez comment gérer le processus ici en douceur, faites-le moi savoir!

splitted = a[0].get('data-swicher-area-list').split('"')
area_dict = {}
for i in range(int((len(splitted)-1)/8)):
    area_dict[splitted[i*8+3]] = splitted[i*8+7]

Avec cela, j'ai réussi à obtenir le dictionnaire suivant.

{'Ueno / Asakusa / Nippori': '/tokyo/A1311/',
 'Ryogoku, Kinshicho, Koiwa': '/tokyo/A1312/',
 'Nakano-Nishiogikubo': '/tokyo/A1319/',...

Pour être honnête, Tokyo seul va bien, mais si vous voulez l'obtenir de manière exhaustive, c'est comme suit.

area_url = {}
for area in a:
    area_dict = {}
    splitted = area.get('data-swicher-area-list').split('"')
    for i in range(int((len(splitted)-1)/8)):
        area_dict[splitted[i*8+3]] = splitted[i*8+7]
    area_url[area.get('data-swicher-city').split('"')[3]] = area_dict

Ce qui m'intéressait en chemin était len (area_url) = 47 pour len (a) = 53. Quand je l'ai recherché, la cause était que Tokyo, Kanagawa, Aichi, Osaka, Kyoto et Fukuoka sont apparus deux fois, mais le contenu était le même pour la partie que je voulais, alors j'ai dit que le code ci-dessus atteignait l'objectif. Cela a été jugé. L'URL peut être obtenue sous la forme suivante.

area_url
  │
  ├──'Tokyo'
  │    ├──'Ueno / Asakusa / Nippori' : '/tokyo/A1311/'
  │    ├──'Ryogoku, Kinshicho, Koiwa' : '/tokyo/A1312/'
  │          ⋮
  │    └──'Ginza / Shimbashi / Yurakucho' : '/tokyo/A1301/'
  │
  ├──'Kanagawa'
  │    ├──'Autour d'Odawara' : '/kanagawa/A1409/'
  │          ⋮
  ⋮          

Maintenant que les zones principales ont été acquises, l'étape suivante consiste à acquérir les catégories mineures. De la même manière que pour obtenir une catégorie majeure,

url = '/tokyo/A1304/'
res = requests.get(root_url + url[1:])
soup = BeautifulSoup(res.content, 'html.parser')
a = soup.find_all('a', class_='c-link-arrow')
area_dict = {}
for area in a:
    href = area['href']
    if href[-21:-8]!=url:
        continue
    else:
        area_dict[area.text] = href

Si tu fais

{'Yoyogi': 'https://tabelog.com/tokyo/A1304/A130403/',
 'Okubo / Nouvel Okubo': 'https://tabelog.com/tokyo/A1304/A130404/',
 'Shinjuku': 'https://tabelog.com/tokyo/A1304/A130401/',
 'Shinjuku Gyoen': 'https://tabelog.com/tokyo/A1304/A130402/'}

Ça fait du bien. De plus, l'instruction if a été insérée car certaines publicités ont class = "c-link-arrow" lorsque soup.find_all ('a', class_ = 'c-link-arrow') est exécuté. Il s'agit de les éliminer.

Ensuite, spécifiez la zone dans laquelle vous souhaitez accéder et obtenez l'URL de cette zone.

visit_areas = ['Roppongi / Azabu / Hiroo', 'Harajuku / Omotesando / Aoyama', 'Yotsuya / Ichigaya / Iidabashi', 'Shinjuku / Yoyogi / Okubo', 
               'Tokyo / Nihonbashi', 'Shibuya / Ebisu / Daikanyama', 'Meguro / Shirokane / Gotanda', 'Akasaka / Nagatacho / Tameike', 'Ginza / Shimbashi / Yurakucho']
url_dict = {}
for visit_area in visit_areas:
    url = area_url['Tokyo'][visit_area]
    time.sleep(1)
    res = requests.get(root_url + url[1:])
    soup = BeautifulSoup(res.content, 'html.parser')
    a = soup.find_all('a', class_='c-link-arrow')
    for area in a:
        href = area['href']
        if href[-21:-8]!=url:
            continue
        else:
            url_dict[area.text] = href

Nous avons réussi à obtenir l'URL de 34 zones sous la forme suivante!

{'Marunouchi / Otemachi': 'https://tabelog.com/tokyo/A1302/A130201/',
 '9 marches plus bas': 'https://tabelog.com/tokyo/A1309/A130906/',...

4. Rachat de l'acquisition d'URL de restaurant individuel

Maintenant que vous avez l'URL qui pointe vers la zone ("// tabelog.com/tokyo/A .... / A ...... /"), l'étape suivante consiste à obtenir l'URL du restaurant individuel ("/". /tabelog.com/tokyo/A .... / A ...... / ........ / ").

url = 'https://tabelog.com/tokyo/A1302/A130201/'
res = requests.get(url)
soup = BeautifulSoup(res.content, 'html.parser')

L'URL ci-dessus affichera les 20 premiers magasins de cette zone. L'URL des 20 magasins suivants est "//tabelog.com/tokyo/A1302/A130201/rstLst/2/", et vous pouvez spécifier rstLst / 3,4,5, ... Tu peux voir ça. Étant donné que la valeur maximale de rstLst est requise pour le traitement en boucle ici, la valeur obtenue en divisant le nombre total de restaurants par 20 est arrondie à un entier comme indiqué ci-dessous.

import math
count = soup.find_all('span', class_='list-condition__count')
print(math.ceil(int(count[0].text)/20))
90

Il y a 1784 magasins au total, et s'il y a 20 magasins sur une page, la dernière page sera la 90e page. Cependant, lorsque j'ai essayé d'afficher la 90e page ...

Impossible d'afficher cette page Merci d'utiliser le journal des repas. Les pages 60 et suivantes ne peuvent pas être affichées. Veuillez affiner les conditions et effectuer une nouvelle recherche.

Est affiché! !! Pour le moment, il n'affiche que 60 pages. Ainsi, vous n'avez pas à vous soucier de savoir si vous souhaitez restreindre les conditions au-delà de la zone afin que le nombre de magasins soit de 1200 ou moins, puis exécutez le traitement en boucle, ou obtenez les 1200 premiers dans le nouvel ordre d'ouverture et soyez satisfait. Dans un mauvais sens. Alors, vérifiez une fois combien de magasins sont répertoriés dans chaque zone.

counts = {}
for key,value in url_dict.items():
    time.sleep(1)
    res = requests.get(value)
    soup = BeautifulSoup(res.content, 'html.parser')
    counts[key] = int(soup.find_all('span', class_='list-condition__count')[0].text)
print(sorted(counts.items(), key=lambda x:x[1], reverse=True)[:15])
[('Shinjuku', 5756),
 ('Shibuya', 3420),
 ('Shimbashi / Shiodome', 2898),
 ('Ginza', 2858),
 ('Roppongi / Nogizaka / Nishi Azabu', 2402),
 ('Marunouchi / Otemachi', 1784),
 ('Iidabashi / Kagurazaka', 1689),
 ('Ebisu', 1584),
 ('Nihonbashi / Kyobashi', 1555),
 ('Akasaka', 1464),
 ('Ningyocho / Kodenmacho', 1434),
 ('Gotanda / Takanawadai', 937),
 ('Yurakucho / Hibiya', 773),
 ('Tameike Sanno / Kasumigaseki', 756),
 ('Kayabacho / Hatchobori', 744)]

En conséquence, il est devenu clair que le nombre de publications dépassait 1 200 dans 11 domaines. À la suite de divers essais et erreurs, j'ai décidé d'être satisfait car j'ai limité le genre aux restaurants à la lumière de cet objectif et j'ai acquis les données des 1 200 premiers articles dans l'ordre de nouvelle ouverture. Pour le moment, obtenez les informations du restaurant affichées sur une page spécifique.

url = 'https://tabelog.com/tokyo/A1301/A130101/rstLst/RC/1/?Srt=D&SrtT=nod'
res = requests.get(url)
soup = BeautifulSoup(res.content, 'html.parser')

rc_list = []
for rc_div in soup.find_all('div', class_='list-rst__wrap js-open-new-window'):
    rc_name = rc_div.find('a', class_='list-rst__rst-name-target cpy-rst-name').text
    rc_url = rc_div.find('a', class_='list-rst__rst-name-target cpy-rst-name')['href']
    rc_score = rc_div.find('span', class_='c-rating__val c-rating__val--strong list-rst__rating-val')
    if rc_score is None:
        rc_score = -1.
    else:
        rc_score = float(rc_score.text)
    rc_review_num = rc_div.find('em', class_='list-rst__rvw-count-num cpy-review-count').text
    rc_list.append([rc_name, rc_url, rc_score, rc_review_num])

J'ajouterai quelques explications. Tout d'abord, pour la première URL, «/ rstLst / RC» limite le genre aux restaurants. Le '/ 1' qui vient après cela signifie la première page, c'est-à-dire les 20 premiers cas. De plus, «/? Srt = D & SrtT = nod» est une nouvelle spécification d'ordre ouvert. Dans la déclaration for, 20 données de restaurant sont traitées dans l'ordre. C'est le score du journal alimentaire qui nécessite une attention. Vous pouvez obtenir le score avec la méthode de recherche ci-dessus, mais s'il n'y a pas de score, cette balise elle-même n'existe pas. Par conséquent, le cas a été divisé par Aucun dans l'instruction if, et s'il n'y avait pas de score, un processus a été ajouté pour définir temporairement le score à -1. En ce qui concerne le nombre d'avis, s'il n'y a pas d'avis, «-» sera retourné. Après cela, vous pouvez obtenir l'URL du restaurant en tournant la boucle pour chaque zone et chaque page! !!

5. Raclage du bouche-à-oreille et acquisition d'évaluation

Maintenant que vous pouvez obtenir l'URL de chaque restaurant, le but est d'obtenir des informations de bouche à oreille à partir de la page de chaque restaurant. Quand j'ai mis le code en premier, j'ai fait ce qui suit.

url = 'https://tabelog.com/tokyo/A1301/A130101/13079232/dtlrvwlst/COND-2/smp0/?smp=0&lc=2&rvw_part=all&PG=1'
res = requests.get(url)
soup = BeautifulSoup(res.content, 'html.parser')

station = soup.find_all('dl', class_='rdheader-subinfo__item')[0].find('span', class_='linktree__parent-target-text').text
genre = '/'.join([genre_.text for genre_ in soup.find_all('dl', class_='rdheader-subinfo__item')[1].find_all('span', class_='linktree__parent-target-text')])
price = soup.find_all('dl', class_='rdheader-subinfo__item')[2].find('p', class_='rdheader-budget__icon rdheader-budget__icon--dinner').find('a', class_='rdheader-budget__price-target').text
score = [score_.next_sibling.next_sibling.text for score_ in soup.find_all('span', class_='c-rating__time c-rating__time--dinner')]
print(station, genre, price, score)

J'ajouterai un commentaire. Tout d'abord, en ce qui concerne l'url, «/ dtlrvwlst» est une liste de révision, «/ COND-2» est une critique de nuit, «smp0» est un affichage simple, «lc = 2» est un affichage de 100 éléments, et «PG = 1» est 1 Cela signifie la page. La station, le genre et le budget les plus proches sont acquis dans l'ordre car il y a des données dans le'dl'tag dont le nom de classe est'rdheader-subinfo__item '. En ce qui concerne les genres, dans la plupart des cas, plusieurs genres sont attribués à un magasin, donc ici, tous les noms de genre sont combinés une fois avec '/'. Le budget et le score de chaque bouche à oreille sont un peu compliqués car je ne voulais que celui du soir.

6. Courez! !!

Maintenant que nous pouvons obtenir les informations nécessaires individuellement, nous avons juste besoin d'obtenir les données que nous voulons par traitement en boucle! !!

import numpy as np
import pandas as pd
import requests
from bs4 import BeautifulSoup
import math
import time
from tqdm import tqdm

root_url = 'https://tabelog.com/'
res = requests.get(root_url)
soup = BeautifulSoup(res.content, 'html.parser')
a = soup.find_all('a', class_='rsttop-area-search__target js-area-swicher-target')

area_url = {}
for area in a:
    area_dict = {}
    splitted = area.get('data-swicher-area-list').split('"')
    for i in range(int((len(splitted)-1)/8)):
        area_dict[splitted[i*8+3]] = splitted[i*8+7]
    area_url[area.get('data-swicher-city').split('"')[3]] = area_dict

visit_areas = ['Shibuya / Ebisu / Daikanyama']
url_dict = {}
for visit_area in visit_areas:
    url = area_url['Tokyo'][visit_area]
    time.sleep(1)
    res = requests.get(root_url + url[1:])
    soup = BeautifulSoup(res.content, 'html.parser')
    a = soup.find_all('a', class_='c-link-arrow')
    for area in a:
        href = area['href']
        if href[-21:-8]!=url:
            continue
        else:
            url_dict[area.text] = href

max_page = 20
restaurant_data = []
for area, url in url_dict.items():
    time.sleep(1)
    res_area = requests.get(url)
    soup_area = BeautifulSoup(res_area.content, 'html.parser')
    rc_count = int(soup_area.find_all('span', class_='list-condition__count')[0].text)
    print('There are ' + str(rc_count) + ' restaurants in ' + area)
    for i in tqdm(range(1,min(math.ceil(rc_count/20)+1,max_page+1,61))):
        url_rc = url + 'rstLst/RC/' + str(i) + '/?Srt=D&SrtT=nod'
        res_rc = requests.get(url_rc)
        soup_rc = BeautifulSoup(res_rc.content, 'html.parser')
        for rc_div in soup_rc.find_all('div', class_='list-rst__wrap js-open-new-window'):
            rc_name = rc_div.find('a', class_='list-rst__rst-name-target cpy-rst-name').text
            rc_url = rc_div.find('a', class_='list-rst__rst-name-target cpy-rst-name')['href']
            rc_score = rc_div.find('span', class_='c-rating__val c-rating__val--strong list-rst__rating-val')
            if rc_score is None:
                rc_score = -1.
            else:
                rc_score = float(rc_score.text)
            rc_review_num = rc_div.find('em', class_='list-rst__rvw-count-num cpy-review-count').text
            if rc_review_num != ' - ':
                for page in range(1,math.ceil(int(rc_review_num)/100)+1):
                    rc_url_pg = rc_url + 'dtlrvwlst/COND-2/smp0/?smp=0&lc=2&rvw_part=all&PG=' + str(page)
                    time.sleep(1)
                    res_pg = requests.get(rc_url_pg)
                    soup_pg = BeautifulSoup(res_pg.content, 'html.parser')
                    if 'Je ne trouve pas la page que je recherche' in soup_pg.find('title').text:
                        continue
                    try:
                        station = soup_pg.find_all('dl', class_='rdheader-subinfo__item')[0].find('span', class_='linktree__parent-target-text').text
                    except:
                        try:
                            station = soup_pg.find_all('dl', class_='rdheader-subinfo__item')[0].find('dd', class_='rdheader-subinfo__item-text').text.replace('\n','').replace(' ','')
                        except:
                            station = ''
                    genre = '/'.join([genre_.text for genre_ in soup_pg.find_all('dl', class_='rdheader-subinfo__item')[1].find_all('span', class_='linktree__parent-target-text')])
                    price = soup_pg.find_all('dl', class_='rdheader-subinfo__item')[2].find('p', class_='rdheader-budget__icon rdheader-budget__icon--dinner').find('a', class_='rdheader-budget__price-target').text
                    score = [score_.next_sibling.next_sibling.text for score_ in soup_pg.find_all('span', class_='c-rating__time c-rating__time--dinner')]
                    restaurant_data.append([area, rc_count, rc_name, rc_url, rc_score, rc_review_num, station, genre, price, score])

En raison de la grande quantité de données, la zone a été temporairement limitée à visit_areas = ['Shibuya / Ebisu / Daikanyama']. De plus, puisque max_page = 20, seules 400 données de 20 éléments / page✖️20 (max_page) peuvent être acquises pour chaque emplacement. De plus, après avoir acquis le nombre d'avis, 100 avis / page sont acquis, mais le nombre d'avis acquis en premier inclut l'évaluation de jour et le traitement en boucle ne cible que l'évaluation de nuit. Par conséquent, dans le processus de traitement en boucle, il y avait une situation où il n'y avait pas autant d'examens que prévu initialement.

if 'Je ne trouve pas la page que je recherche' in soup_pg.find('title').text:
    continue

Il a été traité en. De plus, la plupart des magasins indiquent la gare la plus proche, mais il y a quelques magasins qui répertorient la zone au lieu de la gare la plus proche, auquel cas les noms des balises sont différents, donc

try:
    station = soup_pg.find_all('dl', class_='rdheader-subinfo__item')[0].find('span', class_='linktree__parent-target-text').text
except:
    try:
        station = soup_pg.find_all('dl', class_='rdheader-subinfo__item')[0].find('dd', class_='rdheader-subinfo__item-text').text.replace('\n','').replace(' ','')
    except:
        station = ''

Gère cela. En conséquence, nous avons pu obtenir 895 données. Parmi ceux-ci, 804 avaient même un avis noté.

save.png

Le diagramme de dispersion ci-dessus montre la relation entre l'évaluation moyenne du bouche-à-oreille et le score du journal alimentaire. Les magasins qui n'ont pas de score dans le journal alimentaire sont traités comme 2,9 points. Dans l'ensemble, si l'évaluation moyenne du bouche à oreille est élevée, vous pouvez voir que le score du journal alimentaire est également élevé. Bien que l'évaluation moyenne du bouche-à-oreille soit élevée, le score du journal de consommation était faible et il a également été constaté que certains magasins sont sous-estimés.

Alors, je vais aller dans l'une de ces boutiques sous-estimées la prochaine fois! !!

Recommended Posts

Gratter et manger des bûches - je veux trouver un bon restaurant! ~ (Travail)
Je veux trouver facilement une délicieuse boutique
Je veux trouver un package populaire sur PyPi
Je veux travailler avec un robot en python.
Je veux trouver l'intersection d'une courbe de Bézier et d'une ligne droite (méthode de découpage de Bézier)
Je souhaite enregistrer l'heure d'exécution et conserver un journal.
Je souhaite acquérir et répertorier les données boursières japonaises sans grattage
Je veux créer un fichier pip et le refléter dans le menu fixe
J'ai écrit un script pour aider goodnotes5 et Anki à travailler ensemble
Je veux imprimer dans la notation d'inclusion
Je veux créer un environnement Python
J'étais frustré par Kaggle, alors j'ai essayé de trouver une bonne propriété locative en grattant et en apprentissage automatique
J'ai essayé de vérifier la meilleure façon de trouver un bon partenaire de mariage
Je souhaite créer une application Web en utilisant React et Python flask
Je veux faire de matplotlib un thème sombre
Grattage de la nourriture avec python et sortie en CSV
Je veux INSÉRER un DataFrame dans MSSQL
Je veux créer une fenêtre avec Python
Je veux faire un jeu avec Python
Je ne veux pas passer un test de codage
Je souhaite créer un type d'implémentation pouvant être branché
Je souhaite vendre les produits que j'ai listés par python scraping Mercari
Je veux écrire dans un fichier avec Python
Je souhaite télécharger une application Django sur heroku
Je veux déposer un fichier sur tkinter et obtenir son chemin [Tkinter DnD2]
Je veux créer un lecteur de musique et enregistrer de la musique en même temps
Je veux écrire un élément dans un fichier avec numpy et le vérifier.
Je souhaite intégrer une variable dans une chaîne Python
Je veux facilement implémenter le délai d'expiration en python
Je veux répéter plusieurs fois un générateur Python
Je veux que DQN Puniki frappe un home run
100 coups sur le traitement d'image !! (021-030) Je veux faire une pause ...
Je veux donner un group_id à une trame de données pandas
Je veux générer rapidement UUID (memo memo) ~ Edition Python ~
Je veux faire la transition avec un bouton sur le ballon
Je veux escalader une montagne avec l'apprentissage par renforcement
Je veux écrire en Python! (2) Écrivons un test
Je veux échantillonner au hasard un fichier avec Python
Je souhaite créer facilement un environnement de développement basé sur un modèle
Je veux diviser une chaîne de caractères avec hiragana
Je souhaite installer un package de Php Redis
[Python] Je veux faire d'une liste imbriquée un taple
Je souhaite créer manuellement une légende avec matplotlib
Je souhaite envoyer automatiquement un e-mail de création d'entreprise
Je veux faire fonctionner un ordinateur quantique avec Python
Je veux lier une variable locale avec lambda
Bon code et mauvais code à comparer avec la mini-carte
Je souhaite créer une source sonore de karaoké en séparant les instruments et les voix en utilisant Python
Je veux faire un changeur de voix en utilisant Python et SPTK en référence à un site célèbre
Je veux créer un éditeur de blog avec l'administrateur de django
Je veux démarrer un environnement Jupyter avec une seule commande
Je veux démarrer beaucoup de processus à partir de python
Je veux faire une macro de clic avec pyautogui (désir)
Je veux faire une macro de clic avec pyautogui (Outlook)
J'ai essayé de ramper et de gratter le site de courses de chevaux Partie 2
Je souhaite utiliser un environnement virtuel avec jupyter notebook!
Je veux installer le package de requirements.txt avec poésie
Je veux connaître la nature de Python et pip