<jupyter_start><jupyter_text>Day 2 - Getting Data with PythonSomething about automation and scriptsSomething about exceptions  Let's try a challenge!Error handling - or having a computer program anticipate and respond to errors created by other functions - is a big part of programming. To give you a little more practice with this, we're going to have you team up with person sitting next to you and try challenge B in the challenges directory.  Introduction to the interwebsA vast amount of data exists on the web and is now publicly available. In this section, we give an overview of popular ways to retrieve data from the web, and walk through some important concerns and considerations. ![An extremely simplified model of the web](images/Client-server-model.svg.png)The internet follows a client-server architecture, where clients (e.g. you) ask servers to do things. The most common way that you experience this is through a browser, where you enter a URL and a server sends your computer a page for your browser to render. Most of what you think about as the internet are stored documents (web pages) that are given out to anyone who asks.You probably also have a program on your computer like Outlook or Thunderbird that sends emails to a server and asks it to forward them along to someone else. You may also have proprietary software that's protected by a license, and needs to connect to a license server to verify that you are an authenticated user.Ultimately, the internet is just connecting to computers that you don't own and passing data back and forth. Because the data transfer protocol (`http`) and typical data formats (`html`) are not native to Python, we're going to leave Python just for a little bit.  Intro to HTTP requestsYou can view the request sent by your browser by:1) Opening a new tab in your browser   2) Enabling developer tools (__View -> Developer -> Developer Tools in Chrome__ and __Tools -> Web Developer -> Toggle Tools in Firefox__)  3) Loading or reloading a web page (etc. www.google.com)  4) Navigating to the Network tab in the panel that appears at the bottom of the page.    ![Chrome Examine Request Example](images/chrome_request.png) ![Firefox Examine Request Example](images/firefox_request.png) These requests you send follow the HTTP protocol (Hypertext Transfer Protocol), part of which defines the information (along with the format) the server needs to receive to return the right resources. Your HTTP request contains __headers__, which contains information that the server needs to know in order to return the right information to you.  But we're not here to wander around the web (you probably do this a lot, all on your own). You're here because you want Python to do it for you.In order to get web pages, we're going to use a python library called `requests`, which takes a lot of the fuss out of contacting servers.<jupyter_code>import requests

