[PYTHON] Visualisons le fil 2ch avec WordCloud-Scraping-

introduction

2ch est un babillard anonyme bien connu et possède une énorme quantité d'informations. Cependant, si vous lisez moins chaque fil, cela prendra énormément de temps pour avoir une vue d'ensemble. Par conséquent, j'ai essayé de ** visualiser ces informations avec WordCloud ** et d'obtenir facilement une vue d'ensemble.

  • Ce qui précède est le résultat de la recherche de thread avec "FFRK", qui est la sortie des 8 derniers mois de leçons dans WordCloud. *
  • Cela fait environ un an que le Mystère Synchro a été implémenté, mais on peut voir que le Mystère de l'Éveil est encore plus parlé. En plus de Deshi Urara de FFRK Ori-chara, on parle relativement de Butts, Edge, Cloud, Mog, etc. Peut être attendu. *

Je suis un débutant en scraping et en traitement du langage naturel, mais j'aimerais en parler car il a pris forme à ma manière. Cette fois, dans la première partie, nous collecterons des informations de fil à moins de contenu par scraping Web.

Flux global

  1. [Scraping "log speed" pour extraire l'URL du thread cible](#### [Scraping the thread list from "log speed"]) ← Explication cette fois
  2. [Raclage du fil 2 canaux pour en extraire moins](#### [Raclage du fil 2 canaux]) ← Explication cette fois
  3. Analyse morphologique du contenu le moins extrait avec Mecab
  4. Sortie avec WordCloud

Code complet

Cliquez pour afficher le texte intégral (y compris le traitement autre que le scraping)
#Importer la bibliothèque
import requests, bs4
import re
import time
import pandas as pd
from urllib.parse import urljoin

#Installer les polices localement dans Colab
from google.colab import drive
drive.mount("/content/gdrive")
#Créez à l'avance un dossier appelé police en haut de Mon Drive sur votre Google Drive et placez-y le fichier de police souhaité.
#Copiez chaque dossier localement dans Colab
!cp -a "gdrive/My Drive/font/" "/usr/share/fonts/"

# ------------------------------------------------------------------------
#Préparation
log_database = []  #Une liste qui stocke les informations de fil
base_url = "https://www.logsoku.com/search?q=FFRK&p="

#Mise en place du web scraping
for i in range(1,4):  #À quelle page revenir (ici, provisoirement jusqu'à la 4e page)
  logs_url = base_url+str(i)

  #Corps de traitement de grattage
  res = requests.get(logs_url)
  soup = bs4.BeautifulSoup(res.text, "html.parser")

  #Que faire lorsqu'aucun résultat de recherche n'est trouvé
  if soup.find(class_="search_not_found"):break

  #Récupère la table / ligne où les informations sur les threads sont stockées
  thread_table = soup.find(id="search_result_threads")
  thread_rows = thread_table.find_all("tr")

  #Traitement pour chaque ligne
  for thread_row in thread_rows:
    tmp_dict = {}
    tags = thread_row.find_all(class_=["thread","date","length"])

    #Organisez le contenu
    for tag in tags:
      if "thread" in str(tag):
        tmp_dict["title"] = tag.get("title")
        tmp_dict["link"] = tag.get("href")
      elif "date" in str(tag):
        tmp_dict["date"] = tag.text
      elif "length" in str(tag):
        tmp_dict["length"] = tag.text

    #Seuls ceux qui ont plus de 50 leçons seront ajoutés à la base de données
    if tmp_dict["length"].isdecimal() and int(tmp_dict["length"]) > 50:
      log_database.append(tmp_dict)

  time.sleep(1)

#Convertir en DataFrame
thread_df = pd.DataFrame(log_database)

# ------------------------------------------------------------------------
#Obtenez moins des journaux passés
log_url_base = "http://nozomi.2ch.sc/test/read.cgi/"
res_database = []

for thread in log_database:
  #Nom du tableau et numéro du tableau d'affichage de la liste des journaux précédents.Et générer l'URL du journal passé
  board_and_code_match = re.search("[a-zA-Z0-9_]*?/[0-9]*?/$",thread["link"])
  board_and_code = board_and_code_match.group()
  thread_url = urljoin(log_url_base, board_and_code)

  #Extraire le code HTML de la page de journal précédente
  res = requests.get(thread_url)
  soup = bs4.BeautifulSoup(res.text, "html5lib")

  tmp_dict = {}
  #Informations telles que la date dans la balise dt
  #Le commentaire est stocké dans la balise dd
  dddt = soup.find_all(["dd","dt"])

  for tag in dddt[::-1]:  #Extrait par derrière

    #Extraire uniquement la date de la balise dt
    if "<dt>" in str(tag):
      date_result = re.search(r"\d*/\d*/\d*",tag.text)  #  "(←'"'Pour éviter les anomalies d'affichage de qiita)
      if date_result:
        date_str = date_result.group()
        tmp_dict["date"] = date_str

    #Extraire moins de contenu de la balise dd
    if "<dd>" in str(tag):
      tmp_dict["comment"] = re.sub("\n","",tag.text)

    # tmp_Le contenu stocké dans dict est res_Publier dans la base de données
    if "date" in tmp_dict and "comment" in tmp_dict:
      tmp_dict["thread_title"] = thread["title"]
      res_database.append(tmp_dict)
      tmp_dict = {}

  time.sleep(1)  #promettre

#Convertir en DataFrame
res_df = pd.DataFrame(res_database)

# ------------------------------------------------------------------------

#Bibliothèque d'analyse morphologique MeCab et dictionnaire(mecab-ipadic-NEologd)Installation de
!apt-get -q -y install sudo file mecab libmecab-dev mecab-ipadic-utf8 git curl python-mecab > /dev/null
!git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git > /dev/null 
!echo yes | mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n > /dev/null 2>&1
!pip install mecab-python3 > /dev/null

#Éviter les erreurs avec les liens symboliques
!ln -s /etc/mecabrc /usr/local/etc/mecabrc

#Installation de Wordcloud
!pip install wordcloud

#Combinez tout le moins
sentences = ",".join(res_df["comment"])
sentences_sep = []
n = 10000
for i in range(0,len(sentences), n):
  sentences_sep.append(sentences[i:i + n])

#Les n(=1000)Séparer par moins et combiner par des virgules
#Le but du délimiteur est que le dernier mécab ne peut pas gérer trop de caractères.
sentences_sep = []
n = 1000
for i in range(0, len(res_df["comment"]), n):
  sentences_sep.append(",".join(res_df["comment"][i: i + n]))

# ------------------------------------------------------------------------
import MeCab

# mecab-ipadic-Spécifiez le chemin où le dictionnaire neologd est stocké
path = "-d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd"
#Au-dessus du chemin (/usr/~) Peut être obtenu avec la commande suivante
# !echo `mecab-config --dicdir`"/mecab-ipadic-neologd"

#Créer un objet Tagger
mecab = MeCab.Tagger(path)

#Effectuer une analyse morphologique pour chaque groupe séparé
chasen_list = [mecab.parse(sentence) for sentence in sentences_sep]

word_list = []

# chasen_Décomposer la liste en une seule ligne
# ex.Géant de fer,Nomenclature propriétaire,Général,*,*,*,Géant de fer,Tetsukyojin,Tetsukyojin)
for chasen in chasen_list:
  for line in chasen.splitlines():
    
    if len(line) <= 1: break

    speech = line.split()[-1]
    if "nom" in speech:
      if  (not "Non indépendant" in speech) and (not "Synonyme" in speech) and (not "nombre" in speech):
        word_list.append(line.split()[0])

word_line = ",".join(word_list)

# ------------------------------------------------------------------------
from wordcloud import WordCloud
import matplotlib.pyplot as plt
#Il est nécessaire d'installer au préalable les polices localement dans Colab.
f_path = "BIZ-UDGothicB.ttc"
stop_words = ["https","imgur","net","jpg","com","alors"]

wordcloud = WordCloud(
    font_path=f_path,
    width=1024, height=640,   # default width=400, height=200
    background_color="white",   # default=”black”
    stopwords=set(stop_words),
    max_words=350,   # default=200
    max_font_size=200,   #default=4
    min_font_size=5,   #default=4
    collocations = False   #default = True
    ).generate(word_line)
plt.figure(figsize=(18,15))
plt.imshow(wordcloud)
plt.axis("off") #Masquer la mémoire
plt.show()

Environnement ~ Google Colaboratory ~

** Utilisez Google Colaboratory ** Google Colaboratory est un environnement d'exécution Python sur un navigateur que tout le monde peut utiliser tant qu'il dispose d'un compte Google. Il est souvent utilisé dans des situations d'apprentissage automatique car il peut utiliser un GPU puissant, mais il est également recommandé si vous voulez vous sentir libre de gratter car ** vous n'avez pas besoin d'installer une bibliothèque si vous ne faites que gratter **. (Une installation supplémentaire est requise pour Mecab, WordCloud, etc., qui sera expliquée la prochaine fois) Consultez l'article ci-dessous pour savoir comment utiliser Google Colaboratory ⇒ Résumé de l'utilisation de Google Colab (Je ne l'ai pas essayé, mais je pense que ce n'est pas grave si ce n'est pas Colab si vous installez la bibliothèque, etc.)

Commentaire

Bases du grattage

J'obtiens un objet de page Web avec requests.get () et je l'analyse avec HTML avec bs4.BeautifulSoup (). L'entrée du grattage. "Html.parser" est une spécification d'analyseur. Pour la vitesse de journalisation, "html.parser" (couramment utilisé) convient, mais ** 2ch utilise un analyseur appelé "html5lib" </ font> **. (Parce que "html.parser" ne parvient pas à analyser pour une raison quelconque)

  #Corps de traitement de grattage
  res = requests.get(logs_url)
  soup = bs4.BeautifulSoup(res.text, "html.parser")
promettre

Lorsque vous accédez à un site Web plusieurs fois par grattage, insérez «time.sleep (1)» entre les répétitions afin de ne pas surcharger le serveur.

[Suppression de la liste des fils de "vitesse du journal"]

Recherchez les threads (y compris les threads actuels) qui incluent n'importe quel mot-clé de "vitesse du journal" et extrayez les résultats. Vitesse du journal: https://www.logsoku.com/

Obtenez l'URL à gratter

Lorsque j'ai essayé de rechercher sur le site ci-dessus, il s'est avéré qu'il s'agissait d'une URL comme celle-ci. Recherche par "FFRK": https://www.logsoku.com/search?q=FFRK 2ème pages et suivantes: https://www.logsoku.com/search?q=FFRK&p=2

À partir de l'URL des résultats de la recherche, nous avons trouvé ce qui suit. ・ L'URL de https://www.logsoku.com/search? Avec q = attaché est la page de résultats de recherche (q est la quête). q?) ・ Vous pouvez accéder directement à chaque page du résultat de la recherche avec p = (p est p de la page?) ・ La première page peut être affichée même avec p = 1. ・ Les pages sans résultats de recherche sont accessibles

D'après ce qui précède, il semble que l'URL de la page à accéder puisse être obtenue en tournant l'instruction for. Heureusement, vous pouvez accéder à la page elle-même même si la page de recherche n'est pas valide. Il existe une classe appelée "search_not_found" sur les pages sans résultats de recherche, alors utilisez-la pour juger.

base_url = "https://www.logsoku.com/search?q=FFRK&p="
for i in range(1,100):
  logs_url = base_url+str(i)

  #Effectuer un grattage
  res = requests.get(logs_url)
  soup = bs4.BeautifulSoup(res.text, "html.parser")

  #Que faire lorsqu'aucun résultat de recherche n'est trouvé
  if soup.find(class_="search_not_found"):break
 :
(Traitement pour chaque page)
 :
Scraping pour chaque page de recherche

Avant de procéder au scraping, regardez le code HTML de la page cible et réfléchissez à la manière de le traiter. À ce moment-là, les ** «outils de développement» ** fournis dans chaque navigateur sont utiles. Appuyez sur F12 dans le navigateur pour le démarrer. Si vous cliquez sur ** "Sélectionner un élément de la page" ** dans cet état, vous pourrez voir où dans le HTML vous pointez lorsque vous regardez le curseur n'importe où sur la page Web.

En regardant le fil 2ch avec l'outil de développement, j'ai trouvé les points suivants. -Toutes les informations nécessaires sont stockées sous div # search_result_threads. -Les informations pour un thread sont stockées dans chaque balise tr. -Le titre et le lien du fil sont stockés dans a.thred dans la balise tr. -Le nombre de threads est stocké dans td.length dans la balise tr. -La date et l'heure de mise à jour du fil sont stockées dans td.date dans la balise tr.

Sur cette base, le grattage est effectué comme suit. Les threads avec moins de 50 threads sont omis car il existe une forte possibilité de duplication. Il est facile de comprendre si le résultat de l'extraction est stocké dans un type de dictionnaire, et il peut être apprécié lors de la conversion au format DataFrame décrit plus loin.

  #Récupère la table / ligne où les informations sur les threads sont stockées
  thread_table = soup.find(id="search_result_threads")
  thread_rows = thread_table.find_all("tr")

  #Traitement pour chaque ligne
  for thread_row in thread_rows:
    tmp_dict = {}
    tags = thread_row.find_all(class_=["thread","date","length"])

    #Organisez le contenu
    for tag in tags:
      if "thread" in str(tag):
        tmp_dict["title"] = tag.get("title")
        tmp_dict["link"] = tag.get("href")
      elif "date" in str(tag):
        tmp_dict["date"] = tag.text
      elif "length" in str(tag):
        tmp_dict["length"] = tag.text

    #Seuls ceux qui ont plus de 50 leçons seront ajoutés à la base de données
    if tmp_dict["length"].isdecimal() and int(tmp_dict["length"]) > 50:
      log_database.append(tmp_dict)

  time.sleep(1)

Pour le moment, j'ai pu obtenir les informations de fil 2ch que j'ai recherchées. Convertissez-le au format pandas DataFrame afin qu'il puisse être facilement manipulé plus tard.

thread_df = pd.DataFrame(log_database)  #conversion

afficher

thread_df
Tout d'abord, j'ai pu extraire la liste des fils.

[Grattage des fils 2 canaux]

Sur la base des informations de thread acquises ci-dessus, le contenu du thread est extrait. Dans 2ch, spécifiez l'URL du fil comme suit. "Http://nozomi.2ch.sc/test/read.cgi/" + "code du tableau /" + "babillard n ° / " Le code du tableau et le numéro du tableau d'affichage sont les expressions régulières re.search (" [a-zA-Z0-9_] *? / [0-9] *? / $ ", Thread [" à partir du lien obtenu par le grattage ci-dessus. Extrait en utilisant le lien "].

#Obtenez moins des journaux passés
log_url_base = "http://nozomi.2ch.sc/test/read.cgi/"
res_database = []

for thread in log_database:
  #Nom du tableau et numéro du tableau d'affichage de la liste des journaux précédents.Et générer l'URL du journal passé
  board_and_code_match = re.search("[a-zA-Z0-9_]*?/[0-9]*?/$",thread["link"])
  board_and_code = board_and_code_match.group()  #Convertir les résultats des objets d'expression régulière
  thread_url = urljoin(log_url_base, board_and_code)
 :
(Traitement pour chaque fil)
 :
Acquisition de moins de contenu / date de publication

Utilisez l'outil de développement de navigateur (F12) ainsi que la vitesse de journalisation pour étudier les tendances des pages de scraping. Je pourrais attraper cette tendance sur 2ch. ・ Un de moins avec un ensemble de balises dd et dt. ・ Les balises dd et dt sont peu susceptibles d'être utilisées ailleurs que Les. ・ En plus de la date et de l'heure de la réponse, la balise dd comprend également des informations telles que le fer et l'ID. -Le contenu de la réponse est stocké dans la balise dt.

Sur la base de ceux-ci, le grattage a été effectué comme suit. Comme mentionné ci-dessus **, l'analyseur utilise "html5lib" **.

  #Extraire le code HTML de la page de journal précédente
  res = requests.get(thread_url)
  soup = bs4.BeautifulSoup(res.text, "html5lib")  #Utilisez html5lib pour 2 canaux

  tmp_dict = {}
  #Informations telles que la date dans la balise dt
  #Moins le contenu est stocké dans la balise dd
  dddt = soup.find_all(["dd","dt"])

  for tag in dddt[::-1]:  #Extrait par derrière

    #Extraire uniquement la date de la balise dt
    if "<dt>" in str(tag):
      date_result = re.search(r"\d*/\d*/\d*",tag.text)  #  "(←'"'Pour éviter les anomalies d'affichage de qiita)
      if date_result:
        date_str = date_result.group()
        tmp_dict["date"] = date_str

    #Extraire moins de contenu de la balise dd
    if "<dd>" in str(tag):
      tmp_dict["comment"] = re.sub("\n","",tag.text)

    # tmp_Le contenu stocké dans dict est res_Publier dans la base de données
    if "date" in tmp_dict and "comment" in tmp_dict:
      tmp_dict["thread_title"] = thread["title"]
      res_database.append(tmp_dict)
      tmp_dict = {}

  time.sleep(1)  #promettre

En tant que flux, nous avons d'abord extrait les balises dd et les balises dt de manière confuse, les avons décomposées une par une, jugé s'il s'agissait de balises dt ou dd, et les avons stockées dans un type de dictionnaire. (En fait, la balise dd et la balise dt extraites ensemble sont disposées régulièrement et le jugement n'est pas nécessaire. Cependant, j'ai essayé de porter un jugement afin qu'il puisse être traité lorsqu'un autre motif se produit)

Convertir en DataFrame
res_df = pd.DataFrame(res_database)

afficher

res_df

J'ai pu extraire en toute sécurité les informations sur les leçons 2ch et la date d'affichage.

Plans futurs

La prochaine fois, après avoir effectué une analyse morphologique sur le contenu extrait, il sera envoyé vers WordCloud.

Recommended Posts

Visualisons le fil 2ch avec WordCloud-Scraping-
Visualisons le fil 2ch avec WordCloud-analyse morphologique / édition WordCloud-
Visualisez rapidement avec les pandas
Visualisez les réclamations avec l'IA
Visualisez le nem 2019 avec WordCloud
Visualisez les informations de localisation avec Basemap
Visualisons la connaissance de Wikidata avec Neo4j
Visualisez les dépendances des packages python avec graphviz
[Tkinter] Contrôler les threads avec l'objet Event