首页 > 解决方案 > 整个页面没有被 Beautiful Soup 解析

问题描述

我可以从我想要的网站检索前 19 条记录之后的任何内容。鉴于网站上的列表是动态的,我相信这可能与我运行 python 代码时只返回前 19 个有关。我在网上做了一些阅读,但还没有找到解决我问题的方法。

Bellow 是我的完整 python 代码。我很乐意让社区就我可以做些什么来解决我的问题提出意见。

import requests
from requests import get
from bs4 import BeautifulSoup
import pandas as pd
import numpy as np
import csv


headers = {"Accept-Language": "en-US, en;q=0.5"}
url = "https://www.producthunt.com/time-travel/2019/1/7"
results = requests.get(url, headers=headers) 



soup = BeautifulSoup(results.text, "html.parser")



name = []
description = []
category = []
up_votes = []


ph_project_div = soup.find_all('div', class_='item_54fdd')



for container in ph_project_div:
    
    ph_name = container.a.find('h3', class_='font_9d927 medium_51d18 semiBold_e201b title_9ddaf lineHeight_042f1 underline_57d3c').text
    name.append(ph_name)

    ph_desc = container.a.find('p', class_='font_9d927 grey_bbe43 small_231df normal_d2e66 tagline_619b7 lineHeight_042f1 underline_57d3c').text
    description.append(ph_desc)
    
    
    ph_cat = container.find('span', class_='font_9d927 grey_bbe43 xSmall_1a46e lineHeight_042f1 underline_57d3c')
    category.append(ph_cat)
    
    ph_vote = container.find('span', class_='font_9d927 small_231df semiBold_e201b lineHeight_042f1 underline_57d3c')
    up_votes.append(ph_vote)
    


phunt = pd.DataFrame({
    'Product Name': name,
    'Product Description': description,
    'Product Category': category,
    'Product Votes': up_votes,
})



phunt.to_csv("ph_01_04_2019.csv")

标签: pythonweb-scrapingscreen-scraping

解决方案


所以你是正确的,javascript 被用来获取下一组帖子。这是触发对服务器的 HTTP 请求的滚动事件的示例。

要查看这一点,您可以使用检查页面 --> 网络工具 --> XHR。清除所有请求并向下滚动。您将看到一个发布请求,并且在右侧您将看到数据。我倾向于搜索我希望看到的东西来确认数据是否存在。

有两种方法可以处理这种类型的动态内容。

  1. 模仿 HTTP 请求
  2. 使用浏览器活动来获取数据

第一个总是更好的选择,如果有潜在的 API 点等。那么最好使用它。数据的效率和准确性很重要。

第二种选择依赖于像 selenium 这样速度较慢的包,对 HTML 的更改可能非常脆弱,对代码来说很烦人。

为了收集以下代码的信息,我复制了在网络工具的 XHR 部分中找到的请求的 cURL。我使用一个将其转换为 python 格式的网站,例如 curl.trillworks.com。

HTTP 请求可能只需要对端点的正确 url 进行简单的 get 请求,在您必须输入 headers/params/data 之前先这样做总是好的。在这个例子中,你得到了所有三个,但实际上你只需要一个用户代理和你想要的响应对象的类型以及用于引导你获得正确响应的数据。

代码示例

import requests

headers = {
    'User-Agent': 'M',
    'content-type': 'application/json',
}

data = '{"operationName":"Posts","variables":{"year":2019,"month":1,"day":7,"cursor":"MjA=","includeLayout":false},"query":"query Posts($year: Int, $month: Int, $day: Int, $cursor: String) {\\n posts(first: 20, year: $year, month: $month, day: $day, after: $cursor) {\\n edges {\\n node {\\n id\\n ...PostItemList\\n __typename\\n }\\n __typename\\n }\\n pageInfo {\\n endCursor\\n hasNextPage\\n __typename\\n }\\n __typename\\n }\\n}\\n\\nfragment PostItemList on Post {\\n id\\n ...PostItem\\n __typename\\n}\\n\\nfragment PostItem on Post {\\n id\\n _id\\n comments_count\\n name\\n shortened_url\\n slug\\n tagline\\n updated_at\\n ...CollectButton\\n ...PostThumbnail\\n ...PostVoteButton\\n ...TopicFollowButtonList\\n __typename\\n}\\n\\nfragment CollectButton on Post {\\n id\\n name\\n isCollected\\n __typename\\n}\\n\\nfragment PostThumbnail on Post {\\n id\\n name\\n thumbnail {\\n id\\n media_type\\n ...MediaThumbnail\\n __typename\\n }\\n ...PostStatusIcons\\n __typename\\n}\\n\\nfragment MediaThumbnail on Media {\\n id\\n image_uuid\\n __typename\\n}\\n\\nfragment PostStatusIcons on Post {\\n name\\n product_state\\n __typename\\n}\\n\\nfragment PostVoteButton on Post {\\n _id\\n id\\n featured_at\\n updated_at\\n disabled_when_scheduled\\n has_voted\\n ... on Votable {\\n id\\n votes_count\\n __typename\\n }\\n __typename\\n}\\n\\nfragment TopicFollowButtonList on Topicable {\\n id\\n topics {\\n edges {\\n node {\\n id\\n ...TopicFollowButton\\n __typename\\n }\\n __typename\\n }\\n __typename\\n }\\n __typename\\n}\\n\\nfragment TopicFollowButton on Topic {\\n id\\n slug\\n name\\n isFollowed\\n ...TopicImage\\n __typename\\n}\\n\\nfragment TopicImage on Topic {\\n name\\n image_uuid\\n __typename\\n}\\n"}'
response = requests.post('https://www.producthunt.com/frontend/graphql', headers=headers, data=data)
data = response.json()
for a in response.json()['data']['posts']['edges']:
    name = a['node']['name']
    subtext = a['node']['tagline']
    votes = a['node']['votes_count']
    category = a['node']['topics']['edges'][0]['node']['name']
    print('-'*20)
    print('Name: ',name)
    print('Tagline: ',subtext)
    print('Category: ',category)
    print('Votes: ',votes)