r = requests.get("http://en.wikipedia.org/wiki/Main_Page")<jupyter_output><empty_output><jupyter_text>This response object contains various information about the request you sent to the server, the resources returned, and information about the response the server returned to you, among other information. These are accessible through the __request__ attribute, the __content__ attribute and the __headers__ attribute respectively, which we'll each examine below.<jupyter_code>type(r.request), type(r.content), type(r.headers)<jupyter_output><empty_output><jupyter_text>Here, we can see that __request__ is an object with a custom type, __content__ is a str value and __headers__ is an object with "dict" in its name, suggesting we can interact with it like we would with a dictionary.The content is the actual resource returned to us - let's take a look at the content first before examining the request and response objects more carefully. (We select the first 1000 characters b/c of the display limits of Jupyter/python notebook.)<jupyter_code>from pprint import pprint
pprint(r.content[0:1000])<jupyter_output>(b'<!DOCTYPE html>\n<html lang="en" dir="ltr" class="client-nojs">\n<head>\n<m'
 b'eta charset="UTF-8" />\n<title>Wikipedia, the free encyclopedia</title>\n<'
 b'script>document.documentElement.className = document.documentElement.classNa'
 b'me.replace( /(^|\\s)client-nojs(\\s|$)/, "$1client-js$2" );</script>\n<scri'
 b'pt>(window.RLQ = window.RLQ || []).push(function () {\nmw.config.set({"wg'
 b'CanonicalNamespace":"","wgCanonicalSpecialPageName":false,"wgNamespaceNumber'
 b'":0,"wgPageName":"Main_Page","wgTitle":"Main Page","wgCurRevisionId":6968469'
 b'20,"wgRevisionId":696846920,"wgArticleId":15580374,"wgIsArticle":true,"wgIsR'
 b'edirect":false,"wgAction":"view","wgUserName":null,"wgUserGroups":["*"],"wgC'
 b'ategories":[],"wgBreakFrames":false,"wgPageContentLanguage":"en","wgPageCont'
 b'entModel":"wikitext","wgSeparatorTransformTable":["",""],"wgDigitTransformTa'
 b'ble":["",""],"wgDefaultDateFormat":"dmy","wgMonthNames":["","January","Febru'
 b'ary","March","April","May","June[...]<jupyter_text>The content returned is written in HTML (__H__yper__T__ext __M__arkup __L__anguage), which is the default format in which web pages are returned. The content looks like gibberish at first, with little to no spacing. The reason for this is that some of the formatting rules for the document, like its hierarchical structure, are saved in text along with the text in the document. > note - this is called the __D__ocument __O__bject __M__odel (DOM) and is the same way that markdown and LaTeX documents are writtenIf you save a web page as a ".html" file, and open the file in a text editor like Notepad++ or Sublime Text, this is the same format you'll see. Opening the file in a browser (i.e. by double-clicking it) gives you the Google home page you are familiar with.  You can inspect the information you sent to Wikipedia long with your request<jupyter_code>r.request.headers<jupyter_output><empty_output><jupyter_text>Along with the additional info that Wikipedia sent back:<jupyter_code>r.headers<jupyter_output><empty_output><jupyter_text>But you will probably not ever need this information.Most of what you'll be doing is sending what are called `GET` requests (this is why we typed in `requests.get` above). This is an `HTTP` protocol for asking a server to send you some stuff. We asked Wikipedia to `GET` us their main page. Things like queries (searching Wikipedia) also fall under `GET`.From time to time, you may also want to send information to a server (we'll do this later today). These are called `POST` requests, because you are posting something to the server (and not asking for data back).> note - From the server's perspective, the request it receives from your browser is not so different from the request received from your console (though some servers use a range of methods to determine if the request comes from a "valid" person using a browser, versus an automated program.)To have a look at the content of the web page, we can ask for the content:<jupyter_code>r.content[:1000]<jupyter_output><empty_output><jupyter_text>which gives us the response in bytes, or text:<jupyter_code>r.text[:1000]<jupyter_output><empty_output><jupyter_text>Parsing HTML in PythonTrying to parse this `str` by hand is basically a nightmare. Instead, we'll use a Python library called Beautiful Soup to turn it into something that is still confusing, but less of a nightmare.<jupyter_code>from bs4 import BeautifulSoup

page = BeautifulSoup(r.content)
page<jupyter_output>/Users/dillon/anaconda/lib/python3.5/site-packages/bs4/__init__.py:166: UserWarning: No parser was explicitly specified, so I'm using the best available HTML parser for this system ("lxml"). This usually isn't a problem, but if you run this code on another system, or in a different virtual environment, it may use a different parser and behave differently.

To get rid of this warning, change this:

 BeautifulSoup([your markup])

to this:

 BeautifulSoup([your markup], "lxml")

  markup_type=markup_type))<jupyter_text>Beautiful Soup creates a linked tree, where the root of the tree is the whole HTML document. It has children, which are all the elements of the HTML document. Each of those has children, which are any elements they have. Each element of the tree is aware of its parent and children.You probably don't want to iterate through each child of the whole HTML document - you want a specific thing or things in it. In some cases, you want to seach for html tags. Common tages include:| tag | function ||------------|------------------------------------------------------------|| `` | The title of the web page (shows up in your browser header) || `` | Information about the web page that is not shown to the user | | `` | Links to other web pages | | `` | Paragraph of text |In other cases, you want to look for IDs. These are optional information added to a tag to help developers or other code on the web page know which tag is for which purpose. Unlike tags, these are not standardized, so they will change from site to site and author to author. They will look something like:``With the advent of CSS (__C__ascading __S__tyle __S__heets), it is also common for people to define their own HTML styling tags. So, while things like lists (``) and tables (``, ``, and ``) are in the HTML specification, it's not safe to assume they'll be used when you expect.As a general strategy, when web scraping, you should have the page you want to scrape open in a browser with either the Developer Tools window open, or the HTML source displayed.We can pull out elements by tag with:<jupyter_code>page.p<jupyter_output><empty_output><jupyter_text>This is grabbing the paragraph tag from the page. If we want the first link from the first paragraph, we can try:<jupyter_code>page.p.a<jupyter_output><empty_output><jupyter_text>But what if we want all the links? We are going to use a method of bs4's elements called `find_all`.<jupyter_code>page.p.findAll('a')<jupyter_output><empty_output><jupyter_text>What if you want all the elements in that paragraph, and not just the links? bs4 has an iterator for children:<jupyter_code>for element in page.p.children:
    print(element)<jupyter_output><b><a href="/wiki/California_State_Route_78" title="California State Route 78">State Route 78</a></b>
 is a 
