Response 403 python requests как исправить

I needed to parse a site, but i got an error 403 Forbidden. Here is a code: url = 'http://worldagnetwork.com/' result = requests.get(url) print(result.content.decode()) Its output: ...

I needed to parse a site, but i got an error 403 Forbidden.
Here is a code:

url = 'http://worldagnetwork.com/'
result = requests.get(url)
print(result.content.decode())

Its output:

<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>

Please, say what the problem is.

Aidas Bendoraitis's user avatar

asked Jul 20, 2016 at 19:36

Толкачёв Иван's user avatar

Толкачёв ИванТолкачёв Иван

1,6193 gold badges10 silver badges13 bronze badges

2

It seems the page rejects GET requests that do not identify a User-Agent. I visited the page with a browser (Chrome) and copied the User-Agent header of the GET request (look in the Network tab of the developer tools):

import requests
url = 'http://worldagnetwork.com/'
headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36'}
result = requests.get(url, headers=headers)
print(result.content.decode())

# <!doctype html>
# <!--[if lt IE 7 ]><html class="no-js ie ie6" lang="en"> <![endif]-->
# <!--[if IE 7 ]><html class="no-js ie ie7" lang="en"> <![endif]-->
# <!--[if IE 8 ]><html class="no-js ie ie8" lang="en"> <![endif]-->
# <!--[if (gte IE 9)|!(IE)]><!--><html class="no-js" lang="en"> <!--<![endif]-->
# ...

answered Jul 20, 2016 at 19:48

Alicia Garcia-Raboso's user avatar

2

Just add to Alberto’s answer:

If you still get a 403 Forbidden after adding a user-agent, you may need to add more headers, such as referer:

headers = {
    'User-Agent': '...',
    'referer': 'https://...'
}

The headers can be found in the Network > Headers > Request Headers of the Developer Tools. (Press F12 to toggle it.)

answered Jul 9, 2019 at 5:44

Hansimov's user avatar

5

If You are the server’s owner/admin, and the accepted solution didn’t work for You, then try disabling CSRF protection (link to an SO answer).

I am using Spring (Java), so the setup requires You to make a SecurityConfig.java file containing:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure (HttpSecurity http) throws Exception {
        http.csrf().disable();
    }
    // ...
}

answered May 26, 2018 at 11:31

Aleksandar's user avatar

AleksandarAleksandar

3,1481 gold badge36 silver badges42 bronze badges

The urllib module can be used to make an HTTP request from a site, unlike the requests library, which is a built-in library. This reduces dependencies. In the following article, we will discuss why urllib.error.httperror: http error 403: forbidden occurs and how to resolve it.

What is a 403 error?

The 403 error pops up when a user tries to access a forbidden page or, in other words, the page they aren’t supposed to access. 403 is the HTTP status code that the webserver uses to denote the kind of problem that has occurred on the user or the server end. For instance, 200 is the status code for – ‘everything has worked as expected, no errors’. You can go through the other HTTP status code from here.

ModSecurity is a module that protects websites from foreign attacks. It checks whether the requests are being made from a user or from an automated bot. It blocks requests from known spider/bot agents who are trying to scrape the site. Since the urllib library uses something like python urllib/3.3.0 hence, it is easily detected as non-human and therefore gets blocked by mod security.

from urllib import request
from urllib.request import Request, urlopen

url = "https://www.gamefaqs.com"
request_site = Request(url)
webpage = urlopen(request_site).read()
print(webpage[:200])

http error 403: forbidden error returned

http error 403: forbidden error returned

ModSecurity blocks the request and returns an HTTP error 403: forbidden error if the request was made without a valid user agent. A user-agent is a header that permits a specific string which in turn allows network protocol peers to identify the following:

  • The Operating System, for instance, Windows, Linux, or macOS.
  • Websserver’s browser

Moreover, the browser sends the user agent to each and every website that you get connected to. The user-Agent field is included in the HTTP header when the browser gets connected to a website. The header field data differs for each browser.

Why do sites use security that sends 403 responses?

According to a survey, more than 50% of internet traffic comes from automated sources. Automated sources can be scrapers or bots. Therefore it gets necessary to prevent these attacks. Moreover, scrapers tend to send multiple requests, and sites have some rate limits. The rate limit dictates how many requests a user can make. If the user(here scraper) exceeds it, it gets some kind of error, for instance, urllib.error.httperror: http error 403: forbidden.

Resolving urllib.error.httperror: http error 403: forbidden?

This error is caused due to mod security detecting the scraping bot of the urllib and blocking it. Therefore, in order to resolve it, we have to include user-agent/s in our scraper. This will ensure that we can safely scrape the website without getting blocked and running across an error. Let’s take a look at two ways to avoid urllib.error.httperror: http error 403: forbidden.

Method 1: using user-agent

from urllib import request
from urllib.request import Request, urlopen

url = "https://www.gamefaqs.com"
request_site = Request(url, headers={"User-Agent": "Mozilla/5.0"})
webpage = urlopen(request_site).read()
print(webpage[:500])
  • In the code above, we have added a new parameter called headers which has a user-agent Mozilla/5.0. Details about the user’s device, OS, and browser are given by the webserver by the user-agent string. This prevents the bot from being blocked by the site.
  • For instance, the user agent string gives information to the server that you are using Brace browser and Linux OS on your computer. Thereafter, the server accordingly sends the information.

Using user-agent, error gets resolved

Using the user-agent, the error gets resolved.

Botocore.Exceptions.NoCredentialsError: Unable to Locate Credentials

Method 2: using Session object

There are times when even using user-agent won’t prevent the urllib.error.httperror: http error 403: forbidden. Then we can use the Session object of the request module. For instance:

from random import seed
import requests

url = "https://www.gamefaqs.com"
session_obj = requests.Session()
response = session_obj.get(url, headers={"User-Agent": "Mozilla/5.0"})

print(response.status_code)

The site could be using cookies as a defense mechanism against scraping. It’s possible the site is setting and requesting cookies to be echoed back as a defense against scraping, which might be against its policy.

The Session object is compatible with cookies.

Using Session object to resolve urllib.error

Using Session object to resolve urllib.error

Catching urllib.error.httperror

urllib.error.httperror can be caught using the try-except method. Try-except block can capture any exception, which can be hard to debug. For instance, it can capture exceptions like SystemExit and KeyboardInterupt. Let’s see how can we do this, for instance:

from urllib.request import Request, urlopen
from urllib.error import HTTPError

url = "https://www.gamefaqs.com"

try:
    request_site = Request(url)
    webpage = urlopen(request_site).read()
    print(webpage[:500])
except HTTPError as e:
    print("Error occured!")
    print(e)

Catching the error using the try-except

Catching the error using the try-except

[Resolved] NameError: Name _mysql is Not Defined

FAQs

How to fix the 403 error in the browser?

You can try the following steps in order to resolve the 403 error in the browser try refreshing the page, rechecking the URL, clearing the browser cookies, check your user credentials.

Why do scraping modules often get 403 errors?

Scrapers often don’t use headers while requesting information. This results in their detection by the mod security. Hence, scraping modules often get 403 errors.

Conclusion

In this article, we covered why and when urllib.error.httperror: http error 403: forbidden it can occur. Moreover, we looked at the different ways to resolve the error. We also covered the handling of this error.

Trending Now

  • “Other Commands Don’t Work After on_message” in Discord Bots

    “Other Commands Don’t Work After on_message” in Discord Bots

    February 5, 2023

  • Botocore.Exceptions.NoCredentialsError: Unable to Locate Credentials

    Botocore.Exceptions.NoCredentialsError: Unable to Locate Credentials

    by Rahul Kumar YadavFebruary 5, 2023

  • [Resolved] NameError: Name _mysql is Not Defined

    [Resolved] NameError: Name _mysql is Not Defined

    by Rahul Kumar YadavFebruary 5, 2023

  • Best Ways to Implement Regex New Line in Python

    Best Ways to Implement Regex New Line in Python

    by Rahul Kumar YadavFebruary 5, 2023

Собственно, проблема следующая:
Postman отрабатывает отлично и возвращает ожидаемый результат
5ef5bfc569c0b193705559.png

А python на аналогичный запрос выдает 403 код. Хотя вроде как заголовки одинаковые. Что ему, собаке, не хватает?

import requests
from pprint import pprint

url = 'http://ovga.mos.ru:8080/_ajax/pass/list?search={%22grz%22:%22К239ММ159%22}&sort=validitydate&order=desc'

headers = {"X-Requested-With": "XMLHttpRequest",
           'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                         'Chrome/54.0.2840.99 Safari/537.36',
           }
response = requests.get(url, headers)

pprint(response)

<Response [403]>


  • Вопрос задан

    более двух лет назад

  • 2048 просмотров

Ты не все заголовки передал. Postman по-умолчанию генерирует некоторые заголовки самостоятельно, вот так подключается нормально:

headers = {
    'Host': 'ovga.mos.ru',
    'User-Agent': 'Magic User-Agent v999.26 Windows PRO 11',
    'Accept': '*/*',
    'Accept-Encoding': 'gzip, deflate, br',
    'Connection': 'keep-alive',
    'X-Requested-With': 'XMLHttpRequest'
}
url = 'http://ovga.mos.ru:8080/_ajax/pass/list?search={"grz":"К239ММ159"}&sort=validitydate&order=desc'
response = requests.get(url, headers=headers)

<Response [200]>

Пригласить эксперта


  • Показать ещё
    Загружается…

10 февр. 2023, в 02:20

3000 руб./за проект

10 февр. 2023, в 01:33

1500 руб./за проект

10 февр. 2023, в 00:54

2000 руб./в час