输出

--------------------
Name:  Newbook Models
Tagline:  Find & book fashion models online
Category:  Productivity
Votes:  57
--------------------
Name:  3Leaf Edibles - Quinoa Granola Bite
Tagline:  Vegan, low dose, high quality edibles.
Category:  Health and Fitness
Votes:  57
--------------------
Name:  Payfacile
Tagline:  We simplify access to online payment.
Category:  Fintech
Votes:  43
--------------------
Name:  Halo by Motorola
Tagline:  Watch over your baby from above
Category:  Kids
Votes:  27
--------------------
Name:  Bowflex Max Intelligence
Tagline:  Bring the benefits of a personal trainer to your home
Category:  Health and Fitness
Votes:  23

解释

标题,您不需要指定 a user-agent,任何事情都可以,所以我只使用了一个字符。虽然这content-type是必要的,但您通过试错发现这一点。

我不会说谎的数据我没有勇气尝试分解它,我的猜测是交替它可能最终无法获得你想要的数据。该数据的优点之一是您可以更改年/月/日,它将为您提供需要滚动以触发项目的页面的数据。

你没有特别问过这个问题,但是使用 f-strings 之类的东西来输入每个 requests.get() 调用的特定数据将是一种方法。

现在你得到的是一个 json 对象,该方法response.json()将此对象转换为 python 字典。所以所有归属于字典的方法都可以使用。

我强烈建议您查看response.json()字典,除非您这样做,否则您不会真正了解名称,潜台词的含义。但是大多数 json 对象都是嵌套的数据集,因此您通常想要的数据位于许多键的后面,就像在这种情况下一样。对于每篇文章,我都会四处寻找名称/标语。

response.json()['data']['posts']['edges'] 列表项的典型输出

{'node': {'id': '142398',
  '__typename': 'Post',
  '_id': 'UG9zdC0xNDIzOTg=',
  'comments_count': 9,
  'name': 'dinely',
  'shortened_url': '/r/p/142398',
  'slug': 'dinely',
  'tagline': 'Up to 50% off top restaurants, no coupon needed',
  'updated_at': '2020-04-28T16:55:48-07:00',
  'topics': {'edges': [{'node': {'id': '2',
      '__typename': 'Topic',
      'slug': 'android',
      'name': 'Android',
      'isFollowed': False,
      'image_uuid': 'd3e235c7-437b-4ed1-8298-2ce04eded455'},
     '__typename': 'TopicEdge'},
    {'node': {'id': '8',
      '__typename': 'Topic',
      'slug': 'iphone',
      'name': 'iPhone',
      'isFollowed': False,
      'image_uuid': '0ee71650-973d-4933-a3eb-c7201950db4b'},
     '__typename': 'TopicEdge'},
    {'node': {'id': '159',
      '__typename': 'Topic',
      'slug': 'drinking',
      'name': 'Drinking',
      'isFollowed': False,
      'image_uuid': '33a0c652-253d-491f-b86b-4e9ba32ee203'},
     '__typename': 'TopicEdge'},
    {'node': {'id': '250',
      '__typename': 'Topic',
      'slug': 'travel',
      'name': 'Travel',
      'isFollowed': False,
      'image_uuid': '0a49cae9-ccff-47f1-8998-d0f47c2e7775'},
     '__typename': 'TopicEdge'},
    {'node': {'id': '278',
      '__typename': 'Topic',
      'slug': 'e-commerce',
      'name': 'E-Commerce',
      'isFollowed': False,
      'image_uuid': '1aa939fc-89dd-49ae-9fde-d456f9a6c8d2'},
     '__typename': 'TopicEdge'}],
   '__typename': 'TopicConnection'},
  'featured_at': '2019-01-07T03:52:19-08:00',
  'disabled_when_scheduled': True,
  'has_voted': False,
  'votes_count': 58,
  'thumbnail': {'id': '703734',
   'media_type': 'image',
   '__typename': 'Media',
   'image_uuid': '96893366-45e6-477e-9bb8-0e04c2070da6'},
  'product_state': 'default',
  'isCollected': False},
 '__typename': 'PostEdge'}

推荐阅读