<a href="/wiki/State_highway" title="State highway">state highway</a>
 in 
<a href="/wiki/California" title="California">California</a>
 that runs from 
<a href="/wiki/Oceanside,_California" title="Oceanside, California">Oceanside</a>
 east to 
<a href="/wiki/Blythe,_California" title="Blythe, California">Blythe</a>
, a few miles from 
<a href="/wiki/Arizona" title="Arizona">Arizona</a>
. Its western terminus is at 
<a class="mw-redirect" href="/wiki/Interstate_5_(California)" title="Interstate 5 (California)">Interstate 5</a>
 in 
<a href="/wiki/San_Diego_County,_California" title="San Diego County, California">San Diego County</a>
 and its eastern terminus is at 
<a class="mw-redirect" href="/wiki/Interstate_10_(California)" title="Interstate 10 (California)">Interstate 10</a>
 in 
<a href="/wiki/Riverside_County,_California" title="Riverside County, California">Riverside Coun[...]<jupyter_text>HTML elements can be nested, but children only iterates at one level below the element. If you want everything, you can iterate with `descendants`<jupyter_code>for element in page.p.descendants:
    print(element)<jupyter_output><b><a href="/wiki/California_State_Route_78" title="California State Route 78">State Route 78</a></b>
<a href="/wiki/California_State_Route_78" title="California State Route 78">State Route 78</a>
State Route 78
 is a 
<a href="/wiki/State_highway" title="State highway">state highway</a>
state highway
 in 
<a href="/wiki/California" title="California">California</a>
California
 that runs from 
<a href="/wiki/Oceanside,_California" title="Oceanside, California">Oceanside</a>
Oceanside
 east to 
<a href="/wiki/Blythe,_California" title="Blythe, California">Blythe</a>
Blythe
, a few miles from 
<a href="/wiki/Arizona" title="Arizona">Arizona</a>
Arizona
. Its western terminus is at 
<a class="mw-redirect" href="/wiki/Interstate_5_(California)" title="Interstate 5 (California)">Interstate 5</a>
Interstate 5
 in 
<a href="/wiki/San_Diego_County,_California" title="San Diego County, California">San Diego County</a>
San Diego County
 and its eastern terminus is at 
<a class="mw-redirect" href[...]<jupyter_text>This splits out formatting tags that we *probably* don't care about, like bold-faced text, and so we probably won't use it again.In reality, you won't be inspecting things yourself, so you'll want to get in the habit of using your knowledge from day 2 about looping and control structures to make decisions for you. For example, what if we wanted to look at every link in the page, then print it's neighbor but only if the link is not to a media file? We could do something like:<jupyter_code>for link in page.find_all('a'):
    if link.attrs.get('class') != 'mw-redirect':
        print(link.find_next())<jupyter_output><div id="siteNotice"><!-- CentralNotice --></div>
<a href="#p-search">search</a>
<div class="mw-content-ltr" dir="ltr" id="mw-content-text" lang="en"><table id="mp-topbanner" style="width:100%; background:#f9f9f9; margin:1.2em 0 6px 0; border:1px solid #ddd;">
<tr>
<td style="width:61%; color:#000;">
<table style="width:280px; border:none; background:none;">
<tr>
<td style="width:280px; text-align:center; white-space:nowrap; color:#000;">
<div style="font-size:162%; border:none; margin:0; padding:.1em; color:#000;">Welcome to <a href="/wiki/Wikipedia" title="Wikipedia">Wikipedia</a>,</div>
<div style="top:+0.2em; font-size:95%;">the <a href="/wiki/Free_content" title="Free content">free</a> <a href="/wiki/Encyclopedia" title="Encyclopedia">encyclopedia</a> that <a href="/wiki/Wikipedia:Introduction" title="Wikipedia:Introduction">anyone can edit</a>.</div>
<div id="articlecount" style="font-size:85%;"><a href="/wiki/Special:Statistics" title="Special:Statistics">5,104,889</a> articles [...]<jupyter_text>Time for a challenge!To make sure that everyone is on the same page (and to give you a little more practice dealing with HTML), let's partner up with the person next to you and try challenge A, on using html, in the challenges directory.  Creating data with web APIsMost people who think they want to do web scraping actually want to pull data down from site-supplied APIs. Using an API is better in almost every way, and really the only reason to scrape data is if:1. The website was constructed in the 90s and does not have an API; or,2. You are doing something illegalIf [LiveJournal has an API](http://dev.livejournal.com/), the website you are interested in probably does too. What is an API?**API** is shorthand for **A**pplication **P**rogramming **I**nterface, which is in turn computer-ese for a middleman.Think about it this way. You have a bunch of things on your computer that you want other people to be able to look at. Some of them are static documents, some of them call programs in real time, and some of them are programs themselves. Solution 1You publish login credentials on the internet, and let anyone log into your computerProblems:1. People will need to know how each document and program works to be able to access their data2. You don't want the world looking at your browser history Solution 2You paste everything into HTML and publish it on the internetProblems:1. This can be information overload2. Making things dynamic can be tricky Solution 3You create a set of methods to act as an intermediary between the people you want to help and the things you want them to have access to.Why this is the best solution:1. People only access what you want them to have, in the way that you want them to have it2. People use one language to get the things they wantWhy this is still not Panglossian:1. You will have to explain to people how to use your middleman  Twitter's APITwitter has an API - mostly written for third-party apps - that is comparatively straightforward and gives you access to _nearly_ all of the information that Twitter has about its users, including:1. User histories2. User (and tweet) location3. User language4. Tweet popularity5. Tweet spread6. Conversation chainsAlso, Twitter returns data to you in json, or **J**ava **S**cript **O**bject **N**otation. This is a very common format for passing data around http connections for browsers and servers, so many APIs return it as a datatype as well (instead of using something like xml or plain text).Luckily, json converts into native Python data structures. Specifically, every json object you get from Twitter will be a combination of nested `dicts` and `lists`, which you learned about yesterday. This makes Twitter a lot easier to manipulate in Python than html objects, for example.Here's what a tweet looks like:<jupyter_code>import json

with open('../data/02_tweet.json','r') as f:
    a_tweet = json.loads(f.read())<jupyter_output><empty_output><jupyter_text>We can take a quick look at the structure by pretty printing it:<jupyter_code>from pprint import pprint

pprint(a_tweet)<jupyter_output>{'contributors': None,
 'coordinates': None,
 'created_at': 'Thu Apr 02 06:09:39 +0000 2015',
 'entities': {'hashtags': [], 'symbols': [], 'urls': [], 'user_mentions': []},
 'favorite_count': 0,
 'favorited': False,
 'geo': None,
 'id': 583511591334719488,
 'id_str': '583511591334719488',
 'in_reply_to_screen_name': None,
 'in_reply_to_status_id': None,
 'in_reply_to_status_id_str': None,
 'in_reply_to_user_id': None,
 'in_reply_to_user_id_str': None,
 'lang': 'ht',
 'place': None,
 'retweet_count': 0,
 'retweeted': False,
 'source': '<a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>',
 'text': '.IPA rettiwT eht tuoba nraeL',
 'truncated': False,
 'user': {'contributors_enabled': False,
          'created_at': 'Thu Apr 02 05:54:25 +0000 2015',
          'default_profile': True,
          'default_profile_image': False,
          'description': '',
          'entities': {'description': {'urls': []}},
          'favourites_count': 0,
          'follow_request_sent': Fals[...]<jupyter_text>Time for a challenge!Let's see how much you remember about lists and dicts from yesterday. Go into the challenges directory and try your hand at `02_scraping/C_json.py`.  AuthenticationTwitter controls access to their servers via a process of authentication and authorization. Authentication is how you let Twitter know who you are, in a way that is very hard to fake. Authorization is how the account owner (which will usually be yourself unless you are writing a Twitter app) controls what you are allowed to do in Twitter using their account. In Twitter, different levels of authorization require different levels of authentication. Because we want to be able to interact with everything, we'll need the highest level of authorization and the strictest level of authentication. In Twitter, this means that we need two sets of ID's (called keys or tokens) and passwords (called secrets):* consumer_key* consumer_secret* access_token_key* access_token_secretWe'll provide some for you to use, but if you want to get your own you need to create an account on Twitter with a verified phone number. Then, while signed in to your Twitter account, go to: https://apps.twitter.com/. Follow the prompts to generate your keys and access tokens. Note that getting the second ID/password pair requires that you manually set the authorization level of your app.We've stored our credentials in a separate file, which is smart. However, we have uploaded it to Github so that you have them too, which is not smart. **You should NEVER NEVER NEVER do this in real life.**We've stored it in YAML format, because it is more human-readible than JSON is. However, once it's inside Python, these data structures behave the same way.<jupyter_code>import yaml

with open('../etc/creds.yml', 'r') as f:
    creds = yaml.load(f)<jupyter_output><empty_output><jupyter_text>We're going to load these credentials into a requests module specifically designed for handling the flavor of authentication management that Twitter uses.<jupyter_code>from requests_oauthlib import OAuth1Session

twitter = OAuth1Session(**creds)<jupyter_output><empty_output><jupyter_text>That `**` syntax we just used is called a "double splat" and is a python convenience function for converting the key-value pairs of a dictionary into keyword-argument pairs to pass to a function.  Accessing the API Access to Twitter's API is organized through URLs called "endpoints". An endpoint is the location at which you can submit a request for Twitter to do something for you.For example, the "endpoint" to search for specific kinds of tweets is at:```https://api.twitter.com/1.1/search/tweets.json```whereas posting new tweets is at:```https://api.twitter.com/1.1/statuses/update.json```For more information on the REST APIs, end points, and terms, check out: https://dev.twitter.com/rest/public. For the Streaming APIs: https://dev.twitter.com/streaming/overview.All APIs on Twitter are "rate-limited" - this means that you are only allowed to ask a set number of questions per unit time (to keep their servers from being overloaded). This rate varies by endpoint and authorization, so be sure to check their developer site for the action you are trying to take.For example, at the lowest level of authorization (Twitter calls this `application only`), you are allowed to make 450 search requests per 15 minute window, or about one every two seconds. At the highest level of authorization (Twitter calls this `user`) you can submit 180 requests every 15 minutes, or only about once every five seconds.> side note - Google search is the worst rate-limiting I've ever seen, with an allowance of one hundred requests per day, or about once every *nine hundred seconds*Let's try a couple of simple API queries. We're going to specify query parameters with `param`.<jupyter_code>search = "https://api.twitter.com/1.1/search/tweets.json"

r = twitter.get(search, params={'q' : 'technology'})<jupyter_output><empty_output><jupyter_text>This has returned an http response object, which contains data like whether or not the request succeeded:<jupyter_code>r.ok<jupyter_output><empty_output><jupyter_text>You can also get the http response code, and the reason why Twitter sent you that code (these are all super important for controlling the flow of your program).<jupyter_code>r.status_code, r.reason<jupyter_output><empty_output><jupyter_text>The data that we asked Twitter to send us in r.content<jupyter_code>r.content<jupyter_output><empty_output><jupyter_text>But that's not helpful. We can extract it in python's representation of json with the `json` method:<jupyter_code>r.json()<jupyter_output><empty_output><jupyter_text>This has some helpful metadata about our request, like a url where we can get the next batch of results from Twitter for the same query:<jupyter_code>data = r.json()
data['search_metadata']<jupyter_output><empty_output><jupyter_text>The tweets that we want are under the key "statuses"<jupyter_code>statuses = data['statuses']
statuses[0]<jupyter_output><empty_output><jupyter_text>This is one tweet.> Depending on which tweet this is, you may or may not see that Twitter automatically pulls out links and mentions and gives you their index location in the raw tweet stringTwitter gives you a whole lot of information about their users, including geographical coordinates, the device they are tweeting from, and links to their photographs. Twitter supports what it calls query operators, which modify the search behavior. For example, if you want to search for tweets where a particular user is mentioned, include the at-sign, `@`, followed by the username. To search for tweets sent to a particular user, use `to:username`. For tweets from a particular user, `from:username`. For hashtags, use `hashtag`.For a complete set of options: https://dev.twitter.com/rest/public/search.Let's try a more complicated search:<jupyter_code>r = twitter.get(search, params={
        'q' : 'happy',
        'geocode' : '37.8734855,-122.2597169,10mi'
    })