Минуточку внимания

  1. the urllib Module in Python
  2. Check robots.txt to Prevent urllib HTTP Error 403 Forbidden Message
  3. Adding Cookie to the Request Headers to Solve urllib HTTP Error 403 Forbidden Message
  4. Use Session Object to Solve urllib HTTP Error 403 Forbidden Message

Solve Urllib HTTP Error 403 Forbidden Message in Python

Today’s article explains how to deal with an error message (exception), urllib.error.HTTPError: HTTP Error 403: Forbidden, produced by the error class on behalf of the request classes when it faces a forbidden resource.

the urllib Module in Python

The urllib Python module handles URLs for python via different protocols. It is famous for web scrapers who want to obtain data from a particular website.

The urllib contains classes, methods, and functions that perform certain operations such as reading, parsing URLs, and robots.txt. There are four classes, request, error, parse, and robotparser.

Check robots.txt to Prevent urllib HTTP Error 403 Forbidden Message

When using the urllib module to interact with clients or servers via the request class, we might experience specific errors. One of those errors is the HTTP 403 error.

We get urllib.error.HTTPError: HTTP Error 403: Forbidden error message in urllib package while reading a URL. The HTTP 403, the Forbidden Error, is an HTTP status code that indicates that the client or server forbids access to a requested resource.

Therefore, when we see this kind of error message, urllib.error.HTTPError: HTTP Error 403: Forbidden, the server understands the request but decides not to process or authorize the request that we sent.

To understand why the website we are accessing is not processing our request, we need to check an important file, robots.txt. Before web scraping or interacting with a website, it is often advised to review this file to know what to expect and not face any further troubles.

To check it on any website, we can follow the format below.

https://<website.com>/robots.txt

For example, check YouTube, Amazon, and Google robots.txt files.

https://www.youtube.com/robots.txt
https://www.amazon.com/robots.txt
https://www.google.com/robots.txt

Checking YouTube robots.txt gives the following result.

# robots.txt file for YouTube
# Created in the distant future (the year 2000) after
# the robotic uprising of the mid-'90s wiped out all humans.

User-agent: Mediapartners-Google*
Disallow:

User-agent: *
Disallow: /channel/*/community
Disallow: /comment
Disallow: /get_video
Disallow: /get_video_info
Disallow: /get_midroll_info
Disallow: /live_chat
Disallow: /login
Disallow: /results
Disallow: /signup
Disallow: /t/terms
Disallow: /timedtext_video
Disallow: /user/*/community
Disallow: /verify_age
Disallow: /watch_ajax
Disallow: /watch_fragments_ajax
Disallow: /watch_popup
Disallow: /watch_queue_ajax

Sitemap: https://www.youtube.com/sitemaps/sitemap.xml
Sitemap: https://www.youtube.com/product/sitemap.xml

We can notice a lot of Disallow tags there. This Disallow tag shows the website’s area, which is not accessible. Therefore, any request to those areas will not be processed and is forbidden.

In other robots.txt files, we might see an Allow tag. For example, http://youtube.com/comment is forbidden to any external request, even with the urllib module.

Let’s write code to scrape data from a website that returns an HTTP 403 error when accessed.

Example Code:

import urllib.request
import re

webpage = urllib.request.urlopen('https://www.cmegroup.com/markets/products.html?redirect=/trading/products/#cleared=Options&sortField=oi').read()
findrows = re.compile('<tr class="- banding(?:On|Off)>(.*?)</tr>')
findlink = re.compile('<a href =">(.*)</a>')

row_array = re.findall(findrows, webpage)
links = re.findall(findlink, webpage)

print(len(row_array))

Output:

