Steam 爬虫/爬虫 Python 脚本未生成请求的所有信息?

时间:2020-12-30 15:47:12

标签: python html web-scraping web-crawler steam

我目前正在使用 Steam-crawler (https://github.com/aesuli/steam-crawler) 脚本,该脚本基于带有游戏 ID 的数据集,能够抓取 Steam 评论数据(日期、评论文本、用户 ID 等) ……)。我不是 HTML 抓取方面的专家,但根据我从代码中的理解(即下面的内容),该脚本会为给定的游戏 ID 循环以收集所有评论,直到遇到这种特定情况 endre = re.compile(r'({"success":2})|(no_more_reviews)')。< /p>

import argparse
import csv
import os
import re
import socket
import string
import urllib
import urllib.request
import urllib.parse
import json
from contextlib import closing
from time import sleep


def download_page(url, maxretries, timeout, pause):
    tries = 0
    htmlpage = None
    while tries < maxretries and htmlpage is None:
        try:
            with closing(urllib.request.urlopen(url, timeout=timeout)) as f:
                htmlpage = f.read()
                sleep(pause)
        except (urllib.error.URLError, socket.timeout, socket.error):
            tries += 1
    return htmlpage


def getgameids(filename):
    ids = set()
    with open(filename, encoding='utf8') as f:
        reader = csv.reader(f)
        for row in reader:
            dir = row[0] #à la base c'est 0
            id_ = row[1]
            name = row[2]
            ids.add((dir, id_, name))
    return ids


def getgamereviews(ids, timeout, maxretries, pause, out):
    urltemplate = string.Template(
        'https://store.steampowered.com//appreviews/$id?cursor=$cursor&filter=recent&language=english')
    endre = re.compile(r'({"success":2})|(no_more_reviews)')

    for (dir, id_, name) in ids:
        if dir == 'sub':
            print('skipping sub %s %s' % (id_, name))
            continue

        gamedir = os.path.join(out, 'pages', 'reviews', '-'.join((dir, id_)))

        donefilename = os.path.join(gamedir, 'reviews-done.txt') #When all reviews of a given have been extracted
        if not os.path.exists(gamedir):  #Create a folder if not existing
            os.makedirs(gamedir)
        elif os.path.exists(donefilename): #if folder exists, skip game
            print('skipping app %s %s' % (id_, name))
            continue

        print(dir, id_, name)

        cursor = '*'
        offset = 0
        page = 1
        maxError = 10
        errorCount = 0
        i = 0
        while True:
            url = urltemplate.substitute({'id': id_, 'cursor': cursor})
            print(offset, url)
            htmlpage = download_page(url, maxretries, timeout, pause)

            if htmlpage is None:
                print('Error downloading the URL: ' + url)
                sleep(pause * 3)
                errorCount += 1
                if errorCount >= maxError:
                    print('Max error!')
                    break
            else:
                with open(os.path.join(gamedir, 'reviews-%s.html' % page), 'w', encoding='utf-8') as f:
                    htmlpage = htmlpage.decode()
                    if endre.search(htmlpage):
                        break
                    f.write(htmlpage)
                    page = page + 1
                    parsed_json = (json.loads(htmlpage))
                    cursor = urllib.parse.quote(parsed_json['cursor'])

        with open(donefilename, 'w', encoding='utf-8') as f:
            pass


def main():
    parser = argparse.ArgumentParser(description='Crawler of Steam reviews')
    parser.add_argument('-f', '--force', help='Force download even if already successfully downloaded', required=False,
                        action='store_true')
    parser.add_argument(
        '-t', '--timeout', help='Timeout in seconds for http connections. Default: 180',
        required=False, type=int, default=180)
    parser.add_argument(
        '-r', '--maxretries', help='Max retries to download a file. Default: 5',
        required=False, type=int, default=3)
    parser.add_argument(
        '-p', '--pause', help='Seconds to wait between http requests. Default: 0.5', required=False, default=0.01,
        type=float)
    parser.add_argument(
        '-m', '--maxreviews', help='Maximum number of reviews per item to download. Default:unlimited', required=False,
        type=int, default=5000000)
    parser.add_argument(
        '-o', '--out', help='Output base path', required=False, default='data')
    parser.add_argument(
        '-i', '--ids', help='File with game ids', required=False, default='./data/games.csv')
    args = parser.parse_args()

    if not os.path.exists(args.out):
        os.makedirs(args.out)

    ids = getgameids(args.ids)

    print('%s games' % len(ids))

    getgamereviews(ids, args.timeout, args.maxretries, args.pause, args.out)


if __name__ == '__main__':
    main()

我目前面临的问题是脚本没有正确提取评论:例如,对于像 Counter-Strike Global Offensive 这样大约有大约 1,000,000 条评论的游戏,脚本有时会返回 4000 页评论数(每个 html 页面包含 20 条评论),6000 或 500 条直到停止!

我想象的解决方案可能是保存脚本测试的每个 URL,并在每次 endre = re.compile(r'({"success":2})|(no_more_reviews)') 为 True 时重复循环 10 次,并跳过已经收集的 URL,但是我不确定它真的有效吗?

我也会在 GitHub 页面上提出这个问题,但作者似乎并不经常回复,我真的很想知道它为什么会发生,以及是否有可能解决这个问题。提前致谢。

编辑:所以我查看了 Steam API 文档 https://partner.steamgames.com/doc/store/getreviews,似乎每个页面都提供了一个光标,以便能够加载下一个页面。那么为什么它会随机变化呢?

1 个答案:

答案 0 :(得分:0)

Rate Limiting 就像网络服务器的一种基本自我防御形式。 Steam 使用它来帮助防止其服务器被滥用(过多)。抓取 500 多个完整的网页是大量浪费的带宽——更不用说 4k+ 页面了——其中的许多数据都是无用的,因为机器没有运行 JS 或无论如何都没有显示任何内容。 >

您共享的最后一个链接 seems to be an official endpoint 来执行此操作,但您可能需要专门处理状态代码 429 "Too Many Requests"Related Q & A 提到此状态应该带有 Retry-After 标头和可能的限制说明。根据 MDN docs,Retry-After 的值可能是日期或等待的秒数。

cursor 值可能是在服务器端生成的,仅用于标识 20 条评论,因为这是默认的“页面大小”。文档说这意味着与以下请求一起发送,以从列表中获取下一组评论。