r.ok
statuses = r.json()['statuses']
statuses[0]<jupyter_output><empty_output><jupyter_text>If we want to store this data somewhere, we can output it as json using the json library from above. However, if you're doing a lot of these, you'll probaby want to use a database to handle everything.<jupyter_code>with open('my_tweets.json', 'w') as f:
    json.dump(statuses, f)<jupyter_output><empty_output><jupyter_text>To post tweets, we need to use a different endpoint:<jupyter_code>post = "https://api.twitter.com/1.1/statuses/update.json"<jupyter_output><empty_output><jupyter_text>And now we can pass a new tweet (remember, Twitter calls these 'statuses') as a parameter to our post request.<jupyter_code>r = twitter.post(post, params={
        'status' : "I stole Juan's Twitter credentials"
    })
r.ok<jupyter_output><empty_output><jupyter_text>Other (optional) parameters include things like location, and replies.  Scheduling The real beauty of bots is that they are designed to work without interaction or oversight. Imagine a situation where you want to automatically retweet everything coming out of the D-Lab's twitter account, "@DLabAtBerkeley". You could:1. spend the rest of your life glued to D-Lab's twitter page and hitting refresh; or,2. write a functionWe're going to import a module called `time` that will pause our code, so that we don't hit Twitter's rate limit<jupyter_code>import time

def retweet():
    r = twitter.get(search, {'q':'DLabAtBerkeley'})
    if r.ok:
        statuses = r.json()['statuses']
        for update in statuses:
            username = item['user']['screen_name']
            parameters = {'status':'HOORAY! @' + username}
            r = twitter.post(post, parameters)
            print(r.status_code, r.reason)
            time.sleep(5)<jupyter_output><empty_output><jupyter_text>But you are a human that needs to eat, sleep, and be social with other humans. Luckily, Linux systems have a time-based daemon called `cron` that will run scripts like this *for you*. > People on windows and macs will not be able to run this. That's okay.The way that `cron` works is it reads in files where each line has a time followed by a job (these are called cronjobs). You can edit your crontab by typing `crontab -e` into a terminal.They looks like this:<jupyter_code>with open('../etc/crontab_example', 'r') as f:
    print(f.read())<jupyter_output># In a user's crontab, jobs run under that user
# Time is specified as <min> <hour> <day> <month> <wday>
# To specify any time, use `*`
# For unknown reasons, cronjobs fail unless the tab ends with a newline

00 08 * * 1 echo "It is 8am on Monday" >> /var/dumblog<jupyter_text>This is telling `cron` to print that statement to a file called "dumblog" at 8am every Monday.It's generally frowned upon to enter jobs through crontabs because they are hard to modify without breaking them. The better solution is to put your timed command into a file and copy the file into `/etc/cron.d/`. These files look like this:<jupyter_code>with open('../etc/crond_example', 'r') as f:
    print(f.read())<jupyter_output>#!/bin/bash
# First, make sure you specify all of the paths that you might need to run
# your task. If you aren't sure, copy the entire $PATH variable

PATH=/home/dillon/.conda/envs/py27/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# Then, specify when you want the task to occur; the user account to run it;
# and the job

@hourly dillon cd ~/scripts; python simple.py
{'fraction_non_alphanumeric': 0.09176428185258605, 'fraction_numerical': 0.010524418814200756, 'mean_word_length': 5.089991220368744, 'pattern_counts': {'":': 21, '<': 206, '<?xml version=': 0, '>': 216, 'https://': 8, 'lorem ipsum': 0, 'www.': 1, 'xml': 3}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '32', 'dir': 'jupyter-structured-clean-dedup', 'id': '148281', 'license': 'BSD-2-Clause', 'n_tokens_mistral': 8079, 'n_tokens_neox': 7556, 'n_words': 3537, 'path': 'instructor/day_two.ipynb', 'repo_name': 'deniederhut/workshop_pyintensive', 'size': '27745'}