Traceback (most recent call last):
  File "c:UsersakinlDocumentsPythonindex.py", line 7, in <module>
    webpage = urllib.request.urlopen('https://www.cmegroup.com/markets/products.html?redirect=/trading/products/#cleared=Options&sortField=oi').read()
  File "C:Python310liburllibrequest.py", line 216, in urlopen
    return opener.open(url, data, timeout)
  File "C:Python310liburllibrequest.py", line 525, in open
    response = meth(req, response)
  File "C:Python310liburllibrequest.py", line 634, in http_response
    response = self.parent.error(
  File "C:Python310liburllibrequest.py", line 563, in error
    return self._call_chain(*args)
  File "C:Python310liburllibrequest.py", line 496, in _call_chain
    result = func(*args)
  File "C:Python310liburllibrequest.py", line 643, in http_error_default
    raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 403: Forbidden

The reason is that we are forbidden from accessing the website. However, if we check the robots.txt file, we will notice that https://www.cmegroup.com/markets/ is not with a Disallow tag. However, if we go down the robots.txt file for the website we wanted to scrape, we will find the below.

User-agent: Python-urllib/1.17
Disallow: /

The above text means that the user agent named Python-urllib is not allowed to crawl any URL within the site. That means using the Python urllib module is not allowed to crawl the site.

Therefore, check or parse the robots.txt to know what resources we have access to. we can parse robots.txt file using the robotparser class. These can prevent our code from experiencing an urllib.error.HTTPError: HTTP Error 403: Forbidden error message.

Passing a valid user agent as a header parameter will quickly fix the problem. The website may use cookies as an anti-scraping measure.

The website may set and ask for cookies to be echoed back to prevent scraping, which is maybe against its policy.

from urllib.request import Request, urlopen

def get_page_content(url, head):

  req = Request(url, headers=head)
  return urlopen(req)

url = 'https://example.com'
head = {
  'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36',
  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
  'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.3',
  'Accept-Encoding': 'none',
  'Accept-Language': 'en-US,en;q=0.8',
  'Connection': 'keep-alive',
  'refere': 'https://example.com',
  'cookie': """your cookie value ( you can get that from your web page) """
}

data = get_page_content(url, head).read()
print(data)

Output:

<!doctype html>n<html>n<head>n    <title>Example Domain</title>nn    <meta
'
'
'
<p><a href="https://www.iana.org/domains/example">More information...</a></p>n</div>n</body>n</html>n'

Passing a valid user agent as a header parameter will quickly fix the problem.

Use Session Object to Solve urllib HTTP Error 403 Forbidden Message

Sometimes, even using a user agent won’t stop this error from occurring. The Session object of the requests module can then be used.

from random import seed
import requests

url = "https://stackoverflow.com/search?q=html+error+403"
session_obj = requests.Session()
response = session_obj.get(url, headers={"User-Agent": "Mozilla/5.0"})

print(response.status_code)

Output:

The above article finds the cause of the urllib.error.HTTPError: HTTP Error 403: Forbidden and the solution to handle it. mod_security basically causes this error as different web pages use different security mechanisms to differentiate between human and automated computers (bots).

Summary

TL;DR: requests raises a 403 while requesting an authenticated Github API route, which otherwise succeeds while using curl/another python library like httpx

Was initially discovered in the ‘ghexport’ project; I did a reasonable amount of debugging and created this repo before submitting this issue to PyGithub, but thats a lot to look through, just leaving it here as context.

It’s been hard to reproduce, the creator of ghexport (where this was initially discovered) didn’t have the same issue, so I’m unsure of the exact reason

Expected Result

requests succeeds for the authenticated request

Actual Result

Request fails, with:

{'message': 'Must have push access to repository', 'documentation_url': 'https://docs.github.com/rest/reference/repos#get-repository-clones'}
failed
Traceback (most recent call last):
  File "/home/sean/Repos/pygithub_requests_error/minimal.py", line 47, in <module>
    main()
  File "/home/sean/Repos/pygithub_requests_error/minimal.py", line 44, in main
    make_request(requests.get, url, headers)
  File "/home/sean/Repos/pygithub_requests_error/minimal.py", line 31, in make_request
    resp.raise_for_status()
  File "/home/sean/.local/lib/python3.9/site-packages/requests/models.py", line 941, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 403 Client Error: Forbidden for url: https://api.github.com/repos/seanbreckenridge/albums/traffic/clones

Reproduction Steps

Apologies if this is a bit too specific, but otherwise requests works great on my system and I can’t find any other way to reproduce this — Is a bit long as it requires an auth token

Go here and create a token with scopes like:

I’ve compared this to httpx, where it doesn’t fail:

#!/usr/bin/env python3

from typing import Callable, Any

import requests
import httpx


# extract status/status_code from the requests/httpx item
def extract_status(obj: Any) -> int:
    if hasattr(obj, "status"):
        return obj.status
    if hasattr(obj, "status_code"):
        return obj.status_code
    raise TypeError("unsupported request object")


def make_request(using_verb: Callable[..., Any], url: str, headers: Any) -> None:

    print("using", using_verb.__module__, using_verb.__qualname__, url)

    resp = using_verb(url, headers=headers)
    status = extract_status(resp)

    print(str(resp.json()))

    if status == 200:
        print("succeeded")
    else:
        print("failed")
        resp.raise_for_status()


def main():
    # see https://github.com/seanbreckenridge/pygithub_requests_error for token scopes
    auth_token = "put your auth token here"

    headers = {
        "Authorization": "token {}".format(auth_token),
        "User-Agent": "requests_error",
        "Accept": "application/vnd.github.v3+json",
    }

    # replace this with a URL you have access to
    url = "https://api.github.com/repos/seanbreckenridge/albums/traffic/clones"

    make_request(httpx.get, url, headers)
    make_request(requests.get, url, headers)


if __name__ == "__main__":
    main()

That outputs:

using httpx get https://api.github.com/repos/seanbreckenridge/albums/traffic/clones
{'count': 15, 'uniques': 10, 'clones': [{'timestamp': '2021-04-12T00:00:00Z', 'count': 1, 'uniques': 1}, {'timestamp': '2021-04-14T00:00:00Z', 'count': 1, 'uniques': 1}, {'timestamp': '2021-04-17T00:00:00Z', 'count': 1, 'uniques': 1}, {'timestamp': '2021-04-18T00:00:00Z', 'count': 2, 'uniques': 2}, {'timestamp': '2021-04-23T00:00:00Z', 'count': 9, 'uniques': 5}, {'timestamp': '2021-04-25T00:00:00Z', 'count': 1, 'uniques': 1}]}
succeeded
using requests.api get https://api.github.com/repos/seanbreckenridge/albums/traffic/clones
{'message': 'Must have push access to repository', 'documentation_url': 'https://docs.github.com/rest/reference/repos#get-repository-clones'}
failed
Traceback (most recent call last):
  File "/home/sean/Repos/pygithub_requests_error/minimal.py", line 48, in <module>
    main()
  File "/home/sean/Repos/pygithub_requests_error/minimal.py", line 45, in main
    make_request(requests.get, url, headers)
  File "/home/sean/Repos/pygithub_requests_error/minimal.py", line 32, in make_request
    resp.raise_for_status()
  File "/home/sean/.local/lib/python3.9/site-packages/requests/models.py", line 943, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 403 Client Error: Forbidden for url: https://api.github.com/repos/seanbreckenridge/albums/traffic/clones

Another thing that may be useful as context is the pdb trace I did here, which was me stepping into where the request was made in PyGithub, and making all the requests manually using the computed url/headers. Fails when I use requests.get but httpx.get works fine:

> /home/sean/.local/lib/python3.8/site-packages/github/Requester.py(484)__requestEncode()
-> self.NEW_DEBUG_FRAME(requestHeaders)
(Pdb) n
> /home/sean/.local/lib/python3.8/site-packages/github/Requester.py(486)__requestEncode()
-> status, responseHeaders, output = self.__requestRaw(
(Pdb) w
  /home/sean/Repos/ghexport/export.py(109)<module>()
-> main()
  /home/sean/Repos/ghexport/export.py(84)main()
-> j = get_json(**params)
  /home/sean/Repos/ghexport/export.py(74)get_json()
-> return Exporter(**params).export_json()
  /home/sean/Repos/ghexport/export.py(60)export_json()
-> repo._requester.requestJsonAndCheck('GET', repo.url + '/traffic/' + f)
  /home/sean/.local/lib/python3.8/site-packages/github/Requester.py(318)requestJsonAndCheck()
-> *self.requestJson(
  /home/sean/.local/lib/python3.8/site-packages/github/Requester.py(410)requestJson()
-> return self.__requestEncode(cnx, verb, url, parameters, headers, input, encode)
> /home/sean/.local/lib/python3.8/site-packages/github/Requester.py(486)__requestEncode()
-> status, responseHeaders, output = self.__requestRaw(
(Pdb) url
'/repos/seanbreckenridge/advent-of-code-2019/traffic/views'
(Pdb) requestHeaders
{'Authorization': 'token <MY TOKEN HERE>', 'User-Agent': 'PyGithub/Python'}
(Pdb) import requests
(Pdb) requests.get("https://api.github.com" + url, headers=requestHeaders).json()
{'message': 'Must have push access to repository', 'documentation_url': 'https://docs.github.com/rest/reference/repos#get-page-views'}
(Pdb) httpx.get("https://api.github.com" + url, headers=requestHeaders).json()
{'count': 0, 'uniques': 0, 'views': []}
(Pdb) httpx succeeded??

System Information

$ python -m requests.help
{
  "chardet": {
    "version": "3.0.4"
  },
  "cryptography": {
    "version": "3.4.7"
  },
  "idna": {
    "version": "2.10"
  },
  "implementation": {
    "name": "CPython",
    "version": "3.9.3"
  },
  "platform": {
    "release": "5.11.16-arch1-1",
    "system": "Linux"
  },
  "pyOpenSSL": {
    "openssl_version": "101010bf",
    "version": "20.0.1"
  },
  "requests": {
    "version": "2.25.1"
  },
  "system_ssl": {
    "version": "101010bf"
  },
  "urllib3": {
    "version": "1.25.9"
  },
  "using_pyopenssl": true
}
$ pip freeze | grep -E 'requests|httpx'
httpx==0.16.1
requests==2.25.1

Парсим мемы в питоне: как обойти серверную блокировку +64

Программирование, Python, Data Mining, Открытые данные, Блог компании Open Data Science


Рекомендация: подборка платных и бесплатных курсов 3D-моделирования — https://katalog-kursov.ru/

Новогодние праздники — прекрасный повод попрокрастинировать в уютной домашней обстановке и вспомнить дорогие сердцу мемы из 2k17, уходящие навсегда, как совесть Electronic Arts.

Однако даже обильно сдобренная салатами совесть иногда просыпалась и требовала хоть немного взять себя в руки и заняться полезной деятельностью. Поэтому мы совместили приятное с полезным и на примере любимых мемов посмотрели, как можно спарсить себе небольшую базу
данных, попутно обходя всевозможные блокировки, ловушки и ограничения, расставленные сервером на нашем пути. Всех заинтересованных любезно приглашаем под кат.

Машинное обучение, эконометрика, статистика и многие другие науки о данных занимаются поиском закономерностей. Каждый день доблестные аналитики-инквизиторы пытают природу разными методами и вытаскивают из неё сведенья о том, как именно устроен великий процесс порождения данных, создавший нашу вселенную. Испанские инквизиторы в своей повседневной деятельности использовали непосредственно физическое тело своей жертвы. Природа же вездесуща и не имеет однозначного физического облика. Из-за этого профессия современного инквизитора имеет странную специфику — пытки природы происходят через анализ данных, которые надо откуда-то брать. Обычно данные инквизиторам приносят мирные собиратели. Эта статейка призвана немного приоткрыть завесу тайны насчет того, откуда данные берутся и как их можно немножечко пособирать.

Нашим девизом стала знаменитая фраза капитана Джека Воробья: «Бери всё и не отдавай ничего». Иногда для сбора мемов придется использовать довольно бандитские методы. Тем не менее, мы будем оставаться мирными собирателями данных, и ни в коей мере не будем становиться бандитами. Брать мемы мы будем из главного мемохранилища.

1. Вламываемся в мемохранилище

1.1. Что мы хотим получить

Итак, мы хотим распарсить knowyourmeme.com и получить кучу разных переменных:

  • Name – название мема,
  • Origin_year – год его создания,
  • Views – число просмотров,
  • About – текстовое описание мема,
  • и многие другие

Более того, мы хотим сделать это без вот этого всего:

После скачивания и чистки данных от мусора можно будет заняться строительством моделей. Например, попытаться предсказать популярность мема по его параметрам. Но это все позже, а сейчас познакомимся с парой определений:

  • Парсер — это скрипт, который грабит информацию с сайта
  • Краулер — это часть парсера, которая бродит по ссылкам
  • Краулинг — это переход по страницам и ссылкам
  • Скрапинг — это сбор данных со страниц
  • Парсинг — это сразу и краулинг и скрапинг!

1.2. Что такое HTML

HTML (HyperText Markup Language) — это такой же язык разметки как Markdown или LaTeX. Он является стандартным для написания различных сайтов. Команды в таком языке называются тегами. Если открыть абсолютно любой сайт, нажать на правую кнопку мышки, а после нажать View page source, то перед вами предстанет HTML скелет этого сайта.

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

  • <title> – заголовок страницы
  • <h1>…<h6> – заголовки разных уровней
  • <p> – абзац (paragraph)
  • <div> – выделения фрагмента документа с целью изменения вида содержимого
  • <table> – прорисовка таблицы
  • <tr> – разделитель для строк в таблице
  • <td> – разделитель для столбцов в таблице
  • <b> – устанавливает жирное начертание шрифта

Обычно команда <...> открывает тег, а </...> закрывает его. Все, что находится между этими двумя командами, подчиняется правилу, которое диктует тег. Например, все, что находится между <p> и </p> — это отдельный абзац.

Теги образуют своеобразное дерево с корнем в теге <html> и разбивают страницу на разные логические кусочки. У каждого тега могут быть свои потомки (дети) — те теги, которые вложены в него, и свои родители.

Например, HTML-древо страницы может выглядеть вот так:

<html>
<head> Заголовок </head>
<body>
    <div>
        Первый кусок текста со своими свойствами
    </div>
    <div>
        Второй кусок текста
            <b>
                Третий, жирный кусок
            </b>
    </div>
    Четвёртый кусок текста
</body>
</html>

Можно работать с этим html как с текстом, а можно как с деревом. Обход этого дерева и есть парсинг веб-страницы. Мы всего лишь будем находить нужные нам узлы среди всего этого разнообразия и забирать из них информацию!

Вручную обходить эти деревья не очень приятно, поэтому есть специальные языки для обхода деревьев.

  • CSS-селектор (это когда мы ищем элемент страницы по паре ключ, значение)
  • XPath (это когда мы прописываем путь по дереву вот так: /html/body/div[1]/div[3]/div/div[2]/div)
  • Всякие разные библиотеки для всяких разных языков, например, BeautifulSoup для питона. Именно эту библиотеку мы и будем использовать.

1.3. Наш первый запрос

Доступ к веб-станицам позволяет получать модуль requests. Подгрузим его. За компанию подгрузим ещё парочку дельных пакетов.

import requests      # Библиотека для отправки запросов
import numpy as np   # Библиотека для матриц, векторов и линала
import pandas as pd  # Библиотека для табличек
import time          # Библиотека для тайм-менеджмента

Для наших благородных исследовательских целей нужно собрать данные по каждому мему с соответствующей ему страницы. Но для начала нужно получить адреса этих страниц. Поэтому открываем основную страницу со всеми выложенными мемами. Выглядит она следующим образом:

Отсюда мы и будем тащить ссылки на каждый из перечисленных мемов. Сохраним в переменную page_link адрес основной страницы и откроем её при помощи библиотеки requests.

page_link = 'http://knowyourmeme.com/memes/all/page/1'
response = requests.get(page_link)
response

Out: <Response [403]>

А вот и первая проблема! Обращаемся к главному источнику знаний и выясняем, что 403-я ошибка выдается сервером, если он доступен и способен обрабатывать запросы, но по некоторым личным причинам отказывается это делать.

Попробуем выяснить, почему. Для этого проверим, как выглядел финальный запрос, отправленный нами на сервер, а конкретнее — как выглядел наш User-Agent в глазах сервера.

for key, value in response.request.headers.items():
    print(key+": "+value)

Out:
User-Agent: python-requests/2.14.2
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive

Похоже, мы недвусмысленно дали понять серверу, что мы сидим на питоне и используем библиотеку requests под версией 2.14.2. Скорее всего, это вызвало у сервера некоторые подозрения относительно наших благих намерений и он решил нас безжалостно отвергнуть. Для сравнения, можно посмотреть, как выглядят request-headers у здорового человека:

Очевидно, что нашему скромному запросу не тягаться с таким обилием мета-информации, которое передается при запросе из обычного браузера. К счастью, никто нам не мешает притвориться человечными и пустить пыль в глаза сервера при помощи генерации фейкового юзер-агента. Библиотек, которые справляются с такой задачей, существует очень и очень много, лично мне больше всего нравится fake-useragent. При вызове метода из различных кусочков будет генерироваться рандомное сочетание операционной системы, спецификаций и версии браузера, которые можно передавать в запрос:

# подгрузим один из методов этой библиотеки
from fake_useragent import UserAgent
UserAgent().chrome

Out: 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2224.3 Safari/537.36'

Попробуем прогнать наш запрос еще раз, уже со сгенерированным агентом

response = requests.get(page_link, headers={'User-Agent': UserAgent().chrome})
response

Out: <Response [200]>

Замечательно, наша небольшая маскировка сработала и обманутый сервер покорно выдал благословенный 200 ответ — соединение установлено и данные получены, всё чудесно! Посмотрим, что же все-таки мы получили.

html = response.content
html[:1000]

Out: b'<!DOCTYPE html>n<html xmlns:fb='http://www.facebook.com/2008/fbml' xmlns='http://www.w3.org/1999/xhtml'>n<head>n<meta content='text/html; charset=utf-8' http-equiv='Content-Type'>n<script type="text/javascript">window.NREUM||(NREUM={});NREUM.info={"beacon":"bam.nr-data.net","errorBeacon":"bam.nr-data.net","licenseKey":"c1a6d52f38","applicationID":"31165848","transactionName":"dFdfRUpeWglTQB8GDUNKWFRLHlcJWg==","queueTime":0,"applicationTime":59,"agent":""}</script>n<script type="text/javascript">window.NREUM||(NREUM={}),__nr_require=function(e,t,n){function r(n){if(!t[n]){var o=t[n]={exports:{}};e[n][0].call(o.exports,function(t){var o=e[n][1][t];return r(o||t)},o,o.exports)}return t[n].exports}if("function"==typeof __nr_require)return __nr_require;for(var o=0;o<n.length;o++)r(n[o]);return r}({1:[function(e,t,n){function r(){}function o(e,t,n){return function(){return i(e,[f.now()].concat(u(arguments)),t?null:this,n),t?void 0:this}}var i=e("handle"),a=e(2),u=e(3),c=e("ee").get("tracer")'

Выглядит неудобоваримо, как насчет сварить из этого дела что-то покрасивее? Например, прекрасный суп.

1.4. Прекрасный суп

Пакет bs4, a.k.a BeautifulSoup (тут есть гиперссылка на лучшего друга человека — документацию) был назван в честь стишка про прекрасный суп из Алисы в стране чудес.

Прекрасный суп — это совершенно волшебная библиотека, которая из сырого и необработанного HTML кода страницы выдаст вам структурированный массив данных, по которому очень удобно искать необходимые теги, классы, атрибуты, тексты и прочие элементы веб страниц.

Пакет под названием BeautifulSoup — скорее всего, не то, что нам нужно. Это третья версия (Beautiful Soup 3), а мы будем использовать четвертую. Нужно будет установить пакет beautifulsoup4. Чтобы было совсем весело, при импорте нужно указывать другое название пакета — bs4, а импортировать функцию под названием BeautifulSoup. В общем, сначала легко запутаться, но эти трудности нужно преодолеть.

С необработанным XML кодом страницы пакет также работает (XML — это исковерканый и превращённый в диалект, с помощью своих команд, HTML). Для того, чтобы пакет корректно работал с XML разметкой, придётся в довесок ко всему нашему арсеналу установить пакет xml.

from bs4 import BeautifulSoup

Передадим функции BeautifulSoup текст веб-страницы, которую мы недавно получили.

soup = BeautifulSoup(html,'html.parser') # В опции также можно указать lxml,
                                         # если предварительно установить одноименный пакет

Получим что-то вот такое:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:fb="http://www.facebook.com/2008/fbml">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
<script type="text/javascript">window.NREUM||(NREUM={});NREUM.info={"beacon":"bam.nr-data.net","errorBeacon":"bam.nr-data.net","licenseKey":"c1a6d52f38","applicationID":"31165848","transactionName":"dFdfRUpeWglTQB8GDUNKWFRLHkUNWUU=","queueTime":0,"applicationTime":24,"agent":""}</script>
<script type="text/javascript">window.NREUM||(NREUM={}),__nr_require=function(e,n,t){function r(t){if(!n[t]){var o=n[t]={exports:{}};e[t][0].call(o.exports,function(n){var o=e[t][1][n];return r(o||n)},o,o.exports)}return n[t].exports}if("function"==typeof __nr_require)return __nr_require;for(var o=0;o<t.length;o++)r(t[o]);return r}({1:[function(e,n,t){function r(){}function o(e,n,t){return function(){return i(e,[c.now()].concat(u(arguments)),n?null:this,t),n?void 0:this}}var i=e("handle"),a=e(2),u=e(3),f=e("ee").get("tracer"),c=e("loader"),s=NREUM;"undefined"==typeof window.newrelic&&(newrelic=s);var p=

Стало намного лучше, не правда ли? Что же лежит в переменной soup? Невнимательный пользователь, скорее всего, скажет, что ничего вообще не изменилось. Тем не менее, это не так. Теперь мы можем свободно бродить по HTML-дереву страницы, искать детей, родителей и вытаскивать их!

Например, можно бродить по вершинам, указывая путь из тегов.

soup.html.head.title

Out:  <title>All Entries | Know Your Meme</title>

Можно вытащить из того места, куда мы забрели, текст с помощью метода text.

soup.html.head.title.text

Out: 'All Entries | Know Your Meme'

Более того, зная адрес элемента, мы сразу можем найти его. Например, можно сделать это по классу. Следующая команда должна найти элемент, который лежит внутри тега a и имеет класс photo.

obj = soup.find('a', attrs = {'class':'photo'})
obj

Out: <a class="photo left" href="/memes/nu-male-smile" target="_self"><img alt='The "Nu-Male Smile" Is Duck Face for Men' data-src="http://i0.kym-cdn.com/featured_items/icons/wide/000/007/585/7a2.jpg" height="112" src="http://a.kym-cdn.com/assets/blank-b3f96f160b75b1b49b426754ba188fe8.gif" title='The "Nu-Male Smile" Is Duck Face for Men' width="198"/> <div class="info abs"> <div class="c"> The "Nu-Male Smile" Is Duck Face for Men </div> </div> </a>

Однако, вопреки нашим ожиданиям, вытащенный объект имеет класс "photo left". Оказывается, BeautifulSoup4 расценивает аттрибуты class как набор отдельных значений, поэтому "photo left" для библиотеки равносильно ["photo", "left"], а указанное нами значение этого класса "photo" входит в этот список. Чтобы избежать такой неприятной ситуации и проходов по ненужным нам ссылкам, придется воспользоваться собственной функцией и задать строгое соответствие:

obj = soup.find(lambda tag: tag.name == 'a' and tag.get('class') == ['photo'])
obj

Out: <a class="photo" href="/memes/people/mf-doom"><img alt="MF DOOM" data-src="http://i0.kym-cdn.com/entries/icons/medium/000/025/149/1489698959244.jpg" src="http://a.kym-cdn.com/assets/blank-b3f96f160b75b1b49b426754ba188fe8.gif" title="MF DOOM"/> <div class="entry-labels"> <span class="label label-submission"> Submission </span> <span class="label" style="background: #d32f2e; color: white;">Person</span> </div> </a>

Полученный после поиска объект также обладает структурой bs4. Поэтому можно продолжить искать нужные нам объекты уже в нём! Вытащим ссылку на этот мем. Сделать это можно по атрибуту href, в котором лежит наша ссылка.

obj.attrs['href']

Out: '/memes/people/mf-doom'

Обратите внимание, что после всех этих безумных преобразований у данных поменялся тип. Теперь они str. Это означет, что с ними можно работать как с текстом и пускать в ход для отсеивания лишней информации регулярные выражения.

print("Тип данных до вытаскивания ссылки:", type(obj))
print("Тип данных после вытаскивания ссылки:", type(obj.attrs['href']))

Out:
Тип данных до вытаскивания ссылки: <class 'bs4.element.Tag'>
Тип данных после вытаскивания ссылки: <class 'str'>

Если несколько элементов на странице обладают указанным адресом, то метод find вернёт только самый первый. Чтобы найти все элементы с таким адресом, нужно использовать метод findAll, и на выход будет выдан список. Таким образом, мы можем получить одним поиском сразу все объекты, содержащие ссылки на страницы с мемами.

meme_links = soup.findAll(lambda tag: tag.name == 'a' and tag.get('class') == ['photo'])
meme_links[:3]

Out: [<a class="photo" href="/memes/people/mf-doom"><img alt="MF DOOM" data-src="http://i0.kym-cdn.com/entries/icons/medium/000/025/149/1489698959244.jpg" src="http://a.kym-cdn.com/assets/blank-b3f96f160b75b1b49b426754ba188fe8.gif" title="MF DOOM"/> <div class="entry-labels"> <span class="label label-submission"> Submission </span> <span class="label" style="background: #d32f2e; color: white;">Person</span> </div> </a>,
 <a class="photo" href="/memes/here-lies-beavis-he-never-scored"><img alt="Here Lies Beavis. He Never Scored." data-src="http://i0.kym-cdn.com/entries/icons/medium/000/025/148/maxresdefault.jpg" src="http://a.kym-cdn.com/assets/blank-b3f96f160b75b1b49b426754ba188fe8.gif" title="Here Lies Beavis. He Never Scored."/> <div class="entry-labels"> <span class="label label-submission"> Submission </span> </div> </a>,
 <a class="photo" href="/memes/people/vanossgaming"><img alt="VanossGaming" data-src="http://i0.kym-cdn.com/entries/icons/medium/000/025/147/Evan-Fong-e1501621844732.jpg" src="http://a.kym-cdn.com/assets/blank-b3f96f160b75b1b49b426754ba188fe8.gif" title="VanossGaming"/> <div class="entry-labels"> <span class="label label-submission"> Submission </span> <span class="label" style="background: #d32f2e; color: white;">Person</span> </div> </a>]

Осталось очистить полученный список от мусора:

meme_links = [link.attrs['href'] for link in meme_links]
meme_links[:10]

Out: ['/memes/people/mf-doom',
 '/memes/here-lies-beavis-he-never-scored',
 '/memes/people/vanossgaming',
 '/memes/stream-sniping',
 '/memes/kids-describe-god-to-an-illustrator',
 '/memes/bad-teacher',
 '/memes/people/adam-the-creator',
 '/memes/but-can-you-do-this',
 '/memes/people/ken-ashcorp',
 '/memes/heartbroken-cowboy']

Готово, получили ровно 16 ссылок по числу мемов на одной странице поиска.

Хорошо, то, что можно искать элемент по его адресу, конечно же, круто, но откуда взять этот адрес? Можно установить для своего браузера какую-нибудь утилиту, позволяющую вытаскивать со страницы нужные теги, например, selectorgadget.

Тем не менее, этот путь не подходит для истинного самурая. Для последователей бусидо есть другой способ — искать теги для каждого нужного нам элемента вручную. Для этого придётся жать правой кнопкой мышки по окну браузера и тыкать кнопку Inspect. После всех этих манипуляций браузер будет выглядеть как-то вот так:

Выскочивший кусок html, в котором находится адрес выбранного вами объекта, можно смело копировать в код и наслаждаться своей брутальностью.

Остался последний момент. Когда мы скачаем все мемы с текущей страницы, нам нужно будет каким-то образом забраться на соседнюю. На сайте это можно делать просто пролистывая страницу с мемами вниз, javascript-функции подтянут новые мемы на текущее окно, но сейчас трогать эти функции не хочется.

Обычно, все параметры, которые мы устанавливаем на сайте для поиска, отображаются на структуре хрефа. Мемы не являются исключением. Если мы хотим получить первую порцию мемов, мы должны будем обратиться к сайту по ссылке

http://knowyourmeme.com/memes/all/page/1

Если мы захотим получить вторую поцию с шестнадцатью мемами, нам придётся немного видоизменить ссылку, а именно заменить номер страницы на 2.

http://knowyourmeme.com/memes/all/page/2

Таким незатейливым образом мы сможем пройтись по всем страницам и ограбить мемохранилище. Наконец, обернем в красивую функцию все-все манипуляции, проделанные выше:

Функция для скачивания ссылок на мемы

def getPageLinks(page_number):
    """
        Возвращает список ссылок на мемы, полученный с текущей страницы

        page_number: int/string
            номер страницы для парсинга

    """
    # составляем ссылку на страницу поиска
    page_link = 'http://knowyourmeme.com/memes/all/page/{}'.format(page_number)

    # запрашиваем данные по ней
    response = requests.get(page_link, headers={'User-Agent': UserAgent().chrome})

    if not response.ok:
        # если сервер нам отказал, вернем пустой лист для текущей страницы
        return []

    # получаем содержимое страницы и переводим в суп
    html = response.content
    soup = BeautifulSoup(html,'html.parser')

    # наконец, ищем ссылки на мемы и очищаем их от ненужных тэгов
    meme_links = soup.findAll(lambda tag: tag.name == 'a' and tag.get('class') == ['photo'])
    meme_links = ['http://knowyourmeme.com' + link.attrs['href'] for link in meme_links]

    return meme_links

Протестируем функцию и убедимся, что всё хорошо

meme_links = getPageLinks(1)
meme_links[:2]

Out: ['http://knowyourmeme.com/memes/people/mf-doom',
 'http://knowyourmeme.com/memes/here-lies-beavis-he-never-scored']

Отлично, функция работает и теперь мы теоретически можем достать ссылки на все 17171 мем, для чего нам придется пройтись по 17171/16 ~ 1074 страницам. Прежде чем расстраивать сервер таким количеством запросов, посмотрим, как доставать всю необходимую информацию о конкретном меме.

1.5 Финальная подготовка к грабежу

По аналогии со ссылками можно вытащить что угодно. Для этого надо сделать несколько шагов:

  1. Открываем страничку с мемом
  2. Находим любым способом тег для нужной нам информации
  3. Пихаем всё это в прекрасный суп
  4. ……
  5. Profit

Для закрепления информации в голове любознательного читателя, вытащим число просмотров мема.

А в качестве примера возьмем самый популярный на этом сайте мем — Doge, набравший более 12 миллионов просмотров по состоянию на 1 января 2018 года.

Сама страница, с которой мы будем доставать дорогую нашему исследовательскому сердцу информацию выглядит следуюшим образом:

Как и прежде, для начала сохраним ссылку на страницу в переменную и вытащим по ней контент.

meme_page = 'http://knowyourmeme.com/memes/doge'
response = requests.get(meme_page, headers={'User-Agent': UserAgent().chrome})
html = response.content
soup = BeautifulSoup(html,'html.parser')

Посмотрим, как можно вытащить статистику просмотров, комментариев, а также числа загруженных видео и фото, связанных с нашим мемов. Всё это добро хранится справа вверху под тэгами "dd" и с классами "views", "videos", "photos" и "comments"

views = soup.find('dd', attrs={'class':'views'})
print(views)

Out:
<dd class="views" title="12,318,185 Views">
<a href="/memes/doge" rel="nofollow">12,318,185</a>
</dd>

Очистим от тэгов и пунктуации

views = views.find('a').text
views = int(views.replace(',', ''))
print(views)

Out:
12318185

Снова запихнём всё это в небольшую функцию.

Функция, возвращающая статистику по мему

def getStats(soup, stats):
    """
        Возвращает очищенное число просмотров/коментариев/...

        soup: объект bs4.BeautifulSoup 
            суп текущей страницы

        stats: string
            views/videos/photos/comments

    """

    obj = soup.find('dd', attrs={'class':stats})
    obj = obj.find('a').text
    obj = int(obj.replace(',', ''))

    return obj

Всё готово!

views = getStats(soup, stats='views')
videos = getStats(soup, stats='videos')
photos = getStats(soup, stats='photos')
comments = getStats(soup, stats='comments')

print("Просмотры: {}nВидео: {}nФото: {}nКомментарии: {}".format(views, videos, photos, comments))

Out:
Просмотры: 12318185
Видео: 59
Фото: 1645
Комментарии: 918

Еще из интересного и исследовательского — достанем дату и время добавления мема. Если посмотреть на страницу в браузере, можно подумать, что максимум информации, который мы можем вытащить — это число лет, прошедших с момента публикации — Added 4 years ago by NovaXP. Однако мы так просто сдаваться не будем, полезем в кишки html и откопаем там кусок, ответственный за эту надпись:

Ага! Вот и подробности по дате добавления, с точностью до минуты. Элементарно

date = soup.find('abbr', attrs={'class':'timeago'}).attrs['title']
date

Out: '2017-12-31T01:59:14-05:00'

На самом деле, парсеры — дело непредсказуемое. Часто страницы, которые мы парсим, имеют очень неоднородну структуру. Например, если мы парсим мемы, на части страниц может быть указано описание, а на части нет. Как только код впервые натыкается на отсутствие описания, он выдаёт ошибку и останавливается. Чтобы нормально собрать все данные, приходится прописывать исключения. Вроде бы, хранилище мемов хорошо оборудовано и никаких внештатных ситуаций происходить не должно.
Тем не менее, очень не хочется проснуться утром и увидеть, что код сделал 20 итераций, нарвался на ошибку и отрубился. Чтобы такого не произошло, можно, например, использовать конструкцию try - except и просто обрабатывать неугодные нам ошибки. Про исключения можно почитать на просторах интернета. В нашем же случае до ошибки можно и не доводить, а предварительно проверять, есть ли необходимый элемент на странице или нет при помощи обычного if - else, и уже после этого пытаться его распарсить.

Например, мы хотим вытащить статус мема, для этого найдем окружающие его тэги:

properties = soup.find('aside', attrs={'class':'left'})
meme_status = properties.find("dd")
meme_status

Out:
<dd>
Confirmed
</dd>

Дальше нужно вытащить из тэгов текст, а после обрубить все лишние пробелы.

meme_status.text.strip()

Out: 'Confirmed'

Однако, если неожиданно выяснится, что у мема нет статуса, метод find вернёт пустоту. Метод text, в свою очередь, не сможет найти в тэгах текст и выдаст ошибку. Чтобы обезопасить себя от таких пустот, можно прописать исключение или if - else. Так как в текущем меме статус все-таки есть, нарочно зададим его как пустой объект, чтобы проверить, что ошибка поймается в обоих случаях

# Делай раз! Ищем статус мема, но не находим его
meme_status = None

# Делай два! Пытаемся вытащить его...

# ... с исключениями
try:
    print(meme_status.text.strip()) 
# Ежели возникает ошибка, статус не найден, выдаём пустоту.
except:
    print("Exception")

# ... с проверкой на пустой элемент
if meme_status:
    print(meme_status.text.strip())
else:
    print("Empty")

Out: 
Exception
Empty

Такой код позволяет обезопасить себя от ошибок. В данном случае, мы можем переписать всю конструкцию с if - else в виде одной удобной строки. Эта строка проверит, полон ли респонса meme_status и ежели нет, то выдаст пустоту.

# снова найдем настоящий статус
properties = soup.find('aside', attrs={'class':'left'})
meme_status = properties.find("dd")

meme_status = "" if not meme_status else meme_status.text.strip()
print(meme_status)

Out: Confirmed

По аналогии можно вытащить всю остальную информацию со страницы, для чего вновь напишем функцию

Функция для парсинга свойств мема

def getProperties(soup):
    """
        Возвращает список (tuple) с названием, статусом, типом, 
        годом и местом происхождения и тэгами

        soup: объект bs4.BeautifulSoup 
            суп текущей страницы

    """
    # название - идёт с самым большим заголовком h1, легко найти
    meme_name = soup.find('section', attrs={'class':'info'}).find('h1').text.strip()

    # достаём все данные справа от картинки 
    properties = soup.find('aside', attrs={'class':'left'})

    # статус идет первым - можно не уточнять класс
    meme_status = properties.find("dd")
    # oneliner, заменяющий try-except: если тэга нет в properties, вернётся объект NoneType,
    # у которого аттрибут text отсутствует, и в этом случае он заменится на пустую строку
    meme_status = "" if not meme_status else meme_status.text.strip()

    # тип мема - обладает уникальным классом
    meme_type = properties.find('a', attrs={'class':'entry-type-link'})
    meme_type = "" if not meme_type else meme_type.text 

    # год происхождения первоисточника можно найти после заголовка Year, 
    # находим заголовок, определяем родителя и ищем следущего ребенка - наш раздел
    meme_origin_year = properties.find(text='nYearn')
    meme_origin_year = "" if not meme_origin_year else meme_origin_year.parent.find_next()
    meme_origin_year = meme_origin_year.text.strip()

    # сам первоисточник
    meme_origin_place = properties.find('dd', attrs={'class':'entry_origin_link'})
    meme_origin_place = "" if not meme_origin_place else meme_origin_place.text.strip()

    # тэги, связанные с мемом
    meme_tags = properties.find('dl', attrs={'id':'entry_tags'}).find('dd')
    meme_tags = "" if not meme_tags else meme_tags.text.strip()

    return meme_name, meme_status, meme_type, meme_origin_year, meme_origin_place, meme_tags

getProperties(soup)

Out:
('Doge',
 'Confirmed',
 'Animal',
 '2013',
 'Tumblr',
 'animal, dog, shiba inu, shibe, such doge, super shibe, japanese, super, tumblr, much, very, many, comic sans, photoshop meme, such, shiba, shibe doge, doges, dogges, reddit, comic sans ms, tumblr meme, hacked, bitcoin, dogecoin, shitposting, stare, canine')

Свойства мема собрали. Теперь собираем по аналогии его текстовое описание.

Функция для парсинга текстового описания мема

def getText(soup):
    """
        Возвращает текстовые описания мема

        soup: объект bs4.BeautifulSoup 
            суп текущей страницы

    """

    # достаём все тексты под картинкой
    body = soup.find('section', attrs={'class':'bodycopy'})

    # раздел about (если он есть), должен идти первым, берем его без уточнения класса
    meme_about = body.find('p')
    meme_about = "" if not meme_about else meme_about.text

    # раздел origin можно найти после заголовка Origin или History, 
    # находим заголовок, определяем родителя и ищем следущего ребенка - наш раздел
    meme_origin = body.find(text='Origin') or body.find(text='History')
    meme_origin = "" if not meme_origin else meme_origin.parent.find_next().text

    # весь остальной текст (если он есть) можно запихнуть в одно текстовое поле
    if body.text:
        other_text = body.text.strip().split('n')[4:]
        other_text = " ".join(other_text).strip()
    else:
        other_text = ""

    return meme_about, meme_origin, other_text

meme_about, meme_origin, other_text = getText(soup)

print("О чем мем:n{}nnПроисхождение:n{}nnОстальной текст:n{}...n"      .format(meme_about, meme_origin, other_text[:200]))

Out:
О чем мем:
Doge is a slang term for “dog” that is primarily associated with pictures of Shiba Inus (nicknamed “Shibe”) and internal monologue captions on Tumblr. These photos may be photoshopped to change the dog’s face or captioned with interior monologues in Comic Sans font.

Происхождение:
The use of the misspelled word “doge” to refer to a dog dates back to June 24th, 2005, when it was mentioned in an episode of Homestar Runner’s puppet show. In the episode titled “Biz Cas Fri 1”[2], Homestar calls Strong Bad his “d-o-g-e” while trying to distract him from his work.

Остальной текст:
Identity On February 23rd, 2010, Japanese kindergarten teacher Atsuko Sato posted several photos of her rescue-adopted Shiba Inu dog Kabosu to her personal blog.[38] Among the photos included a peculi...

Наконец, создадим функцию, возвращающую всю информацию по текущему мему

Функция, возвращающая все данные по мему

def getMemeData(meme_page):
    """
        Запрашивает данные по странице, возвращает обработанный словарь с данными

        meme_page: string
            ссылка на страницу с мемом

    """

    # запрашиваем данные по ссылке
    response = requests.get(meme_page, headers={'User-Agent': UserAgent().chrome})

    if not response.ok:
        # если сервер нам отказал, вернем статус ошибки 
        return response.status_code

    # получаем содержимое страницы и переводим в суп
    html = response.content
    soup = BeautifulSoup(html,'html.parser')

    # используя ранее написанные функции парсим информацию
    views = getStats(soup=soup, stats='views')
    videos = getStats(soup=soup, stats='videos')
    photos = getStats(soup=soup, stats='photos')
    comments = getStats(soup=soup, stats='comments')

    # дата
    date = soup.find('abbr', attrs={'class':'timeago'}).attrs['title']

    # имя, статус, и т.д.
    meme_name, meme_status, meme_type, meme_origin_year, meme_origin_place, meme_tags =    getProperties(soup=soup)

    # текстовые поля
    meme_about, meme_origin, other_text = getText(soup=soup)

    # составляем словарь, в котором будут хранится все полученные и обработанные данные
    data_row = {"name":meme_name, "status":meme_status, 
                "type":meme_type, "origin_year":meme_origin_year, 
                "origin_place":meme_origin_place,
                "date_added":date, "views":views, 
                "videos":videos, "photos":photos, "comments":comments, "tags":meme_tags,
                "about":meme_about, "origin":meme_origin, "other_text":other_text}

    return data_row

А теперь подготовим табличку, чтобы в неё записывать всё награбленные честно полученные данные, добавим в неё первую полученную строку и полюбуемся на результат

final_df = pd.DataFrame(columns=['name', 'status', 'type', 'origin_year', 'origin_place',
                                 'date_added', 'views', 'videos', 'photos', 'comments', 
                                 'tags', 'about', 'origin', 'other_text'])

data_row = getMemeData('http://knowyourmeme.com/memes/doge')
final_df = final_df.append(data_row, ignore_index=True)
final_df

Out:

name status type origin_year
Doge Confirmed Animal 2013

Первый мем оказался в наших рукак. Еще раз убедимся что всё работает — пройдемся по списку из ссылок на мемы, полученных ранее в перменной meme_links.

for meme_link in meme_links:
    data_row = getMemeData(meme_link)
    final_df = final_df.append(data_row, ignore_index=True)

Out:

name status type origin_year
Doge Confirmed Animal 2013
Charles C. Johnson Submission Activist 2013
Bat- (Prefix) Submission Snowclone 2018
The Eric Andre Show Deadpool TV Show 2012
Hopsin Submission Musician 2003

Отлично! Всё работает, мемы качаются, данные наполняются и всё было бы хорошо, если бы не одно но — количество запросов, которое нам придётся сделать, чтобы всё получить.

2. Прячемся от стражников

2.1 Когда работающий код больше не работает

Вот он! Тот самый момент абсолютного триумфа, когда код дописан и всё, что нам, мирным собирателям, остаётся — запустить наш код на одну ночку. Кажется, что через страсть мы преобрели силу. Запускаем наш код по всем $1075$ страницам с мемами. На всякий случай обернём наш цикл в try-except. Мало ли что там с этими мемами бывает.

Цикл на ночь

# Немного красивых циклов. При желании пакет можно отключить и 
# удалить команду tqdm_notebook из всех циклов
from tqdm import tqdm_notebook

final_df = pd.DataFrame(columns=['name', 'status', 'type', 'origin_year', 'origin_place',
                                 'date_added', 'views', 'videos', 'photos', 'comments', 
                                 'tags', 'about', 'origin', 'other_text'])

for page_number in tqdm_notebook(range(1075), desc='Pages'):
    # собрали хрефы с текущей страницы
    meme_links = getPageLinks(page_number)  
    for meme_link in tqdm_notebook(meme_links, desc='Memes', leave=False):
        # иногда с первого раза страничка не парсится
        for i in range(5):
            try:
                # пытаемся собрать по мему немного даты
                data_row = getMemeData(meme_link)           
                # и закидываем её в таблицу
                final_df = final_df.append(data_row, ignore_index=True)  
                # если всё получилось - выходим из внутреннего цикла
                break
            except:
                # Иначе, пробуем еще несколько раз, пока не закончатся попытки
                print('AHTUNG! parsing once again:', meme_link)
                continue

Сон был прекрасным! Солнце только-только взошло из-за горизонта, мы уже бежим за компьютер смотреть мемы и видим, что огромное число мемов не скачалось.

Конечно же, вполне естественной реакцией будет нажать на первую же ссылку, перейти в мемохранилище и увидеть, что нас забанили. Все наши реквесты остались без респонсов.

2.2 Тор — сын Одина

Иногда серверу надоедает общаться с одним и тем же человеком, делающим кучу запросов и сервер банит его. К сожалению, не только у людей запас терпения ограничен.

Приходится маскироваться. Для такой маскировки можно использовать разные способы, более того, один из них мы уже использовали, когда притворились человеком в нашем request-header. Для текущей же задачи, когда нас вероломно заблокировали по IP, нужно искать способы помощнее, чтобы иметь возможность этот IP менять. Конечно, как вариант можно было бы использовать прокси-сервера, тогда мы бы имели в запасе некоторое количество разных IP адресов, которые можно подставлять по мере «забанивания». Однако в этом подходе есть пара проблем: первая — нужно где-то раздобыть эти прокси, вторая — а что если ограниченного числа адресов нам не хватит и нужно больше?

В таком случае лучше всего нам подойдёт Tor. Вопреки пропагандируемому мнению, Tor используется не только преступниками, педофилами и прочими нехорошими террористами. Это, мягко говоря, далеко не так, и мы, мирные собиратели данных, являемся тому подтверждением. Всем прелестям, связанным с работой Tor, можно было бы посвятить несколько больших статеек, что собственно говоря уже и сделано. Подробнее про это можно почитатать по следующим ссылкам:

  • Как работает Tor
  • Методы анонимности в сети
  • Прокси-сервер с помощью Tor

Мы же ограничимся только функциональной частью, а именно без углубления в детали опишем шаги, которые нужно предпринять для того, чтобы использовать возможности Tor для обхода блокировки. Для начала полюбуемся на свой ip-адрес. Для этого сделаем get-запрос к сайту, который возвращяет наш IP

def checkIP():
    ip = requests.get('http://checkip.dyndns.org').content
    soup = BeautifulSoup(ip, 'html.parser')
    print(soup.find('body').text)

checkIP()

Out: Current IP Address: 82.198.191.130

Заменить свой ip через Tor можно двумя путями. Простой — через браузер, а сложный — через небольшие махинации с настройками.

Скачаем браузерный tor, чтобы лёгкий путь был совсем прост. Для сложного пути в довесок к браузеру поставим tor через консоль.

  • Linux — нам поможет команда apt-get install tor,
  • Mac — сделаем это в рамках brew, brew install tor.
  • Windows — нам поможет установка другой операционной системы.

2.3 Путь первый

Теперь запускаем свежескачанный браузер и оставляем его открытым. Менять ip нам поможет библиотека PySocks. Конечно же, её нужно установить, скопировав в терминал pip3 install PySocks.

Браузер тора по умолчанию использует порт номер 9150. В питоне при помощи библиотек socks и socket можно задать дефолтный порт для подключения. В результате текущая сессия будет использовать именно этот порт при отправке любого запроса, а значит – запросы будут посылаться из-под запущенного тора.

import socks
import socket
socks.set_default_proxy(socks.SOCKS5, "localhost", 9150)
socket.socket = socks.socksocket

Посмотрим на свой новый ip-aдрес.

checkIP()

Out: Current IP Address: 51.15.92.24

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

Ого… бывший член Европейского союза!

Попробуем обратиться к мемохранилищу с нового ip-адреса.

data_row = getMemeData('http://knowyourmeme.com/memes/doge')

for key, value in data_row.items():
    print(key.capitalize()+":", str(value)[:200], end='nn')

Out:
Name: Doge

Status: Confirmed

Type: Animal

Origin_year: 2013

Origin_place: Tumblr

Date_added: 2017-12-31T01:59:14-05:00

Views: 12318185

Videos: 59

Photos: 1645

Comments: 918

Tags: animal, dog, shiba inu, shibe, such doge, super shibe, japanese, super, tumblr, much, very, many, comic sans, photoshop meme, such, shiba, shibe doge, doges, dogges, reddit, comic sans ms, tumblr meme

About: Doge is a slang term for “dog” that is primarily associated with pictures of Shiba Inus (nicknamed “Shibe”) and internal monologue captions on Tumblr...

Бан снят. Стражники мемов ничего не заподозрили и пустили нас в сокровищницу. Чашу нашего респонса снова переполняет контент. Через силу мы обрели мощь.

При желании, можно выяснить одну занимательную вещь: при базовых настройках, Тор-браузер меняет ip каждые 10 минут. Но что делать, если сервер банит нас быстрее? Всё просто, в папке, куда был установлен Tor найдём файлик с настройками под названием torrc (на маке он лежит по адресу ~/Library/Application Support/TorBrowser-Data/torrc, если не получится найти — добро пожаловать сюда) и отредактируем его. Добавим строки:

CircuitBuildTimeout 10
LearnCircuitBuildTimeout 0
MaxCircuitDirtiness 10

Минимально возможный период для обновления ip составляет 10 секунд. Установим туда эту цифру и попробуем поиграться.

for i in range(10):
    checkIP()
    time.sleep(5)

Out:
Current IP Address: 89.31.57.5
Current IP Address: 93.174.93.71
Current IP Address: 62.210.207.52
Current IP Address: 209.141.43.42
Current IP Address: 209.141.43.42
Current IP Address: 162.247.72.216
Current IP Address: 185.220.101.17
Current IP Address: 193.171.202.150
Current IP Address: 128.31.0.13
Current IP Address: 185.163.1.11

Действительно, смена ip происходит примерно раз в 10 секунд. Для наших целей по скачке мемов было достаточно и базовых настроек. Бан наступал примерно через 20 минут после начала работы кода.

  1. Открываем браузер;
  2. Запускаем кусок кода с подгрузкой библиотек;
  3. Запускаем цикл по мемам
  4. …..
  5. Profit

Еще один цикл на ночь

final_df = pd.DataFrame(columns=['name', 'status', 'type', 'origin_year', 'origin_place',
                                 'date_added', 'views', 'videos', 'photos', 'comments', 
                                 'tags', 'about', 'origin', 'other_text'])

for page_number in tqdm_notebook(range(1075), desc='Pages'):
    # собрали хрефы с текущей страницы
    meme_links = getPageLinks(page_number)  
    for meme_link in tqdm_notebook(meme_links, desc='Memes', leave=False):
        # иногда с первого раза страничка не парсится
        for i in range(5):
            try:
                # пытаемся собрать по мему немного даты
                data_row = getMemeData(meme_link)           
                # и закидываем её в таблицу
                final_df = final_df.append(data_row, ignore_index=True)  
                # если всё получилось - выходим из внутреннего цикла
                break
            except:
                # Иначе, пробуем еще несколько раз, пока не закончатся попытки
                continue

final_df.to_csv('MEMES.csv')

Все мемы в наших руках. Можно приступать к варке фичей и моделированию. Через мощь мы познали победу. Остался только один вопрос: Что, если мы хотим менять ip каждый реквест?

2.4 Путь второй

Второй путь помогает извращаться со сменой ip как угодно. Зайдём на Github каких-то ребят и скачаем себе их скрипт под названием TorCrawler.py. Все недостающие библиотеки, используемые в этом скрипте, придётся доставить. Поставим парням на репозиторий за их код звёздочку. Закинем этот скрипт либо в папку со своими библиотеками либо в папку к этому блокноту. Обратите внимание, что скрипт работает только с третьим питоном.

Перед использованием библиотечки, нужно подкрутить настройки в torrc файлике. На маке он будет лежать в папке /usr/local/etc/tor/, на линуксе в папке /etc/tor/. Проследуем по инструкции авторов скрипта.

  1. Генерируем в консоли пароль tor --hash-password mypassword
  2. Открываем torrc файл в редакторе вроде vim, nano или atom
  3. Сохраняем пароль в наш torrc-файл в строку, которая начинается с HashedControlPassword
  4. Раскоментируем строку, начинающуюся с HashedControlPassword
  5. Раскоментируем (если она закоментирована) строку ControlPort 9051
  6. Сохраним изменения.

Запускаем tor в терминале. Линукс: service tor start, мак: tor.

Теперь мы готовы парсить.

from TorCrawler import TorCrawler
# Создаём свой краулер, в опциях вводим пароль 
crawler = TorCrawler(ctrl_pass='mypassword') 

Мы можем сделать get-запрос по аналогии с тем как делали раньше, причем ответ получим сразу в формате bs4.

meme_page = 'http://knowyourmeme.com/memes/doge'
response = crawler.get(meme_page, headers={'User-Agent': UserAgent().chrome})
type(response)

Out: bs4.BeautifulSoup

Находим внутри что-нибудь нужное.

views = response.find('dd', attrs={'class':'views'})
views

Out: 
<dd class="views" title="12,318,185 Views">
<a href="/memes/doge" rel="nofollow">12,318,185</a>
</dd>

Проверим IP адрес краулера

crawler.ip

Out: '51.15.40.23'

По дефолту краулер меняет ip каждые 25 запросов. За это отвечает параметр n_requests. При создании нового краулера, его можно настроить по собственному желанию.

crawler.n_requests

Out: 25

Более того, ip можно при желании поменять вручную.

crawler.rotate()

IP successfully rotated. New IP: 62.176.4.1

Теперь весь код для парсинга можно перевести на рельсы этого небольшого скрипта, заменив все реквесты на торовские, и менять IP адрес с каждым новым запросом. На бедный сервер внезапно обрушится огромное число запросов из всех уголков света.

Победа порвала наши оковы. Великая Сила освободила нас.

Заключение

Напоследок, хотелось бы сказать пару слов о парсинге вообще и при помощи Тора в частности. Добывать себе данные — это стильно, модно и в принципе интересно, можно получить датасеты, которых еще никто никогда не обрабатывал, сделать что-то новое, посмотреть, наконец, на все мемы мира сразу. Однако не стоит забывать, что ограничения, введенные сервером, в том числе баны, появились не просто так, а в целях защиты сайта от недоброжелательных ковровых бомбардировок запросами и DDoS-атак. К чужому труду стоит относится с уважением, и даже если у сервера никакой защиты нет, — это еще не повод неограниченно забрасывать его реквестами, особенно если это может привести к его отключению — уголовное наказание никто не отменял.

К тому же Тор — это всё-таки не первое решение, к которому стоит прибегать в случае проблем с парсингом. Иногда достаточно будет просто поставить в нужных местах time.sleep() и заполнить request-header информацией об операционной системе — в большинстве случаев этого должно быть достаточно для аккуратного сбора интересующей вас информации.

Успешных и безопасных вам исследований и да прибудет с вами Сила!

Авторы: Ульянкин Филипп filfonul, Сергеев Дмитрий Skolopendriy

Почиташки

  • Годная книга про парсинг на английском языке.
  • Неплохая инструкция о самостоятельном парсинге через Tor без использования чужих готовых классов.
  • Димин репозиторий с собранным датасетом из более чем 16k мемов, а также с их исследованием и ноутбуком с парсером.
  • Филин репозиторий с ещё парой хорошо расписаных блокнотов с парсерами. Репозиторий изначально делался как страничка факультатива для студентов.
  • Оригинальный кодекс адепта тёмной стороны силы.

Python is a rather easy-to-learn but powerful language enough to be used in a variety of applications ranging from AI and Machine Learning all the way to something as simple as creating web scraping bots. 

That said, random bugs and glitches are still the order of the day in a Python programmer’s life. In this article, we’re talking about the “urllib.error.httperror: HTTP error 403: Forbidden” when trying to scrape sites with Python and what you can to do fix the problem. 

Also read: Is Python case sensitive when dealing with identifiers?


Why does this happen?

While the error can be triggered by anything from a simple runtime error in the script to server issues on the website, the most likely reason is the presence of some sort of server security feature to prevent bots or spiders from crawling the site. In this case, the security feature might be blocking urllib, a library used to send requests to websites. 


How to fix this?

Here are two fixes you can try out. 

Disable mod_security or equivalent security features

As mentioned before, server-side security features can cause problems with web scrapers. Try setting your browser agent as follows to see if you can avoid the issue. 

from urllib.request import Request, urlopen

req = Request(
    url='enter request URL here', 
    headers={'User-Agent': 'Mozilla/5.0'}
)
webpage = urlopen(req).read()

A correctly defined browser agent should be able to scrape data from any site. 


Set a timeout

If you aren’t getting a response, try setting a timeout to prevent the server from mistaking your bot for a DDoS attacking and hence blocking all requests altogether. 

from urllib.request import Request, 
urlopen req = Request('enter request URL here', headers={'User-Agent': 'Mozilla/5.0'}) 
webpage = urlopen(req,timeout=10).read()

The aforementioned example sets a 10-second timeout between requests to not overload the server while maintaining good request frequency. 

Also read: How to fix Javascript error: ipython is not defined?

Yadullah Abidi

Someone who writes/edits/shoots/hosts all things tech and when he’s not, streams himself racing virtual cars.

You can contact him here: [email protected]

problem:
The urllib.request-urlopen () method is often used to open the source code of a web page and then analyze the source code of the page, but it will throw an “HTTP Error 403: Forbidden” exception for some websites
For example, when the following statement is executed,

 urllib.request.urlopen("http://blog.csdn.net/eric_sunah/article/details/11099295")

will appear the following exception:

  File "D:Python32liburllibrequest.py", line 475, in open
    response = meth(req, response)
  File "D:Python32liburllibrequest.py", line 587, in http_response
    'http', request, response, code, msg, hdrs)
  File "D:Python32liburllibrequest.py", line 513, in error
    return self._call_chain(*args)
  File "D:Python32liburllibrequest.py", line 447, in _call_chain
    result = func(*args)
  File "D:Python32liburllibrequest.py", line 595, in http_error_default
    raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 403: Forbidden

Analysis:
Appear abnormal, because if use urllib request. Open a URL urlopen way, the server will only receive a simple access request for the page, but the server does not know to send the request to use the browser, operating system, hardware platform, such as information, and often lack the information request are normal access, such as the crawler.
In order to prevent this abnormal access, some websites will verify the UserAgent in the request information (its information includes hardware platform, system software, application software, and user preferences). If the UserAgent is abnormal or does not exist, then the request will be rejected (as shown in the error message above).
So you can try to add the UserAgent’s information
to the request
Solution:
For Python 3.x, adding the UserAgent information to the request is simple as follows

#HTTPError: HTTP Error 403: Forbidden error appears if the following line is not added
    #The main reason is that the site is forbidden to crawl, you can add header information to the request, pretending to be a browser to access the User-Agent, specific information can be found through the Firefox FireBug plugin.
    headers = {'User-Agent':'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20100101 Firefox/23.0'}
    req = urllib.request.Request(url=chaper_url, headers=headers)
    urllib.request.urlopen(req).read()

The urllib. Request. Urlopen. Read () to replace the above code, for problems page can normal visit

olm18

0 / 0 / 0

Регистрация: 15.12.2016

Сообщений: 3

1

18.03.2019, 22:28. Показов 17902. Ответов 3

Метки 403, get запрос, requests (Все метки)


Делаю запрос. Статус запроса 403. Через браузер запрос выполнить получается. Везде пишут, что в такой ситуации надо указать User-Agent. Я указал, но никакого результата. Пробовал добавлять различные параметры в headers, но та же ошибка 403. Подскажите, что я делаю не так?

Python
1
2
3
4
5
6
7
8
9
10
import requests
url = 'https://www.mos.ru/altmosmvc/api/v1/taxi/getInfo/'
param = {'pagenumber':6}
headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36 OPR/58.0.3135.79'}
req = requests.get(url,params=param,headers=headers)
if req.status_code==200:
    print(req.json())
else:
    print(req.status_code)
    print(req.url)

Миниатюры

GET запрос с ошибкой 403
 

__________________
Помощь в написании контрольных, курсовых и дипломных работ, диссертаций здесь



0



Эксперт Python

5403 / 3827 / 1214

Регистрация: 28.10.2013

Сообщений: 9,554

Записей в блоге: 1

18.03.2019, 22:49

2

Сайту нужна кука — поэтому используйте сессию. В requests сессия есть.



0



olm18

0 / 0 / 0

Регистрация: 15.12.2016

Сообщений: 3

19.03.2019, 00:24

 [ТС]

3

Делаю так. В результате всё равно ошибка 403

Python
1
2
3
4
5
6
7
8
9
10
11
import requests
url = 'https://www.mos.ru/altmosmvc/api/v1/taxi/getInfo/'
param = {'pagenumber':10}
session = requests.Session()
session.headers['User-Agent']='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36 OPR/58.0.3135.79'
req = session.get(url,params=param)
if req.status_code==200:
    print(req.json())
else:
    print(req.status_code)
    print(req.url)

Добавлено через 59 минут
Разобрался. Вот так заработало

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
url = "https://www.mos.ru/altmosmvc/api/v1/taxi/getInfo/?Region=Москва&RegNum=&FullName=&LicenseNum=&Condition=&pagenumber=" 
header = { 
'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36', 
'upgrade-insecure-requests': '1', 
'cookie': 'mos_id=CllGxlx+PS20pAxcIuDnAgA=; session-cookie=158b36ec3ea4f5484054ad1fd21407333c874ef0fa4f0c8e34387efd5464a1e9500e2277b0367d71a273e5b46fa0869a; NSC_WBS-QUBG-jo-nptsv-WT-443=ffffffff0951e23245525d5f4f58455e445a4a423660; rheftjdd=rheftjddVal; _ym_uid=1552395093355938562; _ym_d=1552395093; _ym_isad=2' 
}
req = requests.get(url + str(1), headers = header)
if req.status_code==200:
    print(req.json())
else:
    print(req.status_code)
    print(req.url)



0



Garry Galler

Эксперт Python

5403 / 3827 / 1214

Регистрация: 28.10.2013

Сообщений: 9,554

Записей в блоге: 1

19.03.2019, 00:33

4

Лучший ответ Сообщение было отмечено olm18 как решение

Решение

Python
1
2
3
4
5
req = session.get(url,
    params=param,
    headers=headers,
    cookies={'mos_id':'CllGx1yOW5nBYizxkxtbAgA=;'}  # этот mos_id взял из ответа на тостере на точно такой же вопрос
)

Очень странно, но этот параметр из кук (несмотря на предварительный запрос) почему-то в сессионных куках отсутствует, и поэтому без него ничего не работает.



0



Понравилась статья? Поделить с друзьями:
  • Resplendence latency monitoring and auxiliary kernel library как исправить
  • Resources error zbrush
  • Resourceexhaustederror graph execution error
  • Resource panorama ошибка кс го
  • Resource panorama code pbin has invalid data как исправить кс го