Copy disabled (too large)
Download .txt
Showing preview only (16,185K chars total). Download the full file to get everything.
Repository: serp-spider/search-engine-google
Branch: master
Commit: 9f889148e8b3
Files: 156
Total size: 15.4 MB
Directory structure:
gitextract_vwfvhtj1/
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── build/
│ └── .gitignore
├── composer.json
├── phpcs.xml
├── phpunit.dist.xml
├── src/
│ ├── AdwordsResultItem.php
│ ├── AdwordsResultType.php
│ ├── AdwordsSectionResultSet.php
│ ├── Exception/
│ │ ├── GoogleCaptchaException.php
│ │ └── InvalidDOMException.php
│ ├── GoogleClient.php
│ ├── GoogleUrl.php
│ ├── GoogleUrlArchive.php
│ ├── GoogleUrlInterface.php
│ ├── GoogleUrlTrait.php
│ ├── NaturalResultType.php
│ ├── Page/
│ │ ├── GoogleCaptcha.php
│ │ ├── GoogleDom.php
│ │ ├── GoogleError.php
│ │ ├── GoogleSerp.php
│ │ └── NotFound.php
│ └── Parser/
│ ├── AbstractAdwordsParser.php
│ ├── AbstractParser.php
│ ├── Evaluated/
│ │ ├── AdwordsParser.php
│ │ ├── AdwordsSectionParser.php
│ │ ├── MobileAdwordsParser.php
│ │ ├── MobileAdwordsSectionParser.php
│ │ ├── MobileNaturalParser.php
│ │ ├── NaturalParser.php
│ │ └── Rule/
│ │ ├── Adwords/
│ │ │ ├── AdwordsItem.php
│ │ │ ├── AdwordsItemMobile.php
│ │ │ └── Shopping.php
│ │ └── Natural/
│ │ ├── AnswerBox.php
│ │ ├── Classical/
│ │ │ ├── ClassicalCardsResult.php
│ │ │ ├── ClassicalCardsResultO9g5cc.php
│ │ │ ├── ClassicalCardsResultZ1m.php
│ │ │ ├── ClassicalCardsResultZINbbc.php
│ │ │ ├── ClassicalCardsVideoResult.php
│ │ │ ├── ClassicalResult.php
│ │ │ └── ClassicalWithLargeVideo.php
│ │ ├── ComposedTopStories.php
│ │ ├── Divider.php
│ │ ├── Flight.php
│ │ ├── ImageGroup.php
│ │ ├── ImageGroupCarousel.php
│ │ ├── InTheNews.php
│ │ ├── KnowledgeCard.php
│ │ ├── Map.php
│ │ ├── MapLegacy.php
│ │ ├── MapMobile.php
│ │ ├── PeopleAlsoAsk.php
│ │ ├── SearchResultGroup.php
│ │ ├── TopStoriesCarousel.php
│ │ ├── TopStoriesVertical.php
│ │ ├── TweetsCarousel.php
│ │ ├── TweetsCarouselZ1m.php
│ │ └── VideoGroup.php
│ ├── ParserInterface.php
│ └── ParsingRuleInterface.php
├── stubs/
│ └── RelatedSearch.php
└── test/
├── bin/
│ ├── ci.bash
│ ├── phpcbf.bash
│ ├── phpcs.bash
│ └── test.bash
├── resources/
│ ├── pages-evaluated/
│ │ ├── 2018/
│ │ │ ├── 03/
│ │ │ │ ├── asian+massage.html
│ │ │ │ ├── plumber+london.html
│ │ │ │ ├── qui+est+homer+simpsons.html
│ │ │ │ └── super+u+paris.html
│ │ │ ├── 07/
│ │ │ │ └── foo+bar(page2).html
│ │ │ ├── 09/
│ │ │ │ └── 65b6be0a-7619-4018-97c9-989cdec53319.html
│ │ │ ├── 10/
│ │ │ │ ├── agence+web+nantes.html
│ │ │ │ └── who+is+homer+simpson.html
│ │ │ └── 11/
│ │ │ └── acheter+kobo.html
│ │ ├── 2019/
│ │ │ ├── 05/
│ │ │ │ └── plombier+paris.html
│ │ │ └── 07/
│ │ │ └── cheap+video+editing+software+mac.html
│ │ ├── adwords/
│ │ │ └── simpsons+poster.html
│ │ ├── alarmas+para+casa.html
│ │ ├── captcha.html
│ │ ├── cards-design.html
│ │ ├── flights.html
│ │ ├── github(with-vertical-top-stories).html
│ │ ├── github.html
│ │ ├── how+is+homer+simpsons.html
│ │ ├── inde(top-stories).html
│ │ ├── narendra+modi.html
│ │ ├── page-with-bkgrouped-results.html
│ │ ├── ransomware.html
│ │ ├── shop-near-paris.html
│ │ ├── simpsons(related).html
│ │ ├── simpsons+donut.html
│ │ ├── simpsons+movie+trailer.html
│ │ ├── simpsons.html
│ │ ├── simpsonsworld.html
│ │ └── with-DOMText.html
│ ├── pages-mobile/
│ │ ├── 2017/
│ │ │ ├── 11/
│ │ │ │ ├── buy+pen.html
│ │ │ │ └── donald+trump.html
│ │ │ └── 12/
│ │ │ ├── foo.html
│ │ │ └── who+is+homer+simpsons.html
│ │ ├── 2018/
│ │ │ ├── 03/
│ │ │ │ └── foo.html
│ │ │ ├── 07/
│ │ │ │ └── simpsons-episode-1.html
│ │ │ ├── 08/
│ │ │ │ └── new+construction+ct.html
│ │ │ └── 11/
│ │ │ └── 01/
│ │ │ └── plombier+nantes.html
│ │ ├── simpsons+donuts.html
│ │ ├── simpsons+homer.html
│ │ └── simpsons+world.html
│ ├── pages-raw/
│ │ └── simpsons.html
│ └── simple-dom.html
└── suites/
├── AdwordsResultItemTest.php
├── AdwordsSectionResultSetTest.php
├── GoogleClientTest.php
├── GoogleSerpTestCase.php
├── GoogleUrlTest.php
├── Page/
│ ├── GoogleCaptchaTest.php
│ ├── GoogleDomTest.php
│ └── GoogleSerpTest.php
└── Parser/
└── Evaluated/
├── AdwordsParserTest.php
├── NaturalParserTest.php
└── natural-parser-data/
├── 2018/
│ ├── 03/
│ │ ├── asian+massage.yml
│ │ ├── plumber+london.yml
│ │ ├── qui+est+homer+simpsons.yml
│ │ └── super+u+paris.yml
│ ├── 07/
│ │ └── foo+bar(page2).yml
│ ├── 09/
│ │ └── 65b6be0a-7619-4018-97c9-989cdec53319.yml
│ ├── 10/
│ │ ├── agence+web+nantes.yml
│ │ └── who+is+homer+simpson.yml
│ └── 11/
│ └── acheter+kobo.yml
├── 2019/
│ ├── 05/
│ │ └── plombier+paris.yml
│ └── 07/
│ └── cheap+video+editing+software+mac.yml
├── github(with-vertical-top-stories).yml
├── inde(top-stories).yml
├── mobile/
│ ├── 2017/
│ │ ├── 11/
│ │ │ ├── buy+pen.yml
│ │ │ └── mobile-donald+trump.yml
│ │ └── 12/
│ │ ├── foo.yml
│ │ └── who+is+homer+simpsons.yml
│ └── 2018/
│ ├── 03/
│ │ └── foo.yml
│ ├── 07/
│ │ └── simpsons-episode-1.yml
│ ├── 08/
│ │ └── new-construction-ct.yml
│ └── 11/
│ └── 01/
│ └── plombier+nantes.yml
├── mobile-simpsons+donuts.yml
├── mobile-simpsons+homer.yml
├── mobile-simpsons+world.yml
├── narendra+modi.yml
├── natural-cards.yml
├── naturals-data-with_bkgroups.yml
├── naturals-data.yml
├── ransomware.yml
├── simpsons+donuts.yml
├── simpsons+movie+trailer.yml
└── simpsonsworld.yml
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
; This file is for unifying the coding style for different editors and IDEs.
; More information at http://editorconfig.org
root = true
[*]
charset = utf-8
indent_size = 4
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.json]
indent_size = 2
[*.yml]
indent_size = 2
[*.css]
indent_size = 2
[*.scss]
indent_size = 2
================================================
FILE: .gitattributes
================================================
/build export-ignore
/script export-ignore
/test export-ignore
.codeclimate.yml export-ignore
.editorconfig export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.travis.yml export-ignore
phpunit.dist.xml export-ignore
phpcs.xml export-ignore
/test/resources/* linguist-vendored
================================================
FILE: .gitignore
================================================
composer.lock
/vendor
.idea
================================================
FILE: .travis.yml
================================================
language: php
dist: trusty
sudo: false
matrix:
include:
- php: 5.5
env: PROCESS_CODECLIMATE=true
- php: 5.6
- php: 7.0
- php: 7.1
- php: nightly
- php: hhvm
env: IGNORE_XDEBUG=true
fast_finish: true
allow_failures:
- php: nightly
before_script:
- if [ -z "$IGNORE_XDEBUG" ];then phpenv config-rm xdebug.ini; fi
- travis_retry composer self-update
- travis_retry composer update --prefer-dist
- if [ -n "$PROCESS_CODECLIMATE" ];then echo 'zend_extension=xdebug.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; fi
script: ./test/bin/ci.bash summary
env:
global:
secure: kBRY5+RxotmB1NwI1GVJ9ndgsANkyuvMSrYtaDqYFdLo5c5Eow0HE+g8hpTkUCGethUz+/b5UzXl/w+ocCl3n4T17MSceLy1cY0gCTG+drRV+vAn8SOHx6QWB2Zp2ixFtQrv6ivOHq8Rjh244jkyDZrnNRjMHUkdUteSOC10o1bBzNpZ2iNR/Tnzr1uU4MilRCq9u2/QtrPZppu2ki0/h0vtb8K0Zi84S8fWHP+Qf8hMgsGTnOl3jhQHwEA/C+OEHAt3V0cxHIZ/rV6CC6k1c91XvCk4kI0gk2C14DIax5TDtvxif+SmlB2nDjRYbeHyTLuiXxJAXttjQ7mWB4w0D3YtxxLLu3h7CPnZjp+BF6KVLfXWu1T2+/WD/snvGChqkwLB/Z2VsTbxzYlXbZSIp13xXDKJa16jH2tWl3mr6o0UsuZq46zBERemoL3LWzj1wy6o2XJvSrbir/AaLZI5OADYhK7ZST95aHnm9QykKQzGNWVBuvYBPjIOQIrqnRGZSBQWMqVZQLjhEr7TR2GenGRdE8gvh3gWbP2yO8sCUUZO7klDSIrnIgMjvawtDautixoqmH9wlXUZgX1H8K2mBWc7ZoxIaVFBUhrv7+fHKM3zoqg6cBXhhAtgYQu5YieaZv9gnl8B77arfriARMAsKsn0Wh/nZP40AwUAk+ZFSps=
cache:
directories:
- $HOME/.composer/cache
================================================
FILE: CHANGELOG.md
================================================
# CHANGELOG
## 0.4.7
*2018-18-05*
* Bug fix:
* Fixed title for adwords results
## 0.4.6
*2018-30-10*
* Feature:
* Added adwords results for mobiles
* Added map results for mobiles
* Bug fix:
* Fixed description for mobile classical
* Fixed related searches for mobile
* Other:
* Added ``ext-dom`` in composer.json
## 0.4.5
*2018-30-10*
* Bug fix:
* Fixed local pack (#113) Thanks @Human018
* Fixed urls for ads results
## 0.4.4
*2018-10-22*
* Bug fix:
* Fixed people also ask update
## 0.4.3
*2018-09-17*
* Bug fix:
* Fix google dom update on classical results
## 0.4.2
*2018-08-05*
* Bug fix:
* fix mobile serps (#106)
## 0.4.1
*2018-07-05*
* Bug fix:
* fixed multiple mobile issues on mobile results
* fixed parsing for number of results (#100) - thanks @migliori
* fixed related searches on desktop - thanks @gudix
## 0.4.0
*2018-05-29*
* Bug fix:
* fixed the captcha exception. The right exception is now returned when a captcha is found
* fixed invalid type hinting causing errors with hhvm
* Google updates:
* **bc break** removed support for image captcha as google now uses recaptcha
* Other:
* When an invalid classical result is found, throw an exception instead of returning invalid results causing fatal errors.
## 0.3.0
*2018-04-04*
* Dependencies
* **bc break** use version ``0.3.x`` of ``serps/core``
* Updates
* **bc break** google url default domain is now ``"www.google.com`` instead of ``google.com``. This way we avoid extra redirects too the ``"www"`` subdomain.
* Fix a bug with search result group parser that was triggering a php error.
* Dom Updates
* Fix parsing for classical results on mobiles.
* Fix parsing for knowledge cards on mobiles.
## 0.2.5
*2018-03-29*
* Bug fix:
* Fix a bug with map results introduced in version 0.2.4 see [#94](https://github.com/serp-spider/search-engine-google/issues/94)
## 0.2.4
*2018-03-22*
* Bug fix:
* Fix google update for map results
* Fix google update for "destination" data in classical results
* Fix google update for People Also Ask
* Fix google update for answer box [#90](https://github.com/serp-spider/search-engine-google/issues/90)
## 0.2.3
*2017-12-11*
* Features:
* Added parsing for people also ask results [#70](https://github.com/serp-spider/search-engine-google/issues/70)
* Bug fix:
* Fix some mobile card results not parsing [#83](https://github.com/serp-spider/search-engine-google/issues/83)
## 0.2.2
*2017-11-25*
* Bug fix:
* Parse ``bkWMgd`` groups (thanks to [Shiftas](https://github.com/Shiftas)) [#76](https://github.com/serp-spider/search-engine-google/issues/76)
* Fix result count [#76](https://github.com/serp-spider/search-engine-google/issues/76)
* Fix some mobile card results not parsing [#79](https://github.com/serp-spider/search-engine-google/issues/79) and [#78](https://github.com/serp-spider/search-engine-google/issues/78)
* Fix twitter carousel parser for mobile [#81](https://github.com/serp-spider/search-engine-google/issues/81)
* Fix related searches for mobile [#80](https://github.com/serp-spider/search-engine-google/issues/81)
* Features:
* Parsing for "composed top stories" and standardizing old "top stories" [#67](https://github.com/serp-spider/search-engine-google/issues/67)
* Other:
* Dependency to serps/core was updated from ~0.2.0 to ~0.2.4
## 0.2.1
*2017-07-16*
* Features:
* Parsing for mobile knowledge results (fd95ffc07c137223e36fade739b4617c17fe6758)
* Bug fix
* Fixing tweet carousel recognition (4f681da0435454b5ff592c657789010ccf8361ee)
* Fixing tweet carousel non linked to an user
## 0.2.0
*2017-05-01*
* Breaking Changes:
* Images data are returned MediaInterface [#35](https://github.com/serp-spider/search-engine-google/issues/35)
* Drop support for raw parser [5f41ddeb6a9076b363a83071e0f27a0254f1e330](https://github.com/serp-spider/search-engine-google/commit/5f41ddeb6a9076b363a83071e0f27a0254f1e330)
* ``Serps\SearchEngine\Google\GoogleDom`` now extends ``Serps\Core\Dom\WebPage`` [dafe67e](https://github.com/serp-spider/search-engine-google/commit/dafe67eeae3eb46bb570fdc3eadd22d4abe47b7d)
* ``Serps\SearchEngine\Google\GoogleError`` now extends ``Serps\Core\Dom\WebPage``
and does not extend ``Serps\SearchEngine\Google\GoogleDom`` anymore [dafe67e](https://github.com/serp-spider/search-engine-google/commit/dafe67eeae3eb46bb570fdc3eadd22d4abe47b7d)
* Class ``Serps\SearchEngine\Google\Css`` was removed and an equivalent is now provided from the core package in
``Serps\Core\Dom\Css`` [4e5b1a1](https://github.com/serp-spider/search-engine-google/commit/4e5b1a193abfe5093a48152b12878e7cef022b7b)
* Vendor ``symfony/css-selector`` is not provided anymore, instead it moved in core package [4e5b1a1](https://github.com/serp-spider/search-engine-google/commit/4e5b1a193abfe5093a48152b12878e7cef022b7b)
* ``GoogleClient::query($googleUrl, $proxy, $cookieJar)`` was refactored
to ``GoogleClient::query($googleUrl, $browser)`` in order to provide a more fluent management
of browser specifications [a6fe671](https://github.com/serp-spider/search-engine-google/commit/a6fe6711d6fac42977cfc30212e438d8ab933584)
* ``GoogleClient::query`` does not auto set language header anymore, that's now done from the browser instance [a6fe671](https://github.com/serp-spider/search-engine-google/commit/a6fe6711d6fac42977cfc30212e438d8ab933584)
* ``GoogleClient::request`` and ``GoogleClient::getRequestBuilder()`` were removed and are replaced with
browser implementation [a6fe671](https://github.com/serp-spider/search-engine-google/commit/a6fe6711d6fac42977cfc30212e438d8ab933584)
* class ``Serps\SearchEngine\Google\GoogleClient\RequestBuilder`` was removed
* fix the typo in the interface name ``ParsingRuleInterace`` that is now ``ParsingRuleInterface``
* Method ``ParsingRuleInterace::match(GoogleDom $dom, \DOMElement $node)``
is now ``ParsingRuleInterace::match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)``
* the property ``is_carousel`` from top stories is now named ``isCarousel``
* Features:
* Google cards results are now supported [#38](https://github.com/serp-spider/search-engine-google/pull/38)
* Mobile page detection: GoogleSerp::isMobile() [564057ce0ee255cfa138440e033776b85f239acb](https://github.com/serp-spider/search-engine-google/commit/564057ce0ee255cfa138440e033776b85f239acb)
* Mobile results have now their own parser
* Parsing rule for mobile video groups [#41](https://github.com/serp-spider/search-engine-google/issues/41)
* Parsing rule for mobile image groups
* Bug fixes:
* Large video have the CLASSICAL type as mentioned in the doc [#36](https://github.com/serp-spider/search-engine-google/issues/36)
================================================
FILE: CONTRIBUTING.md
================================================
CONTRIBUTING
============
Any contribution is welcome.
Issue
-----
If you encounter an issue with the library you are encouraged to report it and we can work together on solving it.
Before submitting the issue please make sure you are using the latest version of serps/search-engine-google.
If you are and if you still have the issue you can report it
on the [issue tracker](https://github.com/serp-spider/search-engine-google/issues/new).
When reporting an issue try to provide as much details as possible. If the issue is related to a page that cannont
parse correctly, the following details will be very helpful to fix the issue:
- The url that fails to parse
- What is expected
- What you are getting
- You might attach the html of the page that does not parse
- You might send screenshot of what is notparsing correctly
Code contribution
-----------------
### Tests
All contributions must be tested following as much as possible the current test structure.
Look at current tests in ``test/suites`` for more details.
### Coding Standards
The code follows the PSR-2 coding standards.
We provided two useful commands to check and fix automatically code standards:
- Checking standards: ``composer cscheck``
- Fixing standards: ``composer csfix``
### Tools
- run full test suit: ``composer test``
- run some tests only: ``composer test testName``
(``testName`` will be used in [phpunit --filter](https://phpunit.de/manual/current/en/textui.html#textui.examples.filter-patterns))
================================================
FILE: LICENSE
================================================
Copyright (c) 2015-today, Soufiane GHZAL <sghzal@gmail.com> and contributors
Usage of the works is permitted provided that this instrument is retained with the works, so that any entity that uses the works is notified of this instrument.
DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY.
================================================
FILE: README.md
================================================
SERPS - Search Engine: Google
=============================
[](https://travis-ci.org/serp-spider/search-engine-google)
[](https://codeclimate.com/github/serp-spider/search-engine-google/coverage)
[](https://packagist.org/packages/serps/search-engine-google)
[](https://packagist.org/packages/serps/search-engine-google)
This is the Google implementation for [SERPS](https://github.com/serp-spider/serps)
Install
-------
Install it through [composer](https://getcomposer.org/) with the package
[serps/search-engine-google](https://packagist.org/packages/serps/search-engine-google) :
``composer require --prefer-dist 'serps/search-engine-google'``
Note: it is recommended to use ``--prefer-dist`` to avoid downloading html test files.
Documentation
-------------
Browse the [documentation](http://serp-spider.github.io/documentation/search-engine/google/)
Disclaimer
-----------
> Using our Services does not give you ownership of any intellectual property rights in
> our Services or the content you access.
> You may not use content from our Services unless you obtain permission from its owner or
> are otherwise permitted by law
>
> Extract from [Google terms of services](https://www.google.com/policies/terms/)
When using this software must respect terms of services of third parties like Google.
Serps authors and contributors cannot be hold as liable for the use you make of this software.
================================================
FILE: build/.gitignore
================================================
*
!.gitignore
================================================
FILE: composer.json
================================================
{
"name": "serps/search-engine-google",
"description": "Google Rules and client for SERPS",
"type": "library",
"keywords": ["SERPS", "Google"],
"homepage": "https://github.com/serp-spider/search-engine-google",
"license": "Fair",
"minimum-stability": "dev",
"prefer-stable": true,
"authors": [
{
"name": "Soufiane GHZAL",
"homepage": "https://github.com/gsouf"
}
],
"autoload":{
"psr-4" : {
"Serps\\SearchEngine\\Google\\": "src/"
}
},
"autoload-dev":{
"psr-4" : {
"Serps\\Test\\SearchEngine\\Google\\": "test/suites"
}
},
"require": {
"php": ">=5.5",
"serps/core": "~0.3.0",
"ext-dom": "*"
},
"require-dev":{
"phpunit/phpunit": "~4.1",
"symfony/yaml": ">=2.0",
"squizlabs/php_codesniffer": "~3.2",
"guzzlehttp/psr7": "^1.4",
"serps/cli": "^1.1"
},
"suggest": {
"zendframework/zend-diactoros": "For http request",
"guzzlehttp/psr7": "For http request"
},
"scripts": {
"phpunit": "test/bin/test.bash",
"test": [
"@phpunit",
"@cscheck"
],
"csfix": "test/bin/phpcbf.bash",
"cscheck": "test/bin/phpcs.bash emacs"
},
"extra": {
"branch-alias": {
"dev-master": "0.3.0-dev"
}
}
}
================================================
FILE: phpcs.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="gsouf">
<file>./src/</file>
<file>./test/suites</file>
<!--The complete PSR-2 ruleset-->
<rule ref="PSR2"/>
<!-- Arrays -->
<rule ref="Generic.Arrays.DisallowLongArraySyntax"/>
<rule ref="Squiz.Arrays.ArrayBracketSpacing"/>
<!-- Long lines do no apply on test files -->
<rule ref="Generic.Files.LineLength.TooLong">
<exclude-pattern>./test/*</exclude-pattern>
<severity>0</severity>
</rule>
<!-- When test method have weird signatures -->
<rule ref="PEAR.Functions.ValidDefaultValue.NotAtEnd">
<exclude-pattern>./test/*</exclude-pattern>
</rule>
<!-- By default we use single quote only. Double quotes come when a variable is inside -->
<rule ref="Squiz.Strings.DoubleQuoteUsage">
<exclude name="Squiz.Strings.DoubleQuoteUsage.ContainsVar"/>
</rule>
<!-- Disallow some function like var_dump -->
<rule ref="Generic.PHP.ForbiddenFunctions">
<properties>
<property name="forbiddenFunctions" type="array" value="var_dump=>null,sizeof=>count,delete=>unset,echo=>null"/>
</properties>
</rule>
</ruleset>
================================================
FILE: phpunit.dist.xml
================================================
<phpunit bootstrap="vendor/autoload.php"
backupGlobals="false"
forceCoversAnnotation="true"
backupStaticAttributes="false"
colors="true"
verbose="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite>
<directory>test/suites</directory>
</testsuite>
</testsuites>
<filter>
<blacklist>
<directory>./vendor</directory>
<directory>./doc</directory>
<directory>./script</directory>
<directory>./test</directory>
</blacklist>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src</directory>
</whitelist>
</filter>
<logging>
<log type="coverage-html" target="build/logs/html" lowUpperBound="35" highLowerBound="70"/>
<log type="coverage-clover" target="build/logs/clover.xml"/>
<log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/>
</logging>
</phpunit>
================================================
FILE: src/AdwordsResultItem.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\ProxyResult;
use Serps\Core\Serp\ResultDataInterface;
class AdwordsResultItem extends ProxyResult
{
protected $location;
/**
* AdwordsResultItem constructor.
* @param string $location
* @param ResultDataInterface $itemData
*/
public function __construct($location, ResultDataInterface $itemData)
{
$this->location = $location;
parent::__construct($itemData);
}
public function getTypes()
{
$types = parent::getTypes();
$types[] = $this->location;
return $types;
}
public function is($types)
{
$types = func_get_args();
if (in_array($this->location, $types)) {
return true;
}
return call_user_func_array(['parent', 'is'], $types);
}
}
================================================
FILE: src/AdwordsResultType.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google;
class AdwordsResultType
{
const SECTION_TOP = 'adws_section_top';
const SECTION_BOTTOM = 'adws_section_bottom';
const SECTION_RIGHT = 'adws_section_right';
const AD = 'adws_ad';
const SHOPPING_GROUP = 'adws_shopping_group';
const SHOPPING_GROUP_PRODUCT = 'adws_shopping_group_product';
}
================================================
FILE: src/AdwordsSectionResultSet.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google;
use Serps\Core\Serp\ItemPosition;
use Serps\Core\Serp\ResultDataInterface;
use Serps\Core\Serp\IndexedResultSet;
/**
* @method AdwordsResultItem[] getItems()
*/
class AdwordsSectionResultSet extends IndexedResultSet
{
protected $location;
/**
* @param string $location the locations of the results (top, bottom, right)
*/
public function __construct($location)
{
$this->location = $location;
parent::__construct(1);
}
/**
* @param ResultDataInterface $item
*/
public function addItem(ResultDataInterface $item)
{
$this->items[] = new AdwordsResultItem(
$this->location,
$item
);
}
}
================================================
FILE: src/Exception/GoogleCaptchaException.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Exception;
use Serps\Exception\RequestError\CaptchaException;
use Serps\SearchEngine\Google\Page\GoogleCaptcha;
/**
* @method GoogleCaptcha getCaptcha
*/
class GoogleCaptchaException extends CaptchaException
{
public function __construct(GoogleCaptcha $captchaResponse)
{
parent::__construct($captchaResponse);
}
}
================================================
FILE: src/Exception/InvalidDOMException.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Exception;
use Serps\Exception;
class InvalidDOMException extends Exception
{
public function __construct($message)
{
parent::__construct($message . ' Google DOM has possibly changed and an update may be required.');
}
}
================================================
FILE: src/GoogleClient.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google;
use Serps\Core\Browser\BrowserInterface;
use Serps\Core\Cookie\ArrayCookieJar;
use Serps\Core\Cookie\CookieJarInterface;
use Serps\Core\Http\HttpClientInterface;
use Serps\Core\Http\Proxy;
use Serps\Core\UrlArchive;
use Serps\Exception;
use Serps\SearchEngine\Google\Exception\GoogleCaptchaException;
use Serps\SearchEngine\Google\Page\GoogleCaptcha;
use Serps\SearchEngine\Google\Page\GoogleError;
use Serps\SearchEngine\Google\Page\GoogleSerp;
use Serps\SearchEngine\Google\GoogleUrl;
use Serps\Exception\RequestError\PageNotFoundException;
use Serps\Exception\RequestError\RequestErrorException;
use Serps\Exception\RequestError\InvalidResponseException;
/**
* Google client the handles google url routing, dom object constructions and request errors
*
*/
class GoogleClient
{
protected $defaultBrowser;
public function __construct(BrowserInterface $browser = null)
{
$this->defaultBrowser = $browser;
}
/**
* @param GoogleUrlInterface $googleUrl
* @param BrowserInterface|null $browser
* @return GoogleSerp
* @throws Exception
* @throws PageNotFoundException
* @throws InvalidResponseException
* @throws PageNotFoundException
* @throws GoogleCaptchaException
*/
public function query(GoogleUrlInterface $googleUrl, BrowserInterface $browser = null)
{
if ($googleUrl->getResultType() !== GoogleUrl::RESULT_TYPE_ALL) {
throw new Exception(
'The requested url is not valid for the google client.'
. 'Google client only supports general searches. See GoogleUrl::setResultType() for more infos.'
);
}
if (null === $browser) {
$browser = $this->defaultBrowser;
}
if (!$browser) {
throw new Exception('No browser given for query and no default browser was found');
}
$response = $browser->navigateToUrl($googleUrl);
$statusCode = $response->getHttpResponseStatus();
$effectiveUrl = GoogleUrlArchive::fromString($response->getEffectiveUrl()->__toString());
if (200 == $statusCode) {
return new GoogleSerp($response->getPageContent(), $effectiveUrl);
} else {
if (404 == $statusCode) {
throw new PageNotFoundException($response);
} else {
$errorDom = new GoogleError($response->getPageContent(), $effectiveUrl);
if ($errorDom->isCaptcha()) {
throw new GoogleCaptchaException(new GoogleCaptcha($errorDom));
} else {
throw new InvalidResponseException(
$response,
sprintf(
"The http response from %s has an invalid status code: '%d'",
$response->getInitialUrl(),
$statusCode
)
);
}
}
}
}
}
================================================
FILE: src/GoogleUrl.php
================================================
<?php
namespace Serps\SearchEngine\Google;
use Serps\SearchEngine\Google\GoogleUrlTrait;
use Serps\Core\Url;
/**
* A fluent builder for a google url
*/
class GoogleUrl extends Url implements GoogleUrlInterface
{
use GoogleUrlTrait;
const RESULT_TYPE_ALL = 'all';
const RESULT_TYPE_NEWS = 'nws';
const RESULT_TYPE_VIDEOS = 'vid';
const RESULT_TYPE_IMAGES = 'isch';
const RESULT_TYPE_SHOPPING = 'shop';
const RESULT_TYPE_BOOKS = 'bks';
const RESULT_TYPE_APPS = 'app';
const RESULT_TYPE_MAP = 'app';
public function __construct(
$host = 'www.google.com',
$path = '/search',
$scheme = 'https',
array $query = [],
$hash = '',
$port = null,
$user = null,
$pass = null
) {
parent::__construct($scheme, $host, $path, $query, $hash, $port, $user, $pass);
}
public static function build(
$scheme = null,
$host = null,
$path = null,
array $query = [],
$hash = null,
$port = null,
$user = null,
$pass = null
) {
return new static(
$host,
$path,
$scheme,
$query,
$hash,
$port,
$user,
$pass
);
}
/**
* Set the 'lr' param for the search and auto prepend 'lang_' if not present
* @param string $lang the lang to restrict with the format "lang_en" or "en"
* @return $this
*/
public function setLanguageRestriction($lang)
{
// lr format is lang_[ISO]
// if $lang starts with "lang_" do nothing
// else we prepend lang_
if (substr($lang, 0, 5) !== 'lang_') {
$lang = 'lang_' . $lang;
}
$this->setParam('lr', $lang);
return $this;
}
/**
* Set the page number of the page. Starting from 1
* @param int $pageNumber
* @return $this
*/
public function setPage($pageNumber)
{
$pageNumber--;
if ($pageNumber <= 0) {
$this->removeParam('start');
} else {
$this->setParam('start', $pageNumber * $this->getResultsPerPage());
}
return $this;
}
/**
* Changes the number of results per page. Between 1 and 100
* @param int $number number of results per page
*/
public function setResultsPerPage($number)
{
if ($number < 1) {
$number = 1;
} elseif ($number > 100) {
// Google limits it too 100
$number = 100;
}
// page backup (see below)
$currentPage = $this->getPage();
if ($number == 10) {
$this->removeParam('num');
} else {
$this->setParam('num', $number);
}
// need to refresh the page because it's based on the index of the first item
$this->setPage($currentPage);
}
/**
* Set the keywords to search
* @param $search
* @return $this
*/
public function setSearchTerm($search)
{
$this->setParam('q', $search);
return $this;
}
/**
* This allows to enable or disable google auto-correction
* @param bool $enabled by default auto correction is enable, set it to false to disable it
*/
public function setAutoCorrectionEnabled($enabled)
{
if ($enabled) {
$this->removeParam('nfpr');
} else {
$this->setParam('nfpr', 1);
}
}
/**
* Sets the google result type. That's the result type in the top bar 'all', 'images', 'videos'...
* You can use the special constant beginning with ``RESULT_TYPE_`` e.g: ``GoogleUrl::RESULT_TYPE_IMAGES``
*
* @param $resultType
*/
public function setResultType($resultType)
{
if ($resultType == self::RESULT_TYPE_ALL) {
$this->removeParam('tbm');
} else {
$this->setParam('tbm', $resultType);
}
}
}
================================================
FILE: src/GoogleUrlArchive.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google;
use Serps\Core\UrlArchive;
use Serps\SearchEngine\Google\GoogleUrl;
use Serps\SearchEngine\Google\GoogleUrlTrait;
/**
* A frozen version of a google url
*/
class GoogleUrlArchive extends UrlArchive implements GoogleUrlInterface
{
use GoogleUrlTrait;
public function __construct(
$host = 'google.com',
$path = '/search',
$scheme = 'https',
array $query = [],
$hash = '',
$port = null,
$user = null,
$pass = null
) {
parent::__construct($scheme, $host, $path, $query, $hash, $port, $user, $pass);
}
public static function build(
$scheme = null,
$host = null,
$path = null,
array $query = [],
$hash = null,
$port = null,
$user = null,
$pass = null
) {
return new static(
$host,
$path,
$scheme,
$query,
$hash,
$port,
$user,
$pass
);
}
}
================================================
FILE: src/GoogleUrlInterface.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google;
use Serps\Core\Url\UrlArchiveInterface;
/**
* The only purpose of this interface is to offer a type hinting fot GoogleUrlTrait
* Because traits are not support as type hinting
*/
interface GoogleUrlInterface extends UrlArchiveInterface
{
/**
* Get the number of the page, the pages are 1 indexed
* @return int
*/
public function getPage();
/**
* @return string
*/
public function getLanguageRestriction();
/**
* Get the number of results per pages
* @return int the number of results per pages
*/
public function getResultsPerPage();
/**
* Get the google result type. That's the result type in the top bar 'all', 'images', 'videos'...
* You can use the special constant beginning with ``RESULT_TYPE_`` e.g: ``GoogleUrl::RESULT_TYPE_IMAGES``
* @return string
*/
public function getResultType();
/**
* @return GoogleUrlArchive
*/
public function getArchive();
/**
* Check whether or not the auto correction of search term is enabled
* @return bool
*/
public function getAutoCorrectionEnabled();
}
================================================
FILE: src/GoogleUrlTrait.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google;
use Serps\Core\Url\QueryParam;
use Serps\SearchEngine\Google\GoogleUrl;
/**
* Contains the base methods describing a google url.
* @see Serps\SearchEngine\Google\GoogleUrl
* @see Serps\SearchEngine\Google\GoogleUrlArchive
*/
trait GoogleUrlTrait
{
abstract public function getParamValue($param, $defaultValue = null);
abstract public function buildUrl();
abstract public function getParamRawValue($param, $defaultValue = null);
abstract public function getHost();
abstract public function getPath();
abstract public function getScheme();
abstract public function getParams();
abstract public function getHash();
/**
* Get the number of the page, the pages are 1 indexed
* @return int
*/
public function getPage()
{
$resultsPerPage = $this->getResultsPerPage();
return 1 + $this->getParamValue('start', 0) / ($resultsPerPage > 0 ? $resultsPerPage : 10);
}
/**
* @return string
*/
public function getLanguageRestriction()
{
return $this->getParamValue('lr', null);
}
/**
* Get the number of results per pages
* @return int the number of results per pages
*/
public function getResultsPerPage()
{
return $this->getParamValue('num', 10);
}
/**
* Get the google result type. That's the result type in the top bar 'all', 'images', 'videos'...
* You can use the special constant beginning with ``RESULT_TYPE_`` e.g: ``GoogleUrl::RESULT_TYPE_IMAGES``
* @return string
*/
public function getResultType()
{
return $this->getParamValue('tbm', GoogleUrl::RESULT_TYPE_ALL);
}
/**
* Check whether or not the auto correction of search term is enabled
* @return bool true if it enabled (it is by default)
*/
public function getAutoCorrectionEnabled()
{
return 1 == $this->getParamValue('nfpr');
}
/**
* Get the keywords to search
* @return string
*/
public function getSearchTerm()
{
return $this->getParamRawValue('q');
}
public function getArchive()
{
return new GoogleUrlArchive(
$this->getHost(),
$this->getPath(),
$this->getScheme(),
$this->getParams(),
$this->getHash()
);
}
}
================================================
FILE: src/NaturalResultType.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google;
abstract class NaturalResultType
{
const CLASSICAL = 'classical';
const CLASSICAL_LARGE = 'classical_large';
const CLASSICAL_VIDEO = 'classical_video';
const CLASSICAL_SITELINK = 'classical_sitelink';
const CLASSICAL_ILLUSTRATED = 'classical_illustrated';
const KNOWLEDGE = 'knowledge';
const PEOPLE_ALSO_ASK = 'people_also_ask';
const PAA_QUESTION = 'paa_question';
const IMAGE_GROUP = 'image_group';
const IMAGE_GROUP_IMAGE = 'image_group_image';
const VIDEO_GROUP = 'video_group';
const VIDEO_GROUP_VIDEO = 'video_group_video';
const IN_THE_NEWS = 'in_the_news';
const TOP_STORIES = 'top_stories';
const TOP_STORIES_NEWS_VERTICAL = 'top_stories_news_vertical';
const TOP_STORIES_NEWS_CAROUSEL = 'top_stories_news_carousel';
const TOP_STORIES_COMPOSED = 'top_stories_composed';
const TWEETS_CAROUSEL= 'tweets_carousel';
const MAP = 'map';
const MAP_PLACE = 'map_place';
const FLIGHTS = 'flights';
const ANSWER_BOX = 'answer_box';
}
================================================
FILE: src/Page/GoogleCaptcha.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Page;
use Serps\Core\Captcha\CaptchaResponse;
use Serps\Exception;
use Serps\SearchEngine\Google\Exception\InvalidDOMException;
use Serps\SearchEngine\Google\Page\GoogleError;
class GoogleCaptcha implements CaptchaResponse
{
/**
* @var GoogleError
*/
protected $googleError;
/**
* GoogleCaptcha constructor.
* @param GoogleError $googleError
*/
public function __construct(GoogleError $googleError)
{
$this->googleError = $googleError;
}
/**
* @return GoogleError
*/
public function getErrorPage()
{
return $this->googleError;
}
public function getCaptchaType()
{
return self::CAPTCHA_TYPE_RECAPTCHAV2;
}
/**
* Gets the captcha image. Be aware that each call to this method will regenerate the captcha image
* and the previous generated image will be invalid
* @return string
* @throws Exception
*/
public function getData()
{
return null;
}
public function getDetectedIp()
{
$ipV4 = '(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})';
$ipV6 = '(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))';
$regexp = "/($ipV4|$ipV6)/";
$hasMatch = preg_match($regexp, $this->googleError->getDom()->C14N(), $match);
if ($hasMatch) {
return $match[1];
} else {
return null;
}
}
}
================================================
FILE: src/Page/GoogleDom.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Page;
use Psr\Http\Message\ResponseInterface;
use Serps\Core\Dom\WebPage;
use Serps\Core\Http\Proxy;
use Serps\Core\Http\ProxyInterface;
use Serps\Core\UrlArchive;
use Serps\SearchEngine\Google\GoogleUrl;
use Serps\SearchEngine\Google\GoogleUrlArchive;
use Serps\SearchEngine\Google\GoogleUrlInterface;
class GoogleDom extends WebPage
{
protected $xpath;
/**
* @var \DOMDocument
*/
protected $dom;
/**
* @var GoogleUrlInterface
*/
protected $url;
/**
* store parsed json from parseJsonNode
* @var array
*/
private $parsedJsonStore = [];
public function __construct($domString, GoogleUrlInterface $url)
{
$currentEncoding = $url->getParamValue('oe');
if (!$currentEncoding) {
$currentEncoding = 'UTF-8';
}
parent::__construct($domString, $url, $currentEncoding);
}
/**
* Get a property from a google json node.
* Google json nodes are invisible dom nodes that contain json text (found in mobile carousels for instance)
*
* @param string $propertyName name of the property to get
* @param \DOMNode $node
* @return mixed
*/
public function getJsonNodeProperty($propertyName, \DOMNode $node)
{
$hash = spl_object_hash($node);
if (!isset($this->parsedJsonStore[$hash])) {
$nodeValue = $node->nodeValue;
$nodeValue = trim($nodeValue, '"');
$this->parsedJsonStore[$hash] = json_decode($nodeValue, true);
}
$item = $this->parsedJsonStore[$hash];
if ($item && is_array($item) && isset($item[$propertyName])) {
return $item[$propertyName];
} else {
return null;
}
}
}
================================================
FILE: src/Page/GoogleError.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Page;
use Serps\Core\Dom\WebPage;
use Serps\SearchEngine\Google\Page\GoogleDom;
class GoogleError extends WebPage
{
/**
* @return bool Check if the page is a captcha
*/
public function isCaptcha()
{
return $this->cssQuery('#recaptcha')->count() > 0;
}
}
================================================
FILE: src/Page/GoogleSerp.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Page;
use Serps\Exception;
use Serps\SearchEngine\Google\Exception\InvalidDOMException;
use Serps\SearchEngine\Google\Parser\Evaluated\AdwordsParser;
use Serps\SearchEngine\Google\Parser\Evaluated\MobileNaturalParser;
use Serps\SearchEngine\Google\Parser\Evaluated\NaturalParser;
use Serps\SearchEngine\Google\Parser\Evaluated\MobileAdwordsParser;
use Serps\Stubs\RelatedSearch;
class GoogleSerp extends GoogleDom
{
/**
* Get the location detected by google
* @return string
*/
public function getLocation()
{
$locationRegexp = '#"uul_text":"([^"]+)"#';
preg_match($locationRegexp, $this->dom->C14N(), $matches);
if ($matches && isset($matches[1])) {
return $matches[1];
}
return null;
}
/**
* @return \Serps\Core\Serp\IndexedResultSet
*/
public function getNaturalResults()
{
if ($this->javascriptIsEvaluated()) {
if ($this->isMobile()) {
$parser = new MobileNaturalParser();
} else {
$parser = new NaturalParser();
}
} else {
throw new InvalidDOMException('Raw dom is not supported, please provide an evaluated version of the dom');
}
return $parser->parse($this);
}
/**
* @return \Serps\Core\Serp\CompositeResultSet
* @throws Exception
* @throws InvalidDOMException
*/
public function getAdwordsResults()
{
if ($this->javascriptIsEvaluated()) {
if ($this->isMobile()) {
$parser = new MobileAdwordsParser();
} else {
$parser = new AdwordsParser();
}
return $parser->parse($this);
} else {
throw new InvalidDOMException('Raw dom is not supported, please provide an evaluated version of the dom');
}
}
/**
* Get the total number of results available for the search terms
* @return int the number of results
* @throws InvalidDOMException
*/
public function getNumberOfResults()
{
$item = $this->cssQuery('#resultStats');
if ($item->length != 1) {
return null;
}
// number of results is followed by time, we want to targets the first node (text node) that is the number of
// results
$nodeValue = $item->getNodeAt(0)->getChildren()->getNodeAt(0)->getNodeValue();
if (!$nodeValue) {
return null;
}
// WARNING: The number of result is explained in different format according to the country. Fon instance:
// UK: 6,200,000
// FR: 6 200 000
// DE: 2.200.000
// IN: 62,00,000
// We have to use a global matcher
$matched = preg_match_all('/([0-9]+[^0-9]?)+/u', $nodeValue, $countMatch);
if (!$matched) {
return null;
}
// get the last count, when we use pagination the first count is the page number
// see https://github.com/serp-spider/search-engine-google/issues/100
$count = $countMatch[0][count($countMatch[0]) - 1];
return (int) preg_replace('/[^0-9]/', '', $count);
}
public function javascriptIsEvaluated()
{
$body = $this->getXpath()->query('//body');
if ($body->length != 1) {
throw new Exception('No body found');
}
$body = $body->item(0);
/** @var $body \DOMElement */
$class = $body->getAttribute('class');
if ($class=='hsrp') {
return false;
} elseif (strstr($class, 'srp')) {
return true;
} else {
throw new InvalidDOMException('Unable to check javascript status.');
}
}
/**
* @return array|RelatedSearch[]
*/
public function getRelatedSearches()
{
$relatedSearches = [];
if ($this->isMobile()) {
$items = $this->cssQuery('#botstuff div:not(#bres) a.QsZ7bb');
if ($items->length == 0) {
// TODO BC version to remove
$items = $this->cssQuery('#botstuff div:not(#bres)>._Qot>div>a');
}
if ($items->length > 0) {
foreach ($items as $item) {
/* @var $item \DOMElement */
$result = new \stdClass();
$result->title = $item->childNodes->item(0)->nodeValue;
$result->url = $this->getUrl()->resolveAsString($item->getAttribute('href'));
$relatedSearches[] = $result;
}
}
} else {
$items = $this->cssQuery('#brs ._e4b>a, #brs .card-section a'); // TODO ._ed4 is outdated
if ($items->length > 0) {
foreach ($items as $item) {
/* @var $item \DOMElement */
$result = new \stdClass();
$result->title = $item->nodeValue;
$result->url = $this->getUrl()->resolveAsString($item->getAttribute('href'));
$relatedSearches[] = $result;
}
}
}
return $relatedSearches;
}
public function isMobile()
{
$item = $this->cssQuery('head meta[name="viewport"]');
return $item->length == 1;
}
}
================================================
FILE: src/Page/NotFound.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Page;
class NotFound extends GoogleDom
{
}
================================================
FILE: src/Parser/AbstractAdwordsParser.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser;
use Serps\Core\Serp\CompositeResultSet;
use Serps\SearchEngine\Google\Page\GoogleDom;
abstract class AbstractAdwordsParser implements ParserInterface
{
/**
* @var ParserInterface[]
*/
private $parsers = null;
/**
* Generate a list of parsers to be used when parsing dom
* @return ParserInterface[]
*/
abstract public function generateParsers();
/**
* @return ParserInterface[]
*/
public function getParsers()
{
if (null == $this->parsers) {
$this->parsers = $this->generateParsers();
}
return $this->parsers;
}
/**
* @inheritdoc
*/
public function parse(GoogleDom $googleDom)
{
$resultsSets = new CompositeResultSet();
$parsers = $this->getParsers();
foreach ($parsers as $parser) {
$resultsSets->addResultSet(
$parser->parse($googleDom)
);
}
return $resultsSets;
}
}
================================================
FILE: src/Parser/AbstractParser.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser;
use Serps\Core\Dom\DomNodeList;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\Page\GoogleDom;
abstract class AbstractParser implements ParserInterface
{
/**
* @var ParsingRuleInterface[]
*/
private $rules = null;
/**
* @return ParsingRuleInterface[]
*/
abstract protected function generateRules();
/**
* @param GoogleDom $googleDom
* @return DomNodeList
*/
abstract protected function getParsableItems(GoogleDom $googleDom);
/**
* @return ParsingRuleInterface[]
*/
public function getRules()
{
if (null == $this->rules) {
$this->rules = $this->generateRules();
}
return $this->rules;
}
/**
* Parses the given google dom
* @param GoogleDom $googleDom
* @return IndexedResultSet
*/
public function parse(GoogleDom $googleDom)
{
$elementGroups = $this->getParsableItems($googleDom);
$resultSet = $this->createResultSet($googleDom);
return $this->parseGroups($elementGroups, $resultSet, $googleDom);
}
/**
* Defines what resultset to use for results
* @param GoogleDom $googleDom
* @return IndexedResultSet
*/
protected function createResultSet(GoogleDom $googleDom)
{
$startingAt = (int) $googleDom->getUrl()->getParamValue('start', 0);
return new IndexedResultSet($startingAt + 1);
}
/**
* @param $elementGroups
* @param IndexedResultSet $resultSet
* @param $googleDom
* @return IndexedResultSet
*/
protected function parseGroups(DomNodeList $elementGroups, IndexedResultSet $resultSet, $googleDom)
{
$rules = $this->getRules();
foreach ($elementGroups as $group) {
if (!($group instanceof \DOMElement)) {
continue;
}
foreach ($rules as $rule) {
$match = $rule->match($googleDom, $group);
if ($match instanceof \DOMNodeList) {
$this->parseGroups(new DomNodeList($match, $googleDom), $resultSet, $googleDom);
break;
} elseif ($match instanceof DomNodeList) {
$this->parseGroups($match, $resultSet, $googleDom);
break;
} else {
switch ($match) {
case ParsingRuleInterface::RULE_MATCH_MATCHED:
$rule->parse($googleDom, $group, $resultSet);
break 2;
case ParsingRuleInterface::RULE_MATCH_STOP:
break 2;
}
}
}
}
return $resultSet;
}
}
================================================
FILE: src/Parser/Evaluated/AdwordsParser.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated;
use Serps\SearchEngine\Google\AdwordsResultType;
use Serps\Core\Dom\Css;
use Serps\SearchEngine\Google\Parser\AbstractAdwordsParser;
class AdwordsParser extends AbstractAdwordsParser
{
/**
* @inheritdoc
*/
public function generateParsers()
{
return [
// Adwords top
new AdwordsSectionParser(
Css::toXPath('div#tads li.ads-ad, div#tvcap ._oc'),
AdwordsResultType::SECTION_TOP
),
// Adwords bottom
new AdwordsSectionParser(
"descendant::div[@id = 'bottomads']//li[@class='ads-ad']",
AdwordsResultType::SECTION_BOTTOM
)
];
}
}
================================================
FILE: src/Parser/Evaluated/AdwordsSectionParser.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated;
use Serps\SearchEngine\Google\AdwordsSectionResultSet;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\AbstractParser;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Adwords\AdwordsItem;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Adwords\Shopping;
/**
* Parses adwords results from a google SERP
*/
class AdwordsSectionParser extends AbstractParser
{
protected $pathToItems;
protected $location;
/**
* @param $pathToItems
*/
public function __construct($pathToItems, $location)
{
$this->pathToItems = $pathToItems;
$this->location = $location;
}
/**
* @inheritdoc
*/
protected function createResultSet(GoogleDom $googleDom)
{
return new AdwordsSectionResultSet($this->location);
}
/**
* @inheritdoc
*/
protected function generateRules()
{
return [
new AdwordsItem(),
new Shopping()
];
}
/**
* @inheritdoc
*/
protected function getParsableItems(GoogleDom $googleDom)
{
return $googleDom->xpathQuery($this->pathToItems);
}
}
================================================
FILE: src/Parser/Evaluated/MobileAdwordsParser.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated;
use Serps\SearchEngine\Google\AdwordsResultType;
use Serps\Core\Dom\Css;
use Serps\SearchEngine\Google\Parser\AbstractAdwordsParser;
class MobileAdwordsParser extends AbstractAdwordsParser
{
/**
* @inheritdoc
*/
public function generateParsers()
{
return [
// Adwords top
new MobileAdwordsSectionParser(
Css::toXPath('#tads li.ads-fr, #tvcap'),
AdwordsResultType::SECTION_TOP
),
// Adwords bottom
new MobileAdwordsSectionParser(
Css::toXPath('#tadsb li.ads-fr'),
AdwordsResultType::SECTION_BOTTOM
)
];
}
}
================================================
FILE: src/Parser/Evaluated/MobileAdwordsSectionParser.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Adwords\AdwordsItemMobile;
/**
* Parses adwords results from a google SERP
*/
class MobileAdwordsSectionParser extends AdwordsSectionParser
{
/**
* @inheritdoc
*/
protected function generateRules()
{
return [
new AdwordsItemMobile()
];
}
}
================================================
FILE: src/Parser/Evaluated/MobileNaturalParser.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\AbstractParser;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Classical\ClassicalCardsResultO9g5cc;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Classical\ClassicalCardsResultZ1m;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Classical\ClassicalCardsResultZINbbc;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Classical\ClassicalCardsVideoResult;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\ComposedTopStories;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Divider;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\ImageGroup;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\ImageGroupCarousel;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Classical\LargeClassicalResult;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\KnowledgeCard;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Classical\ClassicalCardsResult;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\MapMobile;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\PeopleAlsoAsk;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\SearchResultGroup;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\TweetsCarouselZ1m;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\VideoGroup;
/**
* Parses natural results from a mobile google SERP
*/
class MobileNaturalParser extends AbstractParser
{
/**
* @inheritdoc
*/
protected function generateRules()
{
return [
new Divider(),
new SearchResultGroup(),
new ClassicalCardsResultO9g5cc(),
new ClassicalCardsResultZINbbc(), // TODO maybe outdated
new ClassicalCardsResultZ1m(), // TODO remove (outdated)
new ClassicalCardsVideoResult(),
new ClassicalCardsResult(),
new MapMobile(),
new TweetsCarouselZ1m(), // TODO replace and remove (outdated)
new ImageGroupCarousel(),
new ComposedTopStories(),
new VideoGroup(),
new ImageGroup(),
new PeopleAlsoAsk(), // people also ask must be placed before knowledge card to stop parsing
new KnowledgeCard()
];
}
/**
* @inheritdoc
*/
protected function getParsableItems(GoogleDom $googleDom)
{
$xpathObject = $googleDom->getXpath();
$xpathElementGroups = "//div[@id = 'ires']/*[@id = 'rso']/*";
return $xpathObject->query($xpathElementGroups);
}
}
================================================
FILE: src/Parser/Evaluated/NaturalParser.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\AbstractParser;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\AnswerBox;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Classical\ClassicalResult;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Divider;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Flight;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\ImageGroup;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\InTheNews;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Map;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Classical\ClassicalCardsResult;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\MapLegacy;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\PeopleAlsoAsk;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\SearchResultGroup;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\TopStoriesCarousel;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\TopStoriesVertical;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\TweetsCarousel;
use Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Classical\ClassicalWithLargeVideo;
/**
* Parses natural results from a google SERP
*/
class NaturalParser extends AbstractParser
{
/**
* @inheritdoc
*/
protected function generateRules()
{
return [
new Divider(),
new SearchResultGroup(),
new ClassicalResult(),
new ClassicalCardsResult(),
new ImageGroup(),
new TopStoriesCarousel(),
new TopStoriesVertical(),
new TweetsCarousel(),
new ClassicalWithLargeVideo(),
new InTheNews(),
new Map(),
new MapLegacy(),
new AnswerBox(),
new Flight(),
new PeopleAlsoAsk()
];
}
/**
* @inheritdoc
*/
protected function getParsableItems(GoogleDom $googleDom)
{
return $googleDom->xpathQuery("//*[@id = 'rso']/*");
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Adwords/AdwordsItem.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Adwords;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\AdwordsResultType;
use Serps\Core\Dom\Css;
use Serps\SearchEngine\Google\Exception\InvalidDOMException;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
class AdwordsItem implements ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
if ($node->getAttribute('class') == 'ads-ad') {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
}
public function parse(GoogleDom $googleDOM, \DomElement $node, IndexedResultSet $resultSet)
{
$item = [
'title' => function () use ($googleDOM, $node) {
$aTag = $googleDOM->getXpath()->query('descendant::h3/a[2]', $node)->item(0);
if (!$aTag) {
$aTag = $googleDOM->getXpath()->query('descendant::h3', $node)->item(0);
if (!$aTag) {
return null;
}
}
return $aTag->nodeValue;
},
'url' => function () use ($node, $googleDOM) {
$aTag = $googleDOM->getXpath()->query('descendant::h3/a[2]', $node)->item(0); // TODO remove
if (!$aTag) {
$aTag = $googleDOM->cssQuery('a', $node)->item(0);
if (!$aTag) {
throw new InvalidDOMException('Cannot find ads anchor');
}
}
return $googleDOM->getUrl()->resolveAsString($aTag->getAttribute('href'));
},
'visurl' => function () use ($node, $googleDOM) {
$aTag = $googleDOM->getXpath()->query(
Css::toXPath('div.ads-visurl>cite'),
$node
)->item(0);
if (!$aTag) {
return null;
}
return $aTag->nodeValue;
},
'description' => function () use ($node, $googleDOM) {
$aTag = $googleDOM->getXpath()->query(
Css::toXPath('div.ads-creative'),
$node
)->item(0);
if (!$aTag) {
return null;
}
return $aTag->nodeValue;
},
];
$resultSet->addItem(new BaseResult(AdwordsResultType::AD, $item));
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Adwords/AdwordsItemMobile.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Adwords;
use Serps\Core\Dom\DomElement;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\AdwordsResultType;
use Serps\SearchEngine\Google\Exception\InvalidDOMException;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
class AdwordsItemMobile implements ParsingRuleInterface
{
/**
* @inheritdoc
*/
public function match(GoogleDom $dom, DomElement $node)
{
if ($node->hasClass('ads-fr')) {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
}
/**
* @inheritdoc
*/
public function parse(GoogleDom $googleDOM, \DomElement $node, IndexedResultSet $resultSet)
{
$item = [
'title' => function () use ($googleDOM, $node) {
$aTag = $googleDOM->cssQuery('a .MUxGbd.v0nnCb', $node)->item(0);
if (!$aTag) {
throw new InvalidDOMException('Cannot find title for mobile adwords.');
}
return $aTag->nodeValue;
},
'url' => function () use ($node, $googleDOM) {
$aTag = $googleDOM->cssQuery('a', $node)->item(0);
if (!$aTag) {
throw new InvalidDOMException('Cannot find ads anchor');
}
return $googleDOM->getUrl()->resolveAsString($aTag->getAttribute('href'));
},
'visurl' => function () use ($node, $googleDOM) {
return $googleDOM->cssQuery('.qzEoUe', $node)->getNodeAt(0)->getNodeValue();
},
'description' => function () use ($node, $googleDOM) {
return $googleDOM->cssQuery('div.BmP5tf>div.MUxGbd', $node)
->getNodeAt(0)
->getNodeValue();
},
];
$resultSet->addItem(new BaseResult(AdwordsResultType::AD, $item));
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Adwords/Shopping.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Adwords;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\AdwordsResultType;
use Serps\Core\Dom\Css;
use Serps\SearchEngine\Google\NaturalResultType;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
class Shopping implements ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
$class = $node->getAttribute('class');
if (strpos(' ' . $class . ' ', ' _oc ')) {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
}
public function parse(GoogleDom $googleDOM, \DomElement $node, IndexedResultSet $resultSet)
{
$item = [
'products' => function () use ($googleDOM, $node) {
$items = [];
$xpathCards = Css::toXPath('.pla-unit');
$productNodes = $googleDOM->getXpath()->query($xpathCards, $node);
foreach ($productNodes as $productNode) {
$items[] = $this->parseItem($googleDOM, $productNode);
}
return $items;
}
];
$resultSet->addItem(new BaseResult(AdwordsResultType::SHOPPING_GROUP, $item));
}
public function parseItem(GoogleDom $googleDOM, \DOMNode $node)
{
return new BaseResult(AdwordsResultType::SHOPPING_GROUP_PRODUCT, [
'title' => function () use ($googleDOM, $node) {
$aTag = $googleDOM->getXpath()->query(Css::toXPath('.pla-unit-title-link'), $node)->item(0);
if (!$aTag) {
return null;
}
return $aTag->nodeValue;
},
'url' => function () use ($node, $googleDOM) {
$aTag = $googleDOM->getXpath()->query(Css::toXPath('.pla-unit-title-link'), $node)->item(0);
if (!$aTag) {
return $googleDOM->getUrl()->resolve('/');
}
return $googleDOM->getUrl()->resolveAsString($aTag->getAttribute('href'));
},
'image' => function () use ($node, $googleDOM) {
$imgTag = $googleDOM->getXpath()->query(
Css::toXPath('.pla-unit-img-container-link img'),
$node
)->item(0);
if (!$imgTag) {
return null;
}
return $imgTag->getAttribute('src');
},
'target' => function () use ($node, $googleDOM) {
$aTag = $googleDOM->getXpath()->query(
Css::toXPath('div._mC span.a'),
$node
)->item(0);
if (!$aTag) {
return null;
}
return $aTag->nodeValue;
},
'price' => function () use ($node, $googleDOM) {
$priceTag = $googleDOM->getXpath()->query(
Css::toXPath('._QD._pvi'),
$node
)->item(0);
if (!$priceTag) {
return null;
}
return $priceTag->nodeValue;
}
]);
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/AnswerBox.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
use Serps\SearchEngine\Google\NaturalResultType;
class AnswerBox implements ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
if ($node->getAttribute('class') == 'g mnr-c g-blk'
&& (
$dom->cssQuery('.ifM9O', $node)->length == 1 ||
$dom->cssQuery('._Z7', $node)->length == 1 // TODO used for BC, remove in the future
)
) {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
}
protected function parseNode(GoogleDom $dom, \DOMElement $node)
{
return [
'title' => function () use ($dom, $node) {
$aTag = $dom->cssQuery('.rc .r a', $node)
->item(0);
if (!$aTag) {
// TODO ERROR
return;
}
if ($h3Tag = $dom->cssQuery('h3', $aTag)->item(0)) {
return $h3Tag->getNodeValue();
}
return $aTag->nodeValue;
},
'url' => function () use ($dom, $node) {
$aTag = $dom->cssQuery('.rc .r a', $node)
->item(0);
if (!$aTag) {
// TODO ERROR
return;
}
return $dom->getUrl()->resolveAsString($aTag->getAttribute('href'));
},
'destination' => function () use ($dom, $node) {
$citeTag = $dom->cssQuery('.rc .s cite', $node)
->item(0);
if (!$citeTag) {
// TODO ERROR
return;
}
return $citeTag->nodeValue;
},
'description' => function () use ($dom, $node) {
// TODO "mod ._Tgc" kept for BC, remove in the future
$citeTag = $dom->cssQuery('.mod ._Tgc, .mod .Y0NH2b', $node)
->item(0);
if (!$citeTag) {
// TODO ERROR
return;
}
return $citeTag->nodeValue;
},
];
}
public function parse(GoogleDom $dom, \DOMElement $node, IndexedResultSet $resultSet)
{
$item = new BaseResult(
[NaturalResultType::ANSWER_BOX],
$this->parseNode($dom, $node)
);
$resultSet->addItem($item);
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/Classical/ClassicalCardsResult.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Classical;
use Serps\Core\Dom\DomElement;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\NaturalResultType;
use Serps\SearchEngine\Google\Page\GoogleDom;
class ClassicalCardsResult extends ClassicalResult
{
public function match(GoogleDom $dom, DomElement $node)
{
if ($node->hasClass('mnr-c')) {
$hasgblk = $node->hasClass('g-blk');
// class g-blk is common to classical large, answer box, and some other cards results,
// but not present on base classical results
// class ._Hi is unique to large classical results
if ((
!$hasgblk ||
($hasgblk && $dom->cssQuery('._Hi', $node)->length == 1)
) &&
$dom->cssQuery('.rc', $node)->length == 1
) {
return self::RULE_MATCH_MATCHED;
}
}
return self::RULE_MATCH_NOMATCH;
}
/**
* Is large dectection is not the same for cards and non cards classical results
* @param GoogleDom $dom
* @param \DomElement $node
* @return bool
*/
protected function isLarge(GoogleDom $dom, \DomElement $node)
{
return $dom->cssQuery('._Hi', $node)->length == 1;
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/Classical/ClassicalCardsResultO9g5cc.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Classical;
use Serps\Core\Dom\DomElement;
use Serps\Core\Dom\DomNodeInterface;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\NaturalResultType;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
/**
* First seen in august 2018 in mobile pages, replacing .ZINbbc
*/
class ClassicalCardsResultO9g5cc implements ParsingRuleInterface
{
public function match(GoogleDom $dom, DomElement $node)
{
$res = $dom->cssQuery('.O9g5cc.xpd a.C8nzq', $node);
// TODO consider removing .ZINbbc (replaced with .O9g5cc in august 2018)
if ($res->length == 1) {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
}
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet)
{
$classicalData = $this->parseNode($dom, $node);
$resultTypes = [NaturalResultType::CLASSICAL];
$item = new BaseResult($resultTypes, $classicalData);
$resultSet->addItem($item);
}
protected function parseNode(GoogleDom $dom, DomNodeInterface $node)
{
return [
'title' => function () use ($dom, $node) {
return $dom
->cssQuery('a .MUxGbd', $node)
->getNodeAt(0)
->getNodeValue();
},
'isAmp' => function () use ($dom, $node) {
return $dom
->cssQuery('.ZseVEf', $node)
->length > 0;
},
'url' => function () use ($dom, $node) {
return $dom
->cssQuery('a.C8nzq', $node)
->getNodeAt(0)
->getAttribute('href');
},
'destination' => function () use ($dom, $node) {
return $dom
->cssQuery('span.QHTnWc, span.qzEoUe', $node) // TODO ".QHTnWc" appears to be outdated
->getNodeAt(0)
->getNodeValue();
},
'description' => function () use ($dom, $node) {
// TODO remove BC with ".JTuIPc:not(a)>.MUxGbd"
return $dom
->cssQuery('.JTuIPc:not(a)>.MUxGbd, div.BmP5tf>div.MUxGbd, div.LZ8hH>div.MUxGbd', $node)
->getNodeAt(0)
->getNodeValue();
}
];
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/Classical/ClassicalCardsResultZ1m.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Classical;
use Serps\Core\Dom\DomElement;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\NaturalResultType;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
/**
* Special version of classical card that appeared end 2017 in mobile serps.
* Those results are identified by the class ._Z1m
*
* Z1m card results have nothing in common with other results so they have to be parsed independently
*/
class ClassicalCardsResultZ1m implements ParsingRuleInterface
{
public function match(GoogleDom $dom, DomElement $node)
{
if ($node->childNodes->length == 1) {
$childNode = $node->getChildren()->getNodeAt(0);
// TODO _Z1m results appear to be outdated
// check if has class _Z1m
if ($childNode->hasClass('_Z1m')) {
// check _a5r
if ($node->childNodes->item(0)->childNodes->length == 1) {
/** @var DomElement $subChildNode */
$subChildNode = $childNode->childNodes->item(0);
if ($subChildNode->hasClass('_a5r')) {
return self::RULE_MATCH_MATCHED;
}
}
// check a._Olt._bCp
if ($dom->cssQuery('a._Olt._bCp', $node)->length > 0) {
return self::RULE_MATCH_MATCHED;
}
}
}
return self::RULE_MATCH_NOMATCH;
}
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet)
{
$data = $this->parseNode($dom, $node->childNodes->item(0));
$resultTypes = [NaturalResultType::CLASSICAL];
$item = new BaseResult($resultTypes, $data);
$resultSet->addItem($item);
}
protected function parseNode(GoogleDom $dom, DomElement $node)
{
return [
'title' => function () use ($dom, $node) {
return $dom
->cssQuery('._ees', $node)
->item(0)
->nodeValue;
},
'isAmp' => function () use ($dom, $node) {
return $dom
->cssQuery('.amp_r', $node)
->length > 0;
},
'url' => function () use ($dom, $node) {
return $dom
->cssQuery('a._Olt', $node)
->item(0)
->getAttribute('href');
},
'destination' => function () use ($dom, $node) {
return $dom
->cssQuery('span._Clt', $node)
->item(0)
->nodeValue;
},
'description' => function () use ($dom, $node) {
$res = $dom
->cssQuery('div>div._bCp>div._H1m', $node);
if ($res->length > 0) {
return $res->item(0)->getNodeValue();
} else {
return null;
}
}
];
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/Classical/ClassicalCardsResultZINbbc.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Classical;
use Serps\Core\Dom\DomElement;
use Serps\Core\Dom\DomNodeInterface;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\NaturalResultType;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
/**
* First seen in 2018 in mobile pages
* TODO consider removing .ZINbbc (replaced with .O9g5cc in august 2018)
*/
class ClassicalCardsResultZINbbc implements ParsingRuleInterface
{
public function match(GoogleDom $dom, DomElement $node)
{
$res = $dom->cssQuery('.ZINbbc.xpd a.C8nzq', $node);
if ($res->length == 1) {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
}
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet)
{
$classicalData = $this->parseNode($dom, $node);
$resultTypes = [NaturalResultType::CLASSICAL];
$item = new BaseResult($resultTypes, $classicalData);
$resultSet->addItem($item);
}
protected function parseNode(GoogleDom $dom, DomNodeInterface $node)
{
return [
'title' => function () use ($dom, $node) {
return $dom
->cssQuery('a .pIpgAc', $node)
->getNodeAt(0)
->getNodeValue();
},
'isAmp' => function () use ($dom, $node) {
return $dom
->cssQuery('.ZseVEf', $node)
->length > 0;
},
'url' => function () use ($dom, $node) {
return $dom
->cssQuery('a.C8nzq', $node)
->getNodeAt(0)
->getAttribute('href');
},
'destination' => function () use ($dom, $node) {
return $dom
->cssQuery('span.QHTnWc, span.qzEoUe', $node) // TODO ".QHTnWc" appears to be outdated
->getNodeAt(0)
->getNodeValue();
},
'description' => function () use ($dom, $node) {
$res = $dom
->cssQuery('.JTuIPc', $node);
if ($res->length > 1) {
return $dom->cssQuery('.pIpgAc', $res->getNodeAt(1))->getNodeAt(0)->getNodeValue();
} else {
return null;
}
}
];
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/Classical/ClassicalCardsVideoResult.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Classical;
use Serps\Core\Dom\DomElement;
use Serps\Core\Media\MediaFactory;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\NaturalResultType;
use Serps\SearchEngine\Google\Page\GoogleDom;
class ClassicalCardsVideoResult extends ClassicalCardsResult
{
public function match(GoogleDom $dom, DomElement $node)
{
$match = parent::match($dom, $node);
if ($match === self::RULE_MATCH_MATCHED) {
if ($dom->cssQuery('.A995L', $node)->length) {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
} else {
return $match;
}
}
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet)
{
$resultTypes = [NaturalResultType::CLASSICAL, NaturalResultType::CLASSICAL_VIDEO];
$item = new BaseResult($resultTypes, [
'title' => function () use ($dom, $node) {
return $dom->cssQuery('h3 a', $node)->getNodeAt(0)->getNodeValue();
},
'url' => function () use ($dom, $node) {
return $dom->getUrl()->resolveAsString(
$dom->cssQuery('h3 a', $node)->getNodeAt(0)->getAttribute('href')
);
},
'destination' => function () use ($dom, $node) {
return $dom->cssQuery('.RXIhdf', $node)->getNodeAt(0)->getNodeValue();
},
'description' => '',
'isAmp' => false,
'videoLarge' => false,
'videoCover' => function () use ($dom, $node) {
return MediaFactory::createMediaFromSrc(
$dom->cssQuery('.A995L a img', $node)->getNodeAt(0)->getAttribute('src')
);
}
]);
$resultSet->addItem($item);
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/Classical/ClassicalResult.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Classical;
use Serps\Core\Dom\DomElement;
use Serps\Core\Media\MediaFactory;
use Serps\SearchEngine\Google\Exception\InvalidDOMException;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
use Serps\SearchEngine\Google\NaturalResultType;
class ClassicalResult implements ParsingRuleInterface
{
public function match(GoogleDom $dom, DomElement $node)
{
if ($node->getAttribute('class') == 'g') {
if ($dom->cssQuery('.rc', $node)->length == 1) {
return self::RULE_MATCH_MATCHED;
}
}
return self::RULE_MATCH_NOMATCH;
}
protected function parseNode(GoogleDom $dom, \DomElement $node)
{
// find the title/url
/* @var $aTag \DOMElement */
$aTag = $dom
->xpathQuery("descendant::*[(self::div or self::h3) and @class='r'][1]/a", $node)
->item(0);
if (!$aTag) {
throw new InvalidDOMException('Cannot parse a classical result.');
}
/* @var $h3Tag \DOMElement */
$h3Tag = $dom
->xpathQuery('descendant::h3', $node)
->item(0);
if (!$h3Tag) {
throw new InvalidDOMException('Cannot parse a classical result.');
}
$destinationTag = $dom
->cssQuery('div.f cite, div.TbwUpd cite', $node)
->getNodeAt(0);
if (is_a($destinationTag, Serps\Core\Dom\NullDomNode::class)) {
throw new InvalidDOMException('Cannot parse a classical result.');
}
$descriptionTag = $dom
->xpathQuery("descendant::span[@class='st']", $node)
->item(0);
return [
'title' => $h3Tag->nodeValue,
'url' => $dom->getUrl()->resolveAsString($aTag->getAttribute('href')),
'destination' => $destinationTag->getNodeValue(),
// trim needed for mobile results coming with an initial space
'description' => $descriptionTag ? trim($descriptionTag->nodeValue) : null,
'isAmp' => function () use ($dom, $node) {
return $dom
->cssQuery('.amp_r', $node)
->length > 0;
},
];
}
/**
* If isLarge() matched, this will parse the content of site links
* @param GoogleDom $dom
* @param \DomElement $node
* @return \Closure
*/
protected function parseSiteLink(GoogleDom $dom, \DomElement $node)
{
return function () use ($dom, $node) {
$items = $dom->cssQuery('.mslg .sld', $node);
$siteLinksData = [];
foreach ($items as $item) {
$siteLinksData[] = new BaseResult(NaturalResultType::CLASSICAL_SITELINK, [
'title' => function () use ($dom, $item) {
return $dom->cssQuery('h3.r a', $item)
->getNodeAt(0)
->getNodeValue();
},
'description' => function () use ($dom, $item) {
return $dom->cssQuery('.st', $item)
->getNodeAt(0)
->getNodeValue();
},
'url' => function () use ($dom, $item) {
return $dom->cssQuery('h3.r a', $item)
->getNodeAt(0)
->getAttribute('href');
},
]);
}
return $siteLinksData;
};
}
/**
* Check if has site links. Might be overriden by subparser like ClassicalCard
* @param GoogleDom $dom
* @param \DomElement $node
* @return bool
*/
protected function isLarge(GoogleDom $dom, \DomElement $node)
{
return $dom->cssQuery('.nrgt', $node)->length == 1;
}
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet)
{
$data = $this->parseNode($dom, $node);
$resultTypes = [NaturalResultType::CLASSICAL];
// CLASSICAL RESULT MIGHT BE ENLARGED WITH SITELINKS
if ($this->isLarge($dom, $node)) {
$data['sitelinks'] = $this->parseSiteLink($dom, $node);
$resultTypes[] = NaturalResultType::CLASSICAL_LARGE;
}
// classical result can have a video thumbnail
$thumb = $dom->getXpath()
->query("descendant::g-img[@class='_ygd']/img", $node)
->item(0);
if ($thumb) {
$resultTypes[] = NaturalResultType::CLASSICAL_ILLUSTRATED;
$data['thumb'] = function () use ($thumb) {
if ($thumb) {
return MediaFactory::createMediaFromSrc($thumb->getAttribute('src'));
} else {
return null;
}
};
}
$videoDuration = $dom->cssQuery('.vdur', $node);
if ($videoDuration->length == 1) {
$resultTypes[] = NaturalResultType::CLASSICAL_VIDEO;
$data['videoLarge'] = false;
}
$item = new BaseResult($resultTypes, $data);
$resultSet->addItem($item);
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/Classical/ClassicalWithLargeVideo.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural\Classical;
use Serps\Core\Media\MediaFactory;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
use Serps\SearchEngine\Google\NaturalResultType;
class ClassicalWithLargeVideo implements ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
if ($node->getAttribute('class') == 'g mnr-c g-blk'
&& $dom->cssQuery('.knowledge-block__video-nav-block', $node)->length == 1
) {
return self::RULE_MATCH_MATCHED;
} else {
return self::RULE_MATCH_NOMATCH;
}
}
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet)
{
$xpath = $dom->getXpath();
$aTag = $xpath->query("descendant::h3[@class='r'][1]/a", $node)->item(0);
if (!$aTag) {
return false;
}
$destinationTag = $xpath
->query("descendant::div[@class='f kv _SWb']/cite", $node)
->item(0);
$data = [
'title' => $aTag->nodeValue,
'url' => $dom->getUrl()->resolveAsString($aTag->getAttribute('href')),
'destination' => $destinationTag ? $destinationTag->nodeValue : null,
'description' => null,
'videoLarge' => true,
'thumb' => null,
'videoCover' => function () use ($dom, $node) {
$imageTag = $dom
->cssQuery('._ELb img', $node)
->item(0);
if ($imageTag) {
return MediaFactory::createMediaFromSrc($imageTag->getAttribute('src')); // TODO 1p gif ?
} else {
return null;
}
}
];
$resultSet->addItem(new BaseResult([NaturalResultType::CLASSICAL_VIDEO, NaturalResultType::CLASSICAL], $data));
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/ComposedTopStories.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\Core\Dom\DomElement;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\Core\Serp\ResultSet;
use Serps\SearchEngine\Google\NaturalResultType;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
class ComposedTopStories implements ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
if ($dom->cssQuery('._Fzo ._HSj', $node)->length == 1
&& $dom->cssQuery('.dbsr', $node)->length > 0
// Dont use _yyh, _JTg or _bfj class because it's common to all carousel
) {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
}
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet)
{
$item = new BaseResult(
[NaturalResultType::TOP_STORIES, NaturalResultType::TOP_STORIES_COMPOSED],
$this->parseNode($dom, $node)
);
$resultSet->addItem($item);
}
private function parseNode(GoogleDom $dom, $node)
{
return [
'isCarousel' => true,
'isVertical' => true,
'news' => function () use ($dom, $node) {
$news = $this->parseVerticalResults($dom, $node);
$news = array_merge($news, $this->parseCarouselResults($dom, $node));
$resultSet = new ResultSet();
$resultSet->addItems($news);
return $resultSet;
},
];
}
private function parseVerticalResults(GoogleDom $dom, DomElement $node)
{
$news = [];
$nodes = $dom->cssQuery('.dbsr', $node);
foreach ($nodes as $newsNode) {
$news[] = new BaseResult(NaturalResultType::TOP_STORIES_NEWS_VERTICAL, [
'title' => function () use ($dom, $newsNode) {
$el = $dom->cssQuery('._eNq>span', $newsNode)->item(0);
return $el->nodeValue;
},
'url' => function () use ($dom, $newsNode) {
$el = $dom->cssQuery('._rNq>a', $newsNode)->item(0);
return $el->getAttribute('href');
}
]);
}
return $news;
}
private function parseCarouselResults(GoogleDom $dom, DomElement $node)
{
$news = [];
$nodes = $dom->cssQuery('._HSj ._ERj', $node);
foreach ($nodes as $newsNode) {
$news[] = new BaseResult(NaturalResultType::TOP_STORIES_NEWS_CAROUSEL, [
'title' => function () use ($dom, $newsNode) {
$el = $dom->cssQuery('._IRj', $newsNode)->item(0);
return $el->nodeValue;
},
'url' => function () use ($dom, $newsNode) {
$el = $dom->cssQuery('g-inner-card._KBh>a', $newsNode)->item(0);
return $el->getAttribute('href');
}
]);
}
return $news;
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/Divider.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
class Divider implements \Serps\SearchEngine\Google\Parser\ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
/**
* Divider should not be parsed and for performance we just skip the parsing
*/
if ('hr' == $node->tagName || 'rgsep' == $node->getAttribute('class')) {
return self::RULE_MATCH_STOP;
}
}
public function parse(GoogleDom $googleDOM, \DomElement $group, IndexedResultSet $resultSet)
{
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/Flight.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\NaturalResultType;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
class Flight implements \Serps\SearchEngine\Google\Parser\ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
if ('flun' == $node->getAttribute('id')) {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
}
public function parse(GoogleDom $googleDOM, \DomElement $group, IndexedResultSet $resultSet)
{
$resultSet->addItem(new BaseResult(NaturalResultType::FLIGHTS, []));
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/ImageGroup.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\Core\Media\MediaFactory;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\Core\UrlArchive;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
use Serps\SearchEngine\Google\NaturalResultType;
class ImageGroup implements \Serps\SearchEngine\Google\Parser\ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
if ($node->hasAttribute('id') && $node->getAttribute('id') == 'imagebox_bigimages') {
return self::RULE_MATCH_MATCHED;
} else {
return self::RULE_MATCH_NOMATCH;
}
}
public function parse(GoogleDom $googleDOM, \DomElement $node, IndexedResultSet $resultSet)
{
$item = [
'images' => [],
'isCarousel' => false,
'moreUrl' => function () use ($node, $googleDOM) {
$aTag = $googleDOM->getXpath()->query('descendant::div[@class="_Icb _kk _wI"]/a', $node)->item(0);
if (!$aTag) {
return $googleDOM->getUrl()->resolve('/');
}
return $googleDOM->getUrl()->resolveAsString($aTag->getAttribute('href'));
}
];
// TODO: detect no image (google dom update)
$imageNodes = $googleDOM->cssQuery('.rg_ul>div._ZGc a', $node);
foreach ($imageNodes as $imgNode) {
$item['images'][] = $this->parseItem($googleDOM, $imgNode);
}
$resultSet->addItem(new BaseResult(NaturalResultType::IMAGE_GROUP, $item));
}
/**
* @param GoogleDOM $googleDOM
* @param \DOMElement $imgNode
* @return array
*/
private function parseItem(GoogleDom $googleDOM, \DOMElement $imgNode)
{
$data = [
'sourceUrl' => function () use ($imgNode, $googleDOM) {
$img = $googleDOM->getXpath()->query('descendant::img', $imgNode)->item(0);
if (!$img) {
return $googleDOM->getUrl()->resolve('/');
}
return $googleDOM->getUrl()->resolveAsString($img->getAttribute('title'));
},
'targetUrl' => function () use ($imgNode, $googleDOM) {
return $googleDOM->getUrl()->resolveAsString($imgNode->getAttribute('href'));
},
'image' => function () use ($imgNode, $googleDOM) {
$img = $googleDOM->getXpath()->query('descendant::img', $imgNode)->item(0);
if (!$img) {
return '';
}
return MediaFactory::createMediaFromSrc($img->getAttribute('src'));
},
];
return new BaseResult(NaturalResultType::IMAGE_GROUP_IMAGE, $data);
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/ImageGroupCarousel.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\Core\Media\MediaFactory;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\Core\UrlArchive;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
use Serps\SearchEngine\Google\NaturalResultType;
class ImageGroupCarousel implements \Serps\SearchEngine\Google\Parser\ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
if ($dom->cssQuery('._ekh image-viewer-group g-scrolling-carousel', $node)->length == 1) {
return self::RULE_MATCH_MATCHED;
} else {
return self::RULE_MATCH_NOMATCH;
}
}
public function parse(GoogleDom $googleDOM, \DomElement $node, IndexedResultSet $resultSet)
{
$item = [
'images' => function () use ($node, $googleDOM) {
$items = [];
$imageNodes = $googleDOM->cssQuery('.rg_ul>._sqh g-inner-card', $node);
foreach ($imageNodes as $imageNode) {
$items[] = $this->parseItem($googleDOM, $imageNode);
}
return $items;
},
'isCarousel' => true,
'moreUrl' => function () use ($node, $googleDOM) {
$a = $googleDOM->cssQuery('g-tray-header ._Nbi a');
$a = $a->item(0);
if ($a instanceof \DOMElement) {
return $googleDOM->getUrl()->resolveAsString($a->getAttribute('href'));
}
return null;
}
];
$resultSet->addItem(new BaseResult(NaturalResultType::IMAGE_GROUP, $item));
}
/**
* @param GoogleDOM $googleDOM
* @param \DOMElement $imgNode
* @return array
*
*/
private function parseItem(GoogleDom $googleDOM, \DOMElement $imgNode)
{
$data = [
'sourceUrl' => function () use ($imgNode, $googleDOM) {
$node = $googleDOM->cssQuery('.rg_meta', $imgNode)->item(0);
if (!$node) {
return null;
}
$url = $googleDOM->getJsonNodeProperty('ru', $node);
return $url;
},
'targetUrl' => function () use ($imgNode, $googleDOM) {
// not available for mobile results
return null;
},
'image' => function () use ($imgNode, $googleDOM) {
// TODO: maybe parse from javascript source
$img = $googleDOM->cssquery('.iuth>img')->item(0);
if (!$img) {
return null;
}
return MediaFactory::createMediaFromSrc($img->getattribute('src'));
},
];
return new BaseResult(NaturalResultType::IMAGE_GROUP_IMAGE, $data);
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/InTheNews.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\Core\UrlArchive;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
use Serps\SearchEngine\Google\NaturalResultType;
class InTheNews implements \Serps\SearchEngine\Google\Parser\ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
$child = $node->firstChild;
if (!$child || !($child instanceof \DOMElement)) {
return self::RULE_MATCH_NOMATCH;
}
if ($child->getAttribute('class') == 'mnr-c _yE') {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
}
public function parse(GoogleDom $googleDOM, \DomElement $group, IndexedResultSet $resultSet)
{
$item = [
'news' => []
];
$xpathCards = "div/div[contains(concat(' ',normalize-space(@class),' '),' card-section ')]";
$cardNodes = $googleDOM->getXpath()->query($xpathCards, $group);
foreach ($cardNodes as $cardNode) {
$item['news'][] = $this->parseItem($googleDOM, $cardNode);
}
$resultSet->addItem(new BaseResult(NaturalResultType::IN_THE_NEWS, $item));
}
/**
* @param GoogleDOM $googleDOM
* @param \DomElement $node
* @return array
*/
protected function parseItem(GoogleDom $googleDOM, \DomElement $node)
{
$card = [];
$xpathTitle = "descendant::a[@class = '_Dk']";
$aTag = $googleDOM->getXpath()->query($xpathTitle, $node)->item(0);
if ($aTag) {
$card['title'] = $aTag->nodeValue;
$card['url'] = $aTag->getAttribute('href');
$card['description'] = function () use ($googleDOM, $node) {
$span = $googleDOM->getXpath()->query("descendant::span[@class='_dwd st s std']", $node);
if ($span && $span->length > 0) {
return $span->item(0)->nodeValue;
}
return null;
};
}
return new BaseResult('', $card);
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/KnowledgeCard.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\Core\Dom\DomElement;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\NaturalResultType;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
class KnowledgeCard implements ParsingRuleInterface
{
public function match(GoogleDom $dom, DomElement $node)
{
if ($node->hasClass('mnr-c') && $node->hasClass('kno-kp')) {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
}
public function parse(GoogleDom $googleDOM, \DomElement $node, IndexedResultSet $resultSet)
{
$data = [
'title' => function () use ($googleDOM, $node) {
$item = $googleDOM->cssQuery('._OKe ._Q1n ._sdf');
if (!$item->length) {
$item = $googleDOM->cssQuery('.d1rFIf>.kno-ecr-pt>span');
}
return $item->getNodeAt(0)->getNodeValue();
},
'shortDescription' => function () use ($googleDOM, $node) {
$item = $googleDOM->cssQuery('._OKe ._Q1n ._gdf', $node); // appears to be outdated
if (!$item->length) {
$item = $googleDOM->cssQuery('.sthby', $node);
}
return $item->getNodeAt(0)->getNodeValue();
}
];
$resultSet->addItem($a = new BaseResult(NaturalResultType::KNOWLEDGE, $data));
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/Map.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\Core\Dom\DomElement;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
use Serps\SearchEngine\Google\NaturalResultType;
class Map implements ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
if ($dom->cssQuery('.AEprdc.vk_c', $node)->length == 1) {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
}
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet)
{
$item = [
'localPack' => function () use ($node, $dom) {
$localPackNodes = $dom->cssQuery('.ccBEnf>div', $node);
$data = [];
foreach ($localPackNodes as $localPack) {
$data[] = new BaseResult(NaturalResultType::MAP_PLACE, $this->parseItem($localPack, $dom));
}
return $data;
},
'mapUrl' => function () use ($node, $dom) {
$mapATag = $dom->cssQuery('#lu_map', $node)->item(0)->parentNode;
if ($mapATag) {
return $dom->getUrl()->resolveAsString($mapATag->getAttribute('href'));
}
return null;
}
];
$resultSet->addItem(new BaseResult(NaturalResultType::MAP, $item));
}
private function parseItem($localPack, GoogleDom $dom)
{
return [
'title' => function () use ($localPack, $dom) {
return $dom->cssQuery('.dbg0pd', $localPack)->getNodeAt(0)->getNodeValue();
},
'url' => function () use ($localPack, $dom) {
// we search for explicit <a> with href to the website.
// if not found the url is sometimes in a <link> tag
$nodes = $dom->cssQuery('a.L48Cpd', $localPack);
if ($nodes->length > 0) {
return $nodes->getNodeAt(0)->getAttribute('href');
} else {
return $dom->cssQuery('link[href]', $localPack)
->getNodeAt(0)
->getAttribute('href');
}
},
'street' => function () use ($localPack, $dom) {
$v = $dom->cssQuery(
'.rllt__details>div:nth-child(3)>span',
$localPack
)->getNodeAt(0)->getNodeValue();
if ($v) {
return $v;
} else {
return $dom->cssQuery(
'.rllt__details>div:nth-child(1)>span',
$localPack
)->getNodeAt(0)->getNodeValue();
}
},
'stars' => function () use ($localPack, $dom) {
$rating = $dom->cssQuery('.BTtC6e', $localPack)->getNodeAt(0)->getNodeValue();
// transforms "4,4" to 4.4
return $rating ? (float)str_replace(',', '.', $rating) : null;
},
'review' => function () use ($localPack, $dom) {
$review = $dom->cssQuery(
'.BTtC6e',
$localPack
)->getNodeAt(0);
if ($review instanceof DomElement) {
$value = $review->parentNode->getNodeValue();
} else {
return null;
}
if ($value && preg_match('/(\([0-9 ,\.]+\))/', $value, $matches)) {
// transform '(1 000)' or '(1,000)', etc... to 1000
return (int) preg_replace('/[^0-9]/', '', $matches[1]);
}
return null;
},
'phone' => function () use ($localPack, $dom) {
$item = $dom->cssQuery(
'.rllt__details>div:nth-child(3)',
$localPack
)->item(0);
if (!$item) {
$item = $dom->cssQuery(
'.rllt__details>div:nth-child(1)',
$localPack
)->item(0);
}
if ($item) {
if ($item->childNodes->length > 1 && $item->childNodes->item(1) instanceof \DOMText) {
return trim($item->childNodes->item(1)->nodeValue, ' ·');
}
}
return null;
},
];
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/MapLegacy.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\Core\UrlArchive;
use Serps\SearchEngine\Google\GoogleUrlArchive;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
use Serps\SearchEngine\Google\NaturalResultType;
/**
* This is kept for BC reasons, to be removed in the future
* TODO
* @deprecated
*/
class MapLegacy implements ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
if ($dom->cssQuery('._RBh', $node)->length > 1) {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
}
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet)
{
$xPath = $dom->getXpath();
$item = [
'localPack' => function () use ($xPath, $node, $dom) {
$localPackNodes = $xPath->query('descendant::div[@class="_gt"]', $node);
$data = [];
foreach ($localPackNodes as $localPack) {
$data[] = new BaseResult(NaturalResultType::MAP_PLACE, $this->parseItem($localPack, $dom));
}
return $data;
},
'mapUrl' => function () use ($xPath, $node, $dom) {
$mapATag = $dom->cssQuery('#lu_map', $node)->item(0)->parentNode;
if ($mapATag) {
return $dom->getUrl()->resolveAsString($mapATag->getAttribute('href'));
}
return null;
}
];
$resultSet->addItem(new BaseResult(NaturalResultType::MAP, $item));
}
private function parseItem($localPack, GoogleDom $dom)
{
return [
'title' => function () use ($localPack, $dom) {
$item = $dom->cssQuery('._rl', $localPack)->item(0);
if ($item) {
return $item->nodeValue;
}
return null;
},
'url' => function () use ($localPack, $dom) {
$item = $dom->getXpath()->query('descendant::a', $localPack)->item(1);
if ($item) {
return $item->getAttribute('href');
}
return null;
},
'street' => function () use ($localPack, $dom) {
$item = $dom->cssQuery(
'._iPk>span.rllt__details>div:nth-child(3)>span',
$localPack
)->item(0);
if ($item) {
return $item->nodeValue;
}
return null;
},
'stars' => function () use ($localPack, $dom) {
$item = $dom->cssQuery('._PXi', $localPack)->item(0);
if ($item) {
return $item->nodeValue;
}
return null;
},
'review' => function () use ($localPack, $dom) {
$item = $dom->cssQuery(
'._iPk>span.rllt__details>div:nth-child(1)',
$localPack
)->item(0);
if ($item) {
if ($item->childNodes->length > 0 && !($item->childNodes->item(0) instanceof \DOMText)) {
return null;
} else {
return trim(explode('·', $item->nodeValue)[0]);
}
}
return null;
},
'phone' => function () use ($localPack, $dom) {
$item = $dom->cssQuery(
'._iPk>span.rllt__details>div:nth-child(3)',
$localPack
)->item(0);
if ($item) {
if ($item->childNodes->length > 1 && $item->childNodes->item(1) instanceof \DOMText) {
return trim($item->childNodes->item(1)->nodeValue, ' ·');
}
}
return null;
},
];
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/MapMobile.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\Core\Dom\DomElement;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
use Serps\SearchEngine\Google\NaturalResultType;
class MapMobile implements ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
if ($dom->cssQuery('img.wfAGXd', $node)->length == 1) {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
}
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet)
{
$item = [
'localPack' => function () use ($node, $dom) {
$localPackNodes = $dom->cssQuery('.PX16ld', $node);
$data = [];
foreach ($localPackNodes as $localPack) {
$data[] = new BaseResult(NaturalResultType::MAP_PLACE, $this->parseItem($localPack, $dom));
}
return $data;
},
'mapUrl' => function () use ($node, $dom) {
return null;
}
];
$resultSet->addItem(new BaseResult(NaturalResultType::MAP, $item));
}
private function parseItem($localPack, GoogleDom $dom)
{
return [
'title' => function () use ($localPack, $dom) {
return $dom->cssQuery('.kR1eme', $localPack)->getNodeAt(0)->getNodeValue();
},
'url' => function () use ($localPack, $dom) {
$nodes = $dom->cssQuery('a', $localPack);
$href = $nodes
->getNodeAt(0)
->getAttribute('href');
if ($href) {
return $dom->getUrl()->resolveAsString($href);
}
},
'street' => function () use ($localPack, $dom) {
// TODO
return null;
},
'stars' => function () use ($localPack, $dom) {
$rating = $dom->cssQuery('.BTtC6e', $localPack)->getNodeAt(0)->getNodeValue();
// transforms "4,4" to 4.4
return $rating ? (float)str_replace(',', '.', $rating) : null;
},
'review' => function () use ($localPack, $dom) {
$review = $dom->cssQuery(
'.BTtC6e',
$localPack
)->getNodeAt(0);
if ($review instanceof DomElement) {
$value = $review->parentNode->getNodeValue();
} else {
return null;
}
if ($value && preg_match('/(\([0-9 ,\.]+\))/', $value, $matches)) {
// transform '(1 000)' or '(1,000)', etc... to 1000
return (int) preg_replace('/[^0-9]/', '', $matches[1]);
}
return null;
},
'phone' => function () use ($localPack, $dom) {
return null;
},
];
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/PeopleAlsoAsk.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\Core\Dom\DomElement;
use Serps\Core\Dom\DomNodeList;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\NaturalResultType;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
/**
* Class PeopleAlsoAsk
* @package Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural
*
* Note: "people also ask" would results would also match against "Knowledge" parser.
* For this reason "paa" parser must be processed before knowledge parser.
*
*/
class PeopleAlsoAsk implements ParsingRuleInterface
{
public function match(GoogleDom $dom, DomElement $node)
{
if ($node->hasClasses(['kno-kp', 'mnr-c'])) {
$childNodes = new DomNodeList($node->childNodes, $dom);
if ($childNodes->hasAnyClass(['cUnQKe', '_thf'])) { // TODO "_thf" kept for BC, remove in future
return self::RULE_MATCH_MATCHED;
}
}
return self::RULE_MATCH_NOMATCH;
}
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet)
{
$data = [
'questions' => function () use ($dom, $node) {
$items = [];
$nodes = $dom->cssQuery('.related-question-pair', $node);
foreach ($nodes as $questionNode) {
$items[] = new BaseResult(NaturalResultType::PAA_QUESTION, [
'question' => function () use ($questionNode, $dom) {
return $questionNode->getNodeValue();
}
]);
}
return $items;
}
];
$resultSet->addItem($a = new BaseResult(NaturalResultType::PEOPLE_ALSO_ASK, $data));
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/SearchResultGroup.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
/**
* This is a group of results that need to be sub-parsed
*/
class SearchResultGroup implements ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
if ($node->hasAnyClass(['srg', '_NId', 'bkWMgd'])) {
return $node->childNodes;
} else {
return self::RULE_MATCH_NOMATCH;
}
}
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet)
{
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/TopStoriesCarousel.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\Core\Serp\ResultSet;
use Serps\SearchEngine\Google\NaturalResultType;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
class TopStoriesCarousel implements ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
if ($dom->cssQuery('h3._MRj', $node)->length == 1
&& $dom->cssQuery('g-scrolling-carousel._Ncr', $node)->length == 1
// Dont use _JTg or _bfj class because it's common to all carousel
) {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
}
private function parseNode(GoogleDom $dom, $node)
{
return [
'isCarousel' => true,
'isVertical' => false,
'news' => function () use ($dom, $node) {
$news = [];
$nodes = $dom->cssQuery('._Ocr>._Pcr', $node);
foreach ($nodes as $newsNode) {
$news[] = new BaseResult(NaturalResultType::TOP_STORIES_NEWS_CAROUSEL, [
'title' => function () use ($dom, $newsNode) {
$el = $dom->cssQuery('._IRj', $newsNode)->item(0);
return $el->nodeValue;
},
'url' => function () use ($dom, $newsNode) {
$el = $dom->cssQuery('g-inner-card._KBh>a', $newsNode)->item(0);
return $el->getAttribute('href');
}
]);
}
$resultSet = new ResultSet();
$resultSet->addItems($news);
return $resultSet;
}
];
}
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet)
{
$item = new BaseResult(
[NaturalResultType::TOP_STORIES],
$this->parseNode($dom, $node)
);
$resultSet->addItem($item);
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/TopStoriesVertical.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\Core\Serp\ResultSet;
use Serps\SearchEngine\Google\NaturalResultType;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
class TopStoriesVertical implements ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
if ($dom->cssQuery('h3._MRj', $node)->length == 1
&& $dom->cssQuery('g-scrolling-carousel._Ncr', $node)->length == 0
) {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
}
private function parseNode(GoogleDom $dom, $node)
{
return [
'isCarousel' => false,
'isVertical' => true,
'news' => function () use ($dom, $node) {
$news = [];
$nodes = $dom->cssQuery('._KBh', $node);
foreach ($nodes as $newsNode) {
$news[] = new BaseResult(NaturalResultType::TOP_STORIES_NEWS_VERTICAL, [
'title' => function () use ($dom, $newsNode) {
$el = $dom->cssQuery('a', $newsNode)->item(0);
return $el->nodeValue;
},
'url' => function () use ($dom, $newsNode) {
$el = $dom->cssQuery('a', $newsNode)->item(0);
return $el->getAttribute('href');
}
]);
}
$resultSet = new ResultSet();
$resultSet->addItems($news);
return $resultSet;
}
];
}
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet)
{
$item = new BaseResult(
[NaturalResultType::TOP_STORIES],
$this->parseNode($dom, $node)
);
$resultSet->addItem($item);
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/TweetsCarousel.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
use Serps\SearchEngine\Google\NaturalResultType;
class TweetsCarousel implements ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
if ($dom->cssQuery('.g ._BOf', $node)->length) {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
}
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet)
{
$xpath = $dom->getXpath();
/* @var $aTag \DOMElement */
$aTag=$xpath
->query("descendant::h3[@class='r'][1]//a", $node)
->item(0);
if ($aTag) {
$title = $aTag->nodeValue;
preg_match('/@([A-Za-z0-9_]{1,15})/', $title, $match);
$data = [
'title' => $title,
'url' => $aTag->getAttribute('href'),
'user' => isset($match[0]) ? $match[0] : null
];
$item = new BaseResult(NaturalResultType::TWEETS_CAROUSEL, $data);
$resultSet->addItem($item);
}
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/TweetsCarouselZ1m.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\Exception;
use Serps\SearchEngine\Google\Exception\InvalidDOMException;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
use Serps\SearchEngine\Google\NaturalResultType;
/**
* A slightly different implementation of the twitter carousel that is found on mobile results under the element ._Z1m
*/
class TweetsCarouselZ1m implements ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
if ($node->childNodes->length == 1) {
$childNode = $node->getChildren()->getNodeAt(0);
$subChildNode = $childNode->getChildren()->getNodeAt(0);
if ($childNode->hasClass('_Z1m') && $subChildNode->hasClass('_ujp')) {
return self::RULE_MATCH_MATCHED;
}
}
return self::RULE_MATCH_NOMATCH;
}
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet)
{
$item = new BaseResult(NaturalResultType::TWEETS_CAROUSEL, [
'url' => function () use ($dom, $node) {
$res = $dom->cssQuery('._Z1m>._ujp>a', $node);
if ($res->length == 1) {
return $res->item(0)->getAttribute('href');
} else {
throw new InvalidDOMException('Cannot parse url for twitter carousel.');
}
},
'title' => function () use ($dom, $node) {
return $dom
->cssQuery('._ees', $node)
->item(0)
->nodeValue;
},
'destination' => function () use ($dom, $node) {
return $dom
->cssQuery('span._Clt', $node)
->item(0)
->nodeValue;
},
'user' => function (BaseResult $result) {
$url = $result->getDataValue('url');
$match = preg_match('~twitter.com/([^/?#]+)~', $url, $matches);
if ($match) {
return '@' . $matches[1];
}
return null;
}
]);
$resultSet->addItem($item);
}
}
================================================
FILE: src/Parser/Evaluated/Rule/Natural/VideoGroup.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser\Evaluated\Rule\Natural;
use Serps\Core\Media\MediaFactory;
use Serps\Core\Serp\BaseResult;
use Serps\Core\Serp\IndexedResultSet;
use Serps\SearchEngine\Google\NaturalResultType;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\SearchEngine\Google\Parser\ParsingRuleInterface;
/**
* This rule extracts video groups as present on mobile results
*/
class VideoGroup implements ParsingRuleInterface
{
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
{
if ($dom->cssQuery('._Fzo', $node)->length == 1) {
return self::RULE_MATCH_MATCHED;
}
return self::RULE_MATCH_NOMATCH;
}
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet)
{
$item = [
'videos' => function () use ($node, $dom) {
$items = [];
$nodes = $dom->cssQuery('._ERj', $node);
foreach ($nodes as $node) {
$items[] = new BaseResult(NaturalResultType::VIDEO_GROUP_VIDEO, [
'image' => function () use ($node, $dom) {
$data = $dom->cssQuery('g-img img', $node)->getNodeAt(0)->getAttribute('src');
return MediaFactory::createMediaFromSrc($data);
},
'title' => function () use ($node, $dom) {
return $dom->cssQuery('._IRj', $node)->getNodeAt(0)->getNodeValue();
},
'url' => function () use ($node, $dom) {
return $dom->cssQuery('g-inner-card a', $node)->getNodeAt(0)->getAttribute('href');
}
]);
}
return $items;
}
];
$resultSet->addItem(new BaseResult(NaturalResultType::VIDEO_GROUP, $item));
}
}
================================================
FILE: src/Parser/ParserInterface.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser;
use Serps\Core\Serp\ResultSetInterface;
use Serps\SearchEngine\Google\Page\GoogleDom;
interface ParserInterface
{
/**
* @param GoogleDom $googleDom
* @return ResultSetInterface
*/
public function parse(GoogleDom $googleDom);
}
================================================
FILE: src/Parser/ParsingRuleInterface.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\SearchEngine\Google\Parser;
use Serps\SearchEngine\Google\Page\GoogleDom;
use Serps\Core\Serp\IndexedResultSet;
interface ParsingRuleInterface
{
const RULE_MATCH_MATCHED = 1;
const RULE_MATCH_NOMATCH = 2;
const RULE_MATCH_STOP = 3;
public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node);
public function parse(GoogleDom $dom, \DomElement $node, IndexedResultSet $resultSet);
}
================================================
FILE: stubs/RelatedSearch.php
================================================
<?php
/**
* @license see LICENSE
*/
namespace Serps\Stubs;
class RelatedSearch
{
/**
* @var string url of the related search
*/
public $url;
/**
* @var string title of the related search
*/
public $title;
}
================================================
FILE: test/bin/ci.bash
================================================
#!/bin/bash
set -e
SCRIPTFILE=$(readlink -f "$0")
SCRIPTDIR=$(dirname "$SCRIPTFILE")
echo -e "\e[34m"
echo "======================"
echo -e "= \e[1m\e[33mRunning unit tests\e[0m\e[34m ="
echo -e "======================\e[39m"
php $SCRIPTDIR/../../vendor/bin/phpunit -c "$SCRIPTDIR/../../phpunit.dist.xml"
echo -e "\e[34m"
echo "================================="
echo -e "= \e[1m\e[33mChecking code style standards\e[0m\e[34m ="
echo -e "=================================\e[39m"
$SCRIPTDIR/phpcs.bash $1
echo "Code standards: OK"
echo -e "\e[34m"
echo "============================"
echo -e "= \e[1m\e[33mResporting code coverage\e[0m\e[34m ="
echo -e "============================\e[39m"
if [ "$PROCESS_CODECLIMATE" = true ] && [ "${TRAVIS_PULL_REQUEST}" = "false" ] && [ "${TRAVIS_BRANCH}" = "master" ]
then
composer require codeclimate/php-test-reporter:dev-master
./vendor/bin/test-reporter
else
echo "Skip code coverage report..."
fi
================================================
FILE: test/bin/phpcbf.bash
================================================
#!/bin/bash
SCRIPTFILE=$(readlink -f "$0")
SCRIPTDIR=$(dirname "$SCRIPTFILE")
cd $SCRIPTDIR/../.. && $SCRIPTDIR/../../vendor/bin/phpcbf --standard="$SCRIPTDIR/../../phpcs.xml"
exit 0
================================================
FILE: test/bin/phpcs.bash
================================================
#!/bin/bash
SCRIPTFILE=$(readlink -f "$0")
SCRIPTDIR=$(dirname "$SCRIPTFILE")
if [ -n "$1" ]; then report=$1; else report="summary"; fi
cd $SCRIPTDIR/../.. && $SCRIPTDIR/../../vendor/bin/phpcs --standard="$SCRIPTDIR/../../phpcs.xml" --report=$report -s
================================================
FILE: test/bin/test.bash
================================================
#!/bin/bash
SCRIPTFILE=$(readlink -f "$0")
SCRIPTDIR=$(dirname "$SCRIPTFILE")
if [ -n "$1" ]; then filter=$1; else filter="."; fi
cd $SCRIPTDIR/../.. && $SCRIPTDIR/../../vendor/bin/phpunit -c phpunit.dist.xml --filter $filter
================================================
FILE: test/resources/pages-evaluated/2018/03/asian+massage.html
================================================
<!DOCTYPE html>
<?xml encoding="UTF-8"><html itemscope="" itemtype="http://schema.org/SearchResultsPage" lang="en-GB"><head><meta content="/images/branding/googleg/1x/googleg_standard_color_128dp.png" itemprop="image"><link href="/images/branding/product/ico/googleg_lodp.ico" rel="shortcut icon"><meta content="origin" name="referrer"><title>asian massage - Google Search</title><script nonce="KYI43wH9N6dmn39dV/OrTw==">(function(){window.google={kEI:'mvm4Wo7vNYnmUpTutfgB',kEXPI:'31',authuser:0,kscs:'c9c918f0_mvm4Wo7vNYnmUpTutfgB',u:'c9c918f0',kGL:'GB'};google.kHL='en-GB';})();(function(){google.lc=[];google.li=0;google.getEI=function(a){for(var b;a&&(!a.getAttribute||!(b=a.getAttribute("eid")));)a=a.parentNode;return b||google.kEI};google.getLEI=function(a){for(var b=null;a&&(!a.getAttribute||!(b=a.getAttribute("leid")));)a=a.parentNode;return b};google.https=function(){return"https:"==window.location.protocol};google.ml=function(){return null};google.wl=function(a,b){try{google.ml(Error(a),!1,b)}catch(d){}};google.time=function(){return(new Date).getTime()};google.log=function(a,b,d,c,g){if(a=google.logUrl(a,b,d,c,g)){b=new Image;var e=google.lc,f=google.li;e[f]=b;b.onerror=b.onload=b.onabort=function(){delete e[f]};google.vel&&google.vel.lu&&google.vel.lu(a);b.src=a;google.li=f+1}};google.logUrl=function(a,b,d,c,g){var e="",f=google.ls||"";d||-1!=b.search("&ei=")||(e="&ei="+google.getEI(c),-1==b.search("&lei=")&&(c=google.getLEI(c))&&(e+="&lei="+c));c="";!d&&google.cshid&&-1==b.search("&cshid=")&&(c="&cshid="+google.cshid);a=d||"/"+(g||"gen_204")+"?atyp=i&ct="+a+"&cad="+b+e+f+"&zx="+google.time()+c;/^http:/i.test(a)&&google.https()&&(google.ml(Error("a"),!1,{src:a,glmm:1}),a="");return a};}).call(this);(function(){google.y={};google.x=function(a,b){if(a)var c=a.id;else{do c=Math.random();while(google.y[c])}google.y[c]=[a,b];return!1};google.lm=[];google.plm=function(a){google.lm.push.apply(google.lm,a)};google.lq=[];google.load=function(a,b,c){google.lq.push([[a],b,c])};google.loadAll=function(a,b){google.lq.push([a,b])};}).call(this);google.f={};(function(){google.hs={h:true};})();google.arwt=function(a){a.href=document.getElementById(a.id.substring(1)).href;return!0};(function(){var k=this,l=function(){},t=Date.now||function(){return+new Date};var v={};var w=function(a,d){if(null===d)return!1;if("contains"in a&&1==d.nodeType)return a.contains(d);if("compareDocumentPosition"in a)return a==d||!!(a.compareDocumentPosition(d)&16);for(;d&&a!=d;)d=d.parentNode;return d==a};var B=function(a,d){return function(b){b||(b=window.event);return d.call(a,b)}},C=function(a){a=a.target||a.srcElement;!a.getAttribute&&a.parentNode&&(a=a.parentNode);return a},D="undefined"!=typeof navigator&&/Macintosh/.test(navigator.userAgent),E="undefined"!=typeof navigator&&!/Opera/.test(navigator.userAgent)&&/WebKit/.test(navigator.userAgent),aa={A:1,INPUT:1,TEXTAREA:1,SELECT:1,BUTTON:1},ba=function(){this._mouseEventsPrevented=!0},F={A:13,BUTTON:0,CHECKBOX:32,COMBOBOX:13,GRIDCELL:13,LINK:13,LISTBOX:13,MENU:0,MENUBAR:0,MENUITEM:0,MENUITEMCHECKBOX:0,MENUITEMRADIO:0,OPTION:0,RADIO:32,RADIOGROUP:32,RESET:0,SUBMIT:0,TAB:0,TREE:13,TREEITEM:13},G=function(a){return(a.getAttribute("type")||a.tagName).toUpperCase()in ca},H=function(a){return(a.getAttribute("type")||a.tagName).toUpperCase()in da},ca={CHECKBOX:!0,OPTION:!0,RADIO:!0},da={COLOR:!0,DATE:!0,DATETIME:!0,"DATETIME-LOCAL":!0,EMAIL:!0,MONTH:!0,NUMBER:!0,PASSWORD:!0,RANGE:!0,SEARCH:!0,TEL:!0,TEXT:!0,TEXTAREA:!0,TIME:!0,URL:!0,WEEK:!0},ea={A:!0,AREA:!0,BUTTON:!0,DIALOG:!0,IMG:!0,INPUT:!0,LINK:!0,MENU:!0,OPTGROUP:!0,OPTION:!0,PROGRESS:!0,SELECT:!0,TEXTAREA:!0};var I=function(){this.i=this.g=null},K=function(a,d){var b=J;b.g=a;b.i=d;return b};I.prototype.h=function(){var a=this.g;this.g&&this.g!=this.i?this.g=this.g.__owner||this.g.parentNode:this.g=null;return a};var L=function(){this.l=[];this.g=0;this.i=null;this.o=!1};L.prototype.h=function(){if(this.o)return J.h();if(this.g!=this.l.length){var a=this.l[this.g];this.g++;a!=this.i&&a&&a.__owner&&(this.o=!0,K(a.__owner,this.i));return a}return null};var J=new I,M=new L;var P=function(){this.w=[];this.g=[];this.h=[];this.o={};this.i=null;this.l=[];this.C=l;O(this,"_custom")},fa="undefined"!=typeof navigator&&/iPhone|iPad|iPod/.test(navigator.userAgent),Q=String.prototype.trim?function(a){return a.trim()}:function(a){return a.replace(/^\s+/,"").replace(/\s+$/,"")},ha=/\s*;\s*/,la=function(a,d){return function(b){var c=d;if("_custom"==c){c=b.detail;if(!c||!c._type)return;c=c._type}if("click"==c&&(D&&b.metaKey||!D&&b.ctrlKey||2==b.which||null==b.which&&4==b.button||
"auxclick"==b.type||b.shiftKey))c="clickmod";else{var e=b.which||b.keyCode||b.key;E&&3==e&&(e=13);if(13!=e&&32!=e)e=!1;else{var f=C(b),q=(f.getAttribute("role")||f.type||f.tagName).toUpperCase(),h;(h="keydown"!=b.type)||("getAttribute"in f?(h=(f.getAttribute("role")||f.tagName).toUpperCase(),h=!H(f)&&("COMBOBOX"!=h||"INPUT"!=h)&&!f.isContentEditable):h=!1,h=!h);(h=h||b.ctrlKey||b.shiftKey||b.altKey||b.metaKey||G(f)&&32==e)||((h=f.tagName in aa)||(h=f.getAttributeNode("tabindex"),h=null!=h&&h.specified),h=!(h&&!f.disabled));h?e=!1:(f="INPUT"!=f.tagName.toUpperCase()||f.type,h=!(q in F)&&13==e,e=(0==F[q]%e||h)&&!!f)}e&&(c="clickkey")}q=b.srcElement||b.target;e=R(c,b,q,"",null);b.path?(M.l=b.path,M.g=0,M.i=this,M.o=!1,f=M):f=K(q,this);for(;h=f.h();){var m=h;var g=m;h=c;var p=g.__jsaction;if(!p){var u=null;"getAttribute"in g&&(u=g.getAttribute("jsaction"));if(u){p=v[u];if(!p){p={};for(var x=u.split(ha),y=0,ia=x?x.length:0;y<ia;y++){var r=x[y];if(r){var z=r.indexOf(":"),N=-1!=z,ja=N?Q(r.substr(0,z)):"click";r=N?Q(r.substr(z+1)):r;p[ja]=r}}v[u]=p}g.__jsaction=p}else p=ka,g.__jsaction=p}"clickkey"==h?h="click":"click"!=h||p.click||(h="clickonly");g={v:h,action:p[h]||"",event:null,B:!1};e=R(g.v,g.event||b,q,g.action||"",m,e.timeStamp);if(g.B||g.action)break}e&&"touchend"==e.eventType&&(e.event._preventMouseEvents=ba);if(g&&g.action){if(g="clickkey"==c)g=C(b),g=(g.type||g.tagName).toUpperCase(),(g=32==(b.which||b.keyCode||b.key)&&"CHECKBOX"!=g)||(g=C(b),q=(g.getAttribute("role")||g.tagName).toUpperCase(),g=g.tagName.toUpperCase()in ea&&"A"!=q&&!G(g)&&!H(g)||"BUTTON"==q);g&&(b.preventDefault?b.preventDefault():b.returnValue=!1);if("mouseenter"==c||"mouseleave"==c)if(g=b.relatedTarget,!("mouseover"==b.type&&"mouseenter"==c||"mouseout"==b.type&&"mouseleave"==c)||g&&(g===m||w(m,g)))e.action="",e.actionElement=null;else{c={};for(var n in b)"function"!==typeof b[n]&&"srcElement"!==n&&"target"!==n&&(c[n]=b[n]);c.type="mouseover"==b.type?"mouseenter":"mouseleave";c.target=c.srcElement=m;c.bubbles=!1;e.event=
c;e.targetElement=m}}else e.action="",e.actionElement=null;m=e;a.i&&(n=R(m.eventType,m.event,m.targetElement,m.action,m.actionElement,m.timeStamp),"clickonly"==n.eventType&&(n.eventType="click"),a.i(n,!0));if(m.actionElement){"A"!=m.actionElement.tagName||"click"!=m.eventType&&"clickmod"!=m.eventType||m.actionElement.hasAttribute("data-unjs")&&null==a.i||(b.preventDefault?b.preventDefault():b.returnValue=!1);if(a.i)a.i(m);else{a.C(m);if((n=k.document)&&!n.createEvent&&n.createEventObject)try{var A=
n.createEventObject(b)}catch(oa){A=b}else A=b;m.event=A;a.l.push(m)}if("touchend"==m.event.type&&m.event._mouseEventsPrevented){b=m.event;for(var pa in b);t()}}}},R=function(a,d,b,c,e,f){return{eventType:a,event:d,targetElement:b,action:c,actionElement:e,timeStamp:f||t()}},ka={},ma=function(a,d){return function(b){var c=a,e=d,f=!1;"mouseenter"==c?c="mouseover":"mouseleave"==c&&(c="mouseout");if(b.addEventListener){if("focus"==c||"blur"==c||"error"==c||"load"==c)f=!0;b.addEventListener(c,e,f)}else b.attachEvent&&
("focus"==c?c="focusin":"blur"==c&&(c="focusout"),e=B(b,e),b.attachEvent("on"+c,e));return{v:c,s:e,capture:f}}},O=function(a,d){if(!a.o.hasOwnProperty(d)){var b=la(a,d),c=ma(d,b);a.o[d]=b;a.w.push(c);for(b=0;b<a.g.length;++b){var e=a.g[b];e.h.push(c.call(null,e.g))}"click"==d&&O(a,"keydown")}};P.prototype.s=function(a){return this.o[a]};var V=function(a,d){var b=new na(d),c;a:{for(c=0;c<a.g.length;c++)if(S(a.g[c],d)){c=!0;break a}c=!1}if(c)return a.h.push(b),b;T(a,b);a.g.push(b);U(a);return b},U=function(a){for(var d=a.h.concat(a.g),b=[],c=[],e=0;e<a.g.length;++e){var f=a.g[e];W(f,d)?(b.push(f),X(f)):c.push(f)}for(e=0;e<a.h.length;++e)f=a.h[e],W(f,d)?b.push(f):(c.push(f),T(a,f));a.g=c;a.h=b},T=function(a,d){var b=d.g;fa&&(b.style.cursor="pointer");for(b=0;b<a.w.length;++b)d.h.push(a.w[b].call(null,d.g))},Y=function(a,d){a.i=d;a.l&&
(0<a.l.length&&d(a.l),a.l=null)},na=function(a){this.g=a;this.h=[]},S=function(a,d){for(var b=a.g,c=d;b!=c&&c.parentNode;)c=c.parentNode;return b==c},W=function(a,d){for(var b=0;b<d.length;++b)if(d[b].g!=a.g&&S(d[b],a.g))return!0;return!1},X=function(a){for(var d=0;d<a.h.length;++d){var b=a.g,c=a.h[d];b.removeEventListener?b.removeEventListener(c.v,c.s,c.capture):b.detachEvent&&b.detachEvent("on"+c.v,c.s)}a.h=[]};var Z=new P;V(Z,window.document.documentElement);O(Z,"click");O(Z,"focus");O(Z,"focusin");O(Z,"blur");O(Z,"focusout");O(Z,"error");O(Z,"load");O(Z,"change");O(Z,"dblclick");O(Z,"input");O(Z,"keyup");O(Z,"keydown");O(Z,"keypress");O(Z,"mousedown");O(Z,"mouseenter");O(Z,"mouseleave");O(Z,"mouseout");O(Z,"mouseover");O(Z,"mouseup");O(Z,"paste");O(Z,"touchstart");O(Z,"touchend");O(Z,"touchcancel");O(Z,"speech");(function(a){google.jsad=function(d){Y(a,d)};google.jsaac=function(d){return V(a,d)};google.jsarc=function(d){X(d);for(var b=!1,c=0;c<a.g.length;++c)if(a.g[c]===d){a.g.splice(c,1);b=!0;break}if(!b)for(c=0;c<a.h.length;++c)if(a.h[c]===d){a.h.splice(c,1);break}U(a)}})(Z);window.gws_wizbind=function(a){return{trigger:function(d){var b=a.s(d.type);b||(O(a,d.type),b=a.s(d.type));var c=d.target||d.srcElement;b&&b.call(c.ownerDocument.documentElement,d)},bind:function(d){Y(a,d)}}}(Z);}).call(this);(function(){var a=[];google.jsc={xx:a,x:function(b){a.push(b)}};}).call(this);(function(){google.c={c:{a:true,d:true,m:true,n:false}};google.sn='web';(function(){var e=function(a,b,c){a.addEventListener?a.removeEventListener(b,c,!1):a.attachEvent&&a.detachEvent("on"+b,c)},g=function(a,b,c){f.push({g:a,h:b,i:c});a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent&&a.attachEvent("on"+b,c)},f=[];google.timers={};google.startTick=function(a,b){var c=b&&google.timers[b].t?google.timers[b].t.start:google.time();google.timers[a]={t:{start:c},e:{},m:{}};(c=window.performance)&&c.now&&(google.timers[a].wsrt=Math.floor(c.now()))};google.tick=function(a,b,c){google.timers[a]||google.startTick(a);c=void 0!==c?c:google.time();b instanceof Array||(b=[b]);for(var d=0;d<b.length;++d)google.timers[a].t[b[d].clearcut]={key:b[d],ts:c}};google.c.e=function(a,b,c){google.timers[a].e[b]=c};google.c.b=function(a){var b=google.timers.load.m;b[a]&&google.wl("ch_mab",{m:a});b[a]=!0};google.c.u=function(a){var b=google.timers.load.m;if(b[a]){b[a]=!1;for(a in b)if(b[a])return;google.csiReport()}else google.wl("ch_mnb",{m:a})};google.rll=function(a,b,c){var d=function(b){c(b);e(a,"load",d);e(a,"error",d)};g(a,"load",d);b&&g(a,"error",d)};google.ull=function(){for(var a;a=f.shift();)e(a.g,a.h,a.i)};google.afte=!0;google.aft=function(a){google.c.c.a&&google.afte&&(google.timers.aft||google.startTick("aft"),google.timers.aft.t[a.id||a.src||a.name]=google.time())};google.startTick("webaft");google.startTick("load");google.c.b("pr");google.c.b("xe");}).call(this);})();var a=window.location,b=a.href.indexOf("#");if(0<=b){var c=a.href.substring(b+1);/(^|&)q=/.test(c)&&-1==c.indexOf("#")&&a.replace("/search?"+c.replace(/(^|&)fp=[^&]*/g,"")+"&cad=h")};</script><style>[dir='ltr'],[dir='rtl']{unicode-bidi:isolate;unicode-bidi:isolate}bdo[dir='ltr'],bdo[dir='rtl']{unicode-bidi:bidi-override;unicode-bidi:isolate-override;unicode-bidi:isolate-override}#logo{display:block;overflow:hidden;position:relative}#logo img{border:0;}#logo span{background:url(/images/nav_logo242.png) no-repeat;cursor:pointer;overflow:hidden}#logocont{z-index:1;padding-left:13px;padding-right:10px;margin-top:-2px;padding-top:7px}#logocont.ddl{padding-top:3px}.big #logocont{padding-left:13px;padding-right:12px}.sbibod{background-color:#fff;height:44px;vertical-align:top;border-radius:2px;box-shadow:0 2px 2px 0 rgba(0,0,0,0.16),0 0 0 1px rgba(0,0,0,0.08);transition:box-shadow 200ms cubic-bezier(0.4, 0.0, 0.2, 1);}.lst{border:0;margin-top:5px;margin-bottom:0}.lst:focus{outline:none}#lst-ib{color:#000}.gsfi,.lst{font:16px arial,sans-serif;line-height:34px;height:34px !important;}.lst-c{overflow:hidden}#gs_st0{line-height:44px;padding:0 8px;margin-top:-1px;position:static}.srp #gs_st0{padding:0 2px 0 8px}.gsfs{font:16px arial,sans-serif}.lsb{background:transparent;border:0;font-size:0;height:30px;outline:0;text-align:left;width:100%}.sbico{display:inline-block;height:24px;width:24px;cursor:pointer;vertical-align:middle;color:#4285f4}.sbico-c{background:transparent;border:0;float:right;height:44px;line-height:44px;margin-top:-1px;outline:0;padding-right:16px;position:relative;top:-1px}.hp .sbico-c{display:none}#sblsbb{text-align:center;border-bottom-left-radius:0;border-top-left-radius:0;height:44px;margin:0;padding:0;}#sbds{border:0;margin-left:0}.hp .nojsb,.srp .jsb{display:none}.kpbb,.kprb,.kpgb,.kpgrb{border-radius:2px;border-radius:2px;color:#fff}.kpbb:hover,.kprb:hover,.kpgb:hover,.kpgrb:hover{box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);color:#fff}.kpbb:active,.kprb:active,.kpgb:active,.kpgrb:active{box-shadow:inset 0 1px 2px rgba(0,0,0,0.3);box-shadow:inset 0 1px 2px rgba(0,0,0,0.3)}.kpbb{background-color:#4d90fe;background-image:linear-gradient(top,#4d90fe,#4787ed);background-image:linear-gradient(top,#4d90fe,#4787ed);border:1px solid #3079ed}.kpbb:hover{background-color:#357ae8;background-image:linear-gradient(top,#4d90fe,#357ae8);background-image:linear-gradient(top,#4d90fe,#357ae8);border:1px solid #2f5bb7}a.kpbb:link,a.kpbb:visited{color:#fff}.kprb{background-color:#dd4b39;background-image:linear-gradient(top,#dd4b39,#d14836);background-image:linear-gradient(top,#dd4b39,#d14836);border:1px solid #dd4b39}.kprb:hover{background-color:#c53727;background-image:linear-gradient(top,#dd4b39,#c53727);background-image:linear-gradient(top,#dd4b39,#c53727);border:1px solid #b0281a;border-bottom-color:#af301f}.kprb:active{background-color:#b0281a;background-image:linear-gradient(top,#dd4b39,#b0281a);background-image:linear-gradient(top,#dd4b39,#b0281a)}.kpgb{background-color:#3d9400;background-image:linear-gradient(top,#3d9400,#398a00);background-image:linear-gradient(top,#3d9400,#398a00);border:1px solid #29691d}.kpgb:hover{background-color:#368200;background-image:linear-gradient(top,#3d9400,#368200);background-image:linear-gradient(top,#3d9400,#368200);border:1px solid #2d6200}.kpgrb{background-color:#f5f5f5;background-image:linear-gradient(top,#f5f5f5,#f1f1f1);background-image:linear-gradient(top,#f5f5f5,#f1f1f1);border:1px solid #dcdcdc;color:#555}.kpgrb:hover{background-color:#f8f8f8;background-image:linear-gradient(top,#f8f8f8,#f1f1f1);background-image:linear-gradient(top,#f8f8f8,#f1f1f1);border:1px solid #dcdcdc;color:#333}a.kpgrb:link,a.kpgrb:visited{color:#555}#sfopt{display:inline-block;float:right;line-height:normal}.lsd{font-size:11px;position:absolute;top:3px;left:16px}.tsf{background:none}.tsf-p{position:relative;}.logocont{left:0;position:absolute;}.sfibbbc{padding-bottom:2px;padding-top:3px;margin-top:-3px;width:632px}.sbtc{position:relative}.sbibtd{line-height:0;overflow:visible;white-space:nowrap}.sbibps{padding:0 9px 0 16px;padding-top:0 !important;width:586px}.sfopt{height:28px;position:relative}#sform{height:65px}.hp .sfsbc{display:none}#searchform{width:100%}.hp #searchform{position:absolute;top:311px}.srp #searchform{position:absolute;top:20px}#sfdiv{max-width:484px}.hp .big #sfdiv{max-width:568px;}.srp #sfdiv{max-width:none;overflow:hidden}.srp #tsf{position:relative;}.sfsbc{display:inline-block;float:right;margin-right:1px;vertical-align:top;width:42px;margin-right:0}.sfbg{background:#fafafa;height:69px;left:0;position:absolute;width:100%}.sfbgg{background-color:#fafafa;height:65px}#pocs{background:#fff1a8;color:#000;font-size:10pt;margin:0;padding:5px 7px}#pocs.sft{background:transparent;color:#777}#pocs a{color:#11c}#pocs.sft a{color:#36c}#pocs>div{margin:0;padding:0}#cnt{padding-top:20px;}#subform_ctrl{min-height:0px}</style><style id="ostyle">@-moz-keyframes gb__a{0%{opacity:0}50%{opacity:1}}@keyframes gb__a{0%{opacity:0}50%{opacity:1}}.gb_bb{display:none!important}.gb_cb{visibility:hidden}.gb_da .gb_b{background-position:-132px -38px;opacity:.55}.gb_ea .gb_da .gb_b{background-position:-132px -38px}.gb_X .gb_da .gb_b{background-position:-463px -35px;opacity:1}.gb_fa.gb_ga{min-height:196px;overflow-y:auto;width:320px}.gb_ha{-moz-transition:height .2s ease-in-out;transition:height .2s ease-in-out}.gb_ia{background:#fff;margin:0;min-height:100px;padding:28px;padding-right:27px;text-align:left;white-space:normal;width:265px}.gb_ja{background:#f5f5f5;cursor:pointer;height:40px;overflow:hidden}.gb_ka{position:relative}.gb_ja{display:block;line-height:40px;text-align:center;width:320px}.gb_ka{display:block;line-height:40px;text-align:center}.gb_ka.gb_la{line-height:0}.gb_ja,.gb_ja:visited,.gb_ja:active,.gb_ka,.gb_ka:visited{color:rgba(0,0,0,0.87);text-decoration:none}.gb_ka:active{color:rgba(0,0,0,0.87)}#gb a.gb_ja,#gb a.gb_ja:visited,#gb a.gb_ja:active,#gb a.gb_ka,#gb a.gb_ka:visited{color:rgba(0,0,0,0.87);text-decoration:none}#gb a.gb_ka:active{color:rgba(0,0,0,0.87)}.gb_ka,.gb_ia{display:none}.gb_ba,.gb_ba+.gb_ka,.gb_ma .gb_ka,.gb_ma .gb_ia{display:block}.gb_ka:hover,.gb_ka:active,#gb a.gb_ka:hover,#gb a.gb_ka:active{text-decoration:underline}.gb_ka{border-bottom:1px solid #ebebeb;left:28px;width:264px}.gb_ma .gb_ja{display:none}.gb_ka:last-child{border-bottom-width:0}.gb_na .gb_O{display:initial}.gb_na.gb_oa{height:100px;text-align:center}.gb_na.gb_oa img{padding:34px 0;height:32px;width:32px}.gb_na .gb_2{background-image:url('//ssl.gstatic.com/gb/images/p1_c82e4f78.png');background-size:64px 2341px;background-position:0 -828px}.gb_na .gb_2+img{border:0;margin:8px;height:48px;width:48px}.gb_na div.gb_pa{background:#ffa;-moz-border-radius:5px;border-radius:5px;padding:5px;text-align:center}.gb_na.gb_qa,.gb_na.gb_ra{padding-bottom:0}.gb_na.gb_sa,.gb_na.gb_ra{padding-top:0}.gb_na.gb_ra a,.gb_na.gb_sa a{top:0}.gb_ta .gb_ja{margin-top:0;position:static}.gb_ua{display:inline-block}.gb_va{margin:-12px 28px 28px;position:relative;width:264px;-moz-border-radius:2px;border-radius:2px;-moz-box-shadow:0 1px 2px rgba(0,0,0,0.1),0 0 1px rgba(0,0,0,0.1);box-shadow:0 1px 2px rgba(0,0,0,0.1),0 0 1px rgba(0,0,0,0.1)}.gb_4{background-image:url('//ssl.gstatic.com/gb/images/p1_c82e4f78.png');background-size:64px 2341px;display:inline-block;margin:8px;vertical-align:middle;height:64px;width:64px}.gb_wa{color:#262626;display:inline-block;font:13px/18px Arial,sans-serif;margin-right:80px;padding:10px 10px 10px 0;vertical-align:middle;white-space:normal}.gb_xa{font:16px/24px Arial,sans-serif}.gb_ya,#gb#gb .gb_ya{color:#427fed;text-decoration:none}.gb_ya:hover,#gb#gb .gb_ya:hover{text-decoration:underline}.gb_za .gb_ia{position:relative}.gb_za .gb_O{position:absolute;top:28px;left:28px}.gb_ja.gb_Aa{display:none;height:0}.gb_db{background-size:32px 32px;-moz-border-radius:50%;border-radius:50%;display:block;margin:-1px;overflow:hidden;position:relative;height:32px;width:32px;z-index:0}@media (min-resolution:1.25dppx),(-o-min-device-pixel-ratio:5/4),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gb_db::before{display:inline-block;-moz-transform:scale(.5);transform:scale(.5);-moz-transform-origin:left 0;transform-origin:left 0}.gb_Eb::before{display:inline-block;-moz-transform:scale(.5);transform:scale(.5);-moz-transform-origin:left 0;transform-origin:left 0}}.gb_db:hover,.gb_db:focus{-moz-box-shadow:0 1px 0 rgba(0,0,0,.15);box-shadow:0 1px 0 rgba(0,0,0,.15)}.gb_db:active{-moz-box-shadow:inset 0 2px 0 rgba(0,0,0,.15);box-shadow:inset 0 2px 0 rgba(0,0,0,.15)}.gb_db:active::after{background:rgba(0,0,0,.1);-moz-border-radius:50%;border-radius:50%;content:'';display:block;height:100%}.gb_eb{cursor:pointer;line-height:30px;min-width:30px;opacity:.75;overflow:hidden;vertical-align:middle;text-overflow:ellipsis}.gb_b.gb_eb{width:auto}.gb_eb:hover,.gb_eb:focus{opacity:.85}.gb_fb .gb_eb,.gb_fb .gb_gb{line-height:26px}#gb#gb.gb_fb a.gb_eb,.gb_fb .gb_gb{font-size:11px;height:auto}.gb_hb{border-top:4px solid #000;border-left:4px dashed transparent;border-right:4px dashed transparent;display:inline-block;margin-left:6px;opacity:.75;vertical-align:middle}.gb_ib:hover .gb_hb{opacity:.85}.gb_X .gb_eb,.gb_X .gb_hb{opacity:1}#gb#gb.gb_X.gb_X a.gb_eb,#gb#gb .gb_X.gb_X a.gb_eb{color:#fff}.gb_X.gb_X .gb_hb{border-top-color:#fff;opacity:1}.gb_ea .gb_db:hover,.gb_X .gb_db:hover,.gb_ea .gb_db:focus,.gb_X .gb_db:focus{-moz-box-shadow:0 1px 0 rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.2);box-shadow:0 1px 0 rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.2)}.gb_jb .gb_kb,.gb_lb .gb_kb{position:absolute;right:1px}.gb_kb.gb_R,.gb_mb.gb_R,.gb_ib.gb_R{flex:0 1 auto;flex:0 1 main-size}.gb_nb.gb_W .gb_eb{width:30px!important}.gb_ob.gb_cb{display:none}@-moz-keyframes progressmove{0%{margin-left:-100%}to{margin-left:100%}}@keyframes progressmove{0%{margin-left:-100%}to{margin-left:100%}}.gb_pb.gb_bb{display:none}.gb_pb{background-color:#ccc;height:3px;overflow:hidden}.gb_qb{background-color:#f4b400;height:100%;width:50%;-moz-animation:progressmove 1.5s linear 0s infinite;animation:progressmove 1.5s linear 0s infinite}.gb_sb{height:40px;position:absolute;right:-5px;top:-5px;width:40px}.gb_tb .gb_sb,.gb_ub .gb_sb{right:0;top:0}.gb_eb~.gb_vb,.gb_eb~.gb_wb{left:auto;right:6.5px}.gb_xb{outline:none;transform:translateZ(0)}.gb_xb.gb_Za{width:320px}.gb_yb,#gb a.gb_yb.gb_yb,.gb_zb a,#gb .gb_zb.gb_zb a{color:#36c;text-decoration:none}.gb_yb:active,#gb a.gb_yb:active,.gb_yb:hover,#gb a.gb_yb:hover,.gb_zb a:active,#gb .gb_zb a:active,.gb_zb a:hover,#gb .gb_zb a:hover{text-decoration:underline}.gb_Ab{margin:20px;white-space:nowrap}.gb_Bb,.gb_Cb{display:inline-block;vertical-align:top}.gb_xb.gb_Za .gb_Cb{max-width:164px}.gb_Bb{margin-right:20px;position:relative}.gb_Db{-moz-border-radius:50%;border-radius:50%;overflow:hidden}.gb_Eb{background-size:96px 96px;border:none;vertical-align:top;height:96px;width:96px}.gb_ob{background:rgba(78,144,254,.7);bottom:0;color:#fff;font-size:9px;font-weight:bold;left:0;line-height:9px;position:absolute;padding:7px 0;text-align:center;width:96px}.gb_Db .gb_ob{background:rgba(0,0,0,.54)}.gb_Fb{font-weight:bold;margin:-4px 0 1px 0;text-overflow:ellipsis;overflow:hidden}.gb_Ib{color:#666;text-overflow:ellipsis;overflow:hidden}.gb_zb{color:#ccc;margin:6px 0}.gb_xb.gb_Za .gb_zb a{display:block;line-height:24px;margin:0}.gb_xb.gb_Za .gb_zb a:first-child:last-child{line-height:normal}.gb_xb:not(.gb_Za) .gb_zb a{margin:0 10px}.gb_xb:not(.gb_Za) .gb_zb a:first-child{margin-left:0}.gb_xb:not(.gb_Za) .gb_zb a:last-child{margin-right:0}.gb_Cb .gb_Jb{background:#4d90fe;border-color:#3079ed;font-weight:bold;margin:10px 0 0 0;color:#fff}#gb .gb_Cb a.gb_Jb.gb_Jb{color:#fff}.gb_Cb .gb_Jb:hover{background:#357ae8;border-color:#2f5bb7}.gb_Kb.gb_oa{border-top:none}.gb_Kb{background:#f5f5f5;border-top:1px solid #ccc;border-color:rgba(0,0,0,.2);padding:10px 0;width:100%;display:table}.gb_Kb .gb_Jb{margin:0 20px;white-space:nowrap}.gb_Kb>div{display:table-cell;text-align:right}.gb_Kb>div:first-child{text-align:left}.gb_Kb .gb_Lb{display:block;text-align:center}.gb_Mb .gb_vb{border-bottom-color:#fef9db}.gb_Nb{background:#fef9db;font-size:11px;padding:10px 20px;white-space:normal}.gb_Nb b,.gb_yb{white-space:nowrap}.gb_Qb{background:#f5f5f5;border-top:1px solid #ccc;border-top-color:rgba(0,0,0,.2);max-height:230px;overflow:auto}.gb_Qb.gb_Za{max-height:170px}.gb_Qb.gb_Za.gb_Rb{max-height:124px}.gb_Sb{border-top:1px solid #ccc;border-top-color:rgba(0,0,0,.2);display:block;padding:10px 20px;position:relative;white-space:nowrap}.gb_Tb .gb_Sb:focus .gb_Ub{outline:1px dotted #fff}.gb_Sb:hover{background:#eee}.gb_Sb[selected="true"]{overflow:hidden}.gb_Sb[selected="true"]>.gb_Vb{background-color:rgba(117,117,117,.9)}.gb_Sb[selected="true"]>.gb_Wb{display:block;position:absolute;z-index:2}.gb_Wb::-moz-focus-inner{border:0}.gb_Wb{background-color:transparent;border:none;color:#fff;display:none;font-family:Roboto,Arial,sans-serif;font-weight:400;font-size:14px;height:36px;min-width:86px;text-align:center;top:16px;width:auto}.gb_Sb[selected="true"]>.gb_Wb:focus{background-color:rgba(0,0,0,.24);-moz-border-radius:2px;border-radius:2px;outline:0}.gb_Sb[selected="true"]>.gb_Wb:hover,.gb_Sb[selected="true"]>.gb_Wb:focus:hover{background-color:#565656;-moz-border-radius:2px;border-radius:2px}.gb_Sb[selected="true"]>.gb_Wb:active{-moz-border-radius:2px;border-radius:2px;background-color:#212121}.gb_Xb{left:0;margin-left:5%}.gb_Zb{margin-right:5%;right:0}.gb_Sb:first-child,.gb_0b:first-child+.gb_Sb{border-top:0}.gb_0b{display:none}.gb_1b{cursor:default}.gb_1b:hover{background:transparent}.gb_2b{border:none;vertical-align:top;height:48px;width:48px}.gb_Ub{display:inline-block;margin:6px 0 0 10px}.gb_xb.gb_Za .gb_Ub{max-width:222px}.gb_1b .gb_2b,.gb_1b .gb_Ub{opacity:.4}.gb_3b{color:#000;text-overflow:ellipsis;overflow:hidden}.gb_1b .gb_3b{color:#666}.gb_4b{color:#666;text-overflow:ellipsis;overflow:hidden}.gb_5b{color:#666;font-style:italic}.gb_Vb{background-color:transparent;height:100%;left:0;position:absolute;text-align:center;top:0;width:100%;z-index:1}.gb_Wb:hover{background-color:rgba(100,100,100,0.4)}.gb_6b{background:#f5f5f5;border-top:1px solid #ccc;border-top-color:rgba(0,0,0,.2);display:block;padding:10px 20px}.gb_7b{background-position:-244px 0;display:inline-block;margin:1px 0;vertical-align:middle;height:25px;width:25px}.gb_N .gb_7b::before{left:-244px;top:0}.gb_8b{color:#427fed;display:inline-block;padding:0 25px 0 10px;vertical-align:middle;white-space:normal}.gb_6b:hover .gb_8b{text-decoration:underline}.gb_Kb .gb_Jb:hover{-moz-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);border-color:#c6c6c6;color:#222;background-color:#fff;background-image:-moz-linear-gradient(top,#fff,#f8f8f8);background-image:-moz-linear-gradient(top,#fff,#f8f8f8);background-image:linear-gradient(top,#fff,#f8f8f8);filter:progid:DXImageTransform.Microsoft.gradient(startColorStr='#ffffff',EndColorStr='#f8f8f8')}.gb_9c{display:inline-block;padding:0 0 0 15px;vertical-align:middle}.gb_9c:first-child,#gbsfw:first-child+.gb_9c{padding-left:0}.gb_Qc{position:relative}.gb_b{display:inline-block;outline:none;vertical-align:middle;-moz-border-radius:2px;border-radius:2px;-moz-box-sizing:border-box;box-sizing:border-box;height:30px;width:30px;color:#000;cursor:pointer;text-decoration:none}#gb#gb a.gb_b{color:#000;cursor:pointer;text-decoration:none}.gb_vb{border-color:transparent;border-bottom-color:#fff;border-style:dashed dashed solid;border-width:0 8.5px 8.5px;display:none;position:absolute;left:6.5px;top:37px;z-index:1;height:0;width:0;-moz-animation:gb__a .2s;animation:gb__a .2s}.gb_wb{border-color:transparent;border-style:dashed dashed solid;border-width:0 8.5px 8.5px;display:none;position:absolute;left:6.5px;z-index:1;height:0;width:0;-moz-animation:gb__a .2s;animation:gb__a .2s;border-bottom-color:#ccc;border-bottom-color:rgba(0,0,0,.2);top:36px}x:-o-prefocus,div.gb_wb{border-bottom-color:#ccc}.gb_fa{background:#fff;border:1px solid #ccc;border-color:rgba(0,0,0,.2);color:#000;-moz-box-shadow:0 2px 10px rgba(0,0,0,.2);box-shadow:0 2px 10px rgba(0,0,0,.2);display:none;outline:none;overflow:hidden;position:absolute;right:0;top:44px;-moz-animation:gb__a .2s;animation:gb__a .2s;-moz-border-radius:2px;border-radius:2px;-moz-user-select:text}.gb_9c.gb_g .gb_vb,.gb_9c.gb_g .gb_wb,.gb_9c.gb_g .gb_fa,.gb_g.gb_fa{display:block}.gb_9c.gb_g.gb_Bf .gb_vb,.gb_9c.gb_g.gb_Bf .gb_wb{display:none}.gb_Cf{position:absolute;right:0;top:44px;z-index:-1}.gb_fb .gb_vb,.gb_fb .gb_wb,.gb_fb .gb_fa{margin-top:-10px}#gbsfw{min-width:400px;overflow:visible}.gb_9b,#gbsfw.gb_g{display:block;outline:none}#gbsfw.gb_pa iframe{display:none}.gb_ac{padding:118px 0;text-align:center}.gb_bc{background:no-repeat center 0;color:#aaa;font-size:13px;line-height:20px;padding-top:76px;background-image:url('//ssl.gstatic.com/gb/images/a/f5cdd88b65.png')}.gb_bc a{color:#4285f4;text-decoration:none}@-moz-keyframes gb__nb{0%{-moz-transform:scale(0,0);transform:scale(0,0)}20%{-moz-transform:scale(1.4,1.4);transform:scale(1.4,1.4)}50%{-moz-transform:scale(.8,.8);transform:scale(.8,.8)}85%{-moz-transform:scale(1.1,1.1);transform:scale(1.1,1.1)}to{-moz-transform:scale(1.0,1.0);transform:scale(1.0,1.0)}}@keyframes gb__nb{0%{-moz-transform:scale(0,0);transform:scale(0,0)}20%{-moz-transform:scale(1.4,1.4);transform:scale(1.4,1.4)}50%{-moz-transform:scale(.8,.8);transform:scale(.8,.8)}85%{-moz-transform:scale(1.1,1.1);transform:scale(1.1,1.1)}to{-moz-transform:scale(1.0,1.0);transform:scale(1.0,1.0)}}.gb_Hc{background-position:-314px -38px;opacity:.55;height:100%;width:100%}.gb_b:hover .gb_Hc,.gb_b:focus .gb_Hc{opacity:.85}.gb_Ic .gb_Hc{background-position:-463px 0}.gb_Jc{background-color:#cb4437;-moz-border-radius:8px;border-radius:8px;font:bold 11px Arial;color:#fff;line-height:16px;min-width:14px;padding:0 1px;position:absolute;right:0;text-align:center;text-shadow:0 1px 0 rgba(0,0,0,0.1);top:0;visibility:hidden;z-index:990}.gb_Kc .gb_Jc,.gb_Kc .gb_Lc,.gb_Kc .gb_Lc.gb_Mc{visibility:visible}.gb_Lc{padding:0 2px;visibility:hidden}.gb_Nc:not(.gb_Oc) .gb_wb,.gb_Nc:not(.gb_Oc) .gb_vb{left:3px}.gb_Jc.gb_Pc{-moz-animation:gb__nb .6s 1s both ease-in-out;animation:gb__nb .6s 1s both ease-in-out;-moz-perspective-origin:top right;perspective-origin:top right;-moz-transform:scale(1,1);transform:scale(1,1);-moz-transform-origin:top right;transform-origin:top right}.gb_Pc .gb_Lc{visibility:visible}.gb_ea .gb_b .gb_Hc{background-position:0 0;opacity:.7}.gb_ea .gb_Ic .gb_Hc{background-position:-279px -38px}.gb_ea .gb_b:hover .gb_Hc,.gb_ea .gb_b:focus .gb_Hc{opacity:.85}.gb_X .gb_b .gb_Hc{background-position:-349px -38px;opacity:1}.gb_X .gb_Ic .gb_Hc{background-position:-393px 0}.gb_ea .gb_Jc,.gb_X .gb_Jc{border:none}.gb_Nc .gb_Qc{font-size:14px;font-weight:bold;top:0;right:0}.gb_Nc .gb_b{display:inline-block;vertical-align:middle;-moz-box-sizing:border-box;box-sizing:border-box;height:30px;width:30px}.gb_Nc .gb_vb{border-bottom-color:#e5e5e5}.gb_Rc{background-color:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:bold;line-height:20px;margin:5px;padding:0 2px;text-align:center;-moz-box-sizing:border-box;box-sizing:border-box;-moz-border-radius:50%;border-radius:50%;height:20px;width:20px}.gb_Rc.gb_Sc{background-position:-194px -21px}.gb_Rc.gb_Tc{background-position:-194px -46px}.gb_b:hover .gb_Rc,.gb_b:focus .gb_Rc{background-color:rgba(0,0,0,.85)}#gbsfw.gb_Uc{background:#e5e5e5;border-color:#ccc}.gb_ea .gb_Rc{background-color:rgba(0,0,0,.7)}.gb_X .gb_Rc.gb_Rc,.gb_X .gb_Kc .gb_Rc.gb_Rc,.gb_X .gb_Kc .gb_b:hover .gb_Rc,.gb_X .gb_Kc .gb_b:focus .gb_Rc{background-color:#fff;color:#404040}.gb_X .gb_Rc.gb_Sc{background-position:-70px 0}.gb_X .gb_Rc.gb_Tc{background-position:-219px 0}.gb_Kc .gb_Rc.gb_Rc{background-color:#db4437;color:#fff}.gb_Kc .gb_b:hover .gb_Rc,.gb_Kc .gb_b:focus .gb_Rc{background-color:#a52714}#gb#gb a.gb_O,#gb#gb a.gb_P,#gb#gb span.gb_P{color:rgba(0,0,0,0.87);text-decoration:none}#gb#gb a.gb_P:hover,#gb#gb a.gb_P:focus{opacity:.85;text-decoration:underline}.gb_Q.gb_R{display:none;padding-left:15px;vertical-align:middle}.gb_Q.gb_R:first-child{padding-left:0}.gb_S.gb_R{display:inline-block}.gb_Q span{opacity:.55;-moz-user-select:text}.gb_T .gb_S.gb_R{flex:0 1 auto;flex:0 1 main-size;display:-webkit-flex;display:flex}.gb_U .gb_S.gb_R{display:none}.gb_Q .gb_P{display:inline-block;line-height:24px;outline:none;vertical-align:middle}.gb_S .gb_P{display:none}.gb_V .gb_S .gb_P{min-width:0}.gb_W .gb_S .gb_P{width:0!important}#gb#gb.gb_X a.gb_P,#gb#gb.gb_X span.gb_P,#gb#gb .gb_X a.gb_P,#gb#gb .gb_X span.gb_P{color:#fff}#gb#gb.gb_X span.gb_P,#gb#gb .gb_X span.gb_P{opacity:.7}.gb_N .gbqfi::before{left:-428px;top:0}.gb_Tb .gbqfb:focus .gbqfi{outline:1px dotted #fff}a.gb_Ba{border:none;color:#4285f4;cursor:default;font-weight:bold;outline:none;position:relative;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap;-moz-user-select:none}a.gb_Ba:hover:after,a.gb_Ba:focus:after{background-color:rgba(0,0,0,.12);content:'';height:100%;left:0;position:absolute;top:0;width:100%}a.gb_Ba:hover,a.gb_Ba:focus{text-decoration:none}a.gb_Ba:active{background-color:rgba(153,153,153,.4);text-decoration:none}a.gb_Ca{background-color:#4285f4;color:#fff}a.gb_Ca:active{background-color:#0043b2}.gb_Da{-moz-box-shadow:0 1px 1px rgba(0,0,0,.16);box-shadow:0 1px 1px rgba(0,0,0,.16)}.gb_Ba,.gb_Ca,.gb_Ea,.gb_Fa{display:inline-block;line-height:28px;padding:0 12px;-moz-border-radius:2px;border-radius:2px}.gb_Ea{background:#f8f8f8;border:1px solid #c6c6c6}.gb_Fa{background:#f8f8f8}.gb_Ea,#gb a.gb_Ea.gb_Ea,.gb_Fa{color:#666;cursor:default;text-decoration:none}#gb a.gb_Fa.gb_Fa{cursor:default;text-decoration:none}.gb_Fa{border:1px solid #4285f4;font-weight:bold;outline:none;background:#4285f4;background:-moz-linear-gradient(top,#4387fd,#4683ea);background:linear-gradient(top,#4387fd,#4683ea);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4387fd,endColorstr=#4683ea,GradientType=0)}#gb a.gb_Fa.gb_Fa{color:#fff}.gb_Fa:hover{-moz-box-shadow:0 1px 0 rgba(0,0,0,.15);box-shadow:0 1px 0 rgba(0,0,0,.15)}.gb_Fa:active{-moz-box-shadow:inset 0 2px 0 rgba(0,0,0,.15);box-shadow:inset 0 2px 0 rgba(0,0,0,.15);background:#3c78dc;background:-moz-linear-gradient(top,#3c7ae4,#3f76d3);background:linear-gradient(top,#3c7ae4,#3f76d3);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3c7ae4,endColorstr=#3f76d3,GradientType=0)}.gb_cc{min-width:127px;overflow:hidden;position:relative;z-index:987}.gb_dc{position:absolute;padding:0 20px 0 15px}.gb_ec .gb_dc{right:100%;margin-right:-127px}.gb_fc{display:inline-block;line-height:0;outline:none;vertical-align:middle}.gb_gc .gb_fc{position:relative;top:2px}.gb_fc .gb_hc,.gb_0a{display:block}.gb_ic{border:none;display:block;visibility:hidden}.gb_fc .gb_hc{background-position:0 -35px;height:33px;width:92px}img.gb_1a{border:0;vertical-align:middle}.gb_X .gb_fc .gb_hc{background-position:-296px 0}.gb_ea .gb_fc .gb_hc{background-position:-97px 0;opacity:.54}.gb_Df{display:inline-block;line-height:normal;position:relative;z-index:987}.gb_Jf .gb_b{background-position:-498px -35px;opacity:.55;height:30px;width:30px}.gb_Jf .gb_b:hover,.gb_Jf .gb_b:focus{opacity:.85}.gb_Jf .gb_vb{border-bottom-color:#f5f5f5}#gbsfw.gb_Kf{background:#f5f5f5;border-color:#ccc}.gb_X .gb_Jf .gb_b{background-position:-428px -35px;opacity:1}.gb_ea .gb_Jf .gb_b{background-position:-498px 0;opacity:.7}.gb_ea .gb_Jf .gb_b:hover,.gb_ea .gb_Jf .gb_b:focus{opacity:.85}.gb_Fg{color:#000;font:13px/27px Arial,sans-serif;left:0;min-width:1092px;position:absolute;top:0;-moz-user-select:-moz-none;width:100%}.gb_Mf{font:13px/27px Arial,sans-serif;position:relative;height:60px;width:100%}.gb_fb .gb_Mf{height:28px}#gba{height:60px}#gba.gb_fb{height:28px}#gba.gb_Hg{height:90px}#gba.gb_Ig{height:132px}#gba.gb_Hg.gb_fb{height:58px}.gb_Mf>.gb_R{height:60px;line-height:58px;vertical-align:middle}.gb_fb .gb_Mf>.gb_R{height:28px;line-height:26px}.gb_Mf::before{background:#e5e5e5;bottom:0;content:'';display:none;height:1px;left:0;position:absolute;right:0}.gb_Mf{background:#f1f1f1}.gb_Jg .gb_Mf{background:#fff}.gb_Jg .gb_Mf::before,.gb_fb .gb_Mf::before{display:none}.gb_ea .gb_Mf,.gb_X .gb_Mf,.gb_fb .gb_Mf{background:transparent}.gb_ea .gb_Mf::before{background:#e1e1e1;background:rgba(0,0,0,.12)}.gb_X .gb_Mf::before{background:#333;background:rgba(255,255,255,.2)}.gb_R{display:inline-block;flex:0 0 auto;flex:0 0 main-size}.gb_R.gb_Kg{float:right;order:1}.gb_Lg{white-space:nowrap}.gb_T .gb_Lg{display:-webkit-flex;display:flex}.gb_Lg,.gb_R{margin-left:0!important;margin-right:0!important}.gb_hc{background-image:url('//ssl.gstatic.com/gb/images/i1_1967ca6a.png');background-size:528px 68px}@media (min-resolution:1.25dppx),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gb_hc{background-image:url('//ssl.gstatic.com/gb/images/i2_2ec824b0.png')}}.gb_nb{min-width:255px;padding-left:30px;padding-right:30px;position:relative;text-align:right;z-index:986;align-items:center;justify-content:flex-end;-moz-user-select:-moz-none}.gb_fb .gb_nb{min-width:0}.gb_nb.gb_R{flex:1 1 auto;flex:1 1 main-size}.gb_Ec{line-height:normal;position:relative;text-align:left}.gb_Ec.gb_R,.gb_oe.gb_R,.gb_gb.gb_R{flex:0 1 auto;flex:0 1 main-size}.gb_vg,.gb_wg{display:inline-block;padding:0 0 0 15px;position:relative;vertical-align:middle}.gb_oe{line-height:normal;padding-right:15px}.gb_nb .gb_oe.gb_U{padding-right:0}.gb_gb{color:#404040;line-height:30px;min-width:30px;overflow:hidden;vertical-align:middle;text-overflow:ellipsis}#gb.gb_fb.gb_fb .gb_3f,#gb.gb_fb.gb_fb .gb_Ec>.gb_wg .gb_4f{background:none;border:none;color:#36c;cursor:pointer;filter:none;font-size:11px;line-height:26px;padding:0;-moz-box-shadow:none;box-shadow:none}#gb.gb_fb.gb_X .gb_3f,#gb.gb_fb.gb_X .gb_Ec>.gb_wg .gb_4f{color:#fff}.gb_fb .gb_3f{text-transform:uppercase}.gb_nb.gb_V{padding-left:0;padding-right:29px}.gb_nb.gb_xg{max-width:400px}.gb_yg{background-clip:content-box;background-origin:content-box;opacity:.27;padding:22px;height:16px;width:16px}.gb_yg.gb_R{display:none}.gb_yg:hover,.gb_yg:focus{opacity:.55}.gb_zg{background-position:-219px -25px}.gb_Ag{background-position:-194px 0;padding-left:30px;padding-right:14px;position:absolute;right:0;top:0;z-index:990}.gb_jb:not(.gb_lb) .gb_Ag,.gb_V .gb_zg{display:inline-block}.gb_jb .gb_zg{padding-left:30px;padding-right:0;width:0}.gb_jb:not(.gb_lb) .gb_Bg{display:none}.gb_nb.gb_R.gb_V,.gb_V:not(.gb_lb) .gb_Ec{flex:0 0 auto;flex:0 0 main-size}.gb_yg,.gb_V .gb_oe,.gb_lb .gb_Ec{overflow:hidden}.gb_jb .gb_oe{padding-right:0}.gb_V .gb_Ec{padding:1px 1px 1px 0}.gb_jb .gb_Ec{width:75px}.gb_nb.gb_Cg,.gb_nb.gb_Cg .gb_zg,.gb_nb.gb_Cg .gb_zg::before,.gb_nb.gb_Cg .gb_oe,.gb_nb.gb_Cg .gb_Ec{-moz-transition:width .5s ease-in-out,min-width .5s ease-in-out,max-width .5s ease-in-out,padding .5s ease-in-out,left .5s ease-in-out;transition:width .5s ease-in-out,min-width .5s ease-in-out,max-width .5s ease-in-out,padding .5s ease-in-out,left .5s ease-in-out}.gb_T .gb_nb{min-width:0}.gb_nb.gb_W,.gb_nb.gb_W .gb_Ec,.gb_nb.gb_Dg,.gb_nb.gb_Dg .gb_Ec{min-width:0!important}.gb_nb.gb_W,.gb_nb.gb_W .gb_R{-moz-box-flex:0 0 auto!important;flex:0 0 auto!important}.gb_nb.gb_W .gb_gb{width:30px!important}.gb_Eg{margin-right:32px}.gb_cb{display:none}.gb_Mf ::-webkit-scrollbar{height:15px;width:15px}.gb_Mf ::-webkit-scrollbar-button{height:0;width:0}.gb_Mf ::-webkit-scrollbar-thumb{background-clip:padding-box;background-color:rgba(0,0,0,.3);border:5px solid transparent;-moz-border-radius:10px;border-radius:10px;min-height:20px;min-width:20px;height:5px;width:5px}.gb_Mf ::-webkit-scrollbar-thumb:hover,.gb_Mf ::-webkit-scrollbar-thumb:active{background-color:rgba(0,0,0,.4)}#gb.gb_Og{min-width:980px}#gb.gb_Og .gb_If{min-width:0;position:static;width:0}.gb_Xc{display:none}.gb_Og .gb_Mf{background:transparent;border-bottom-color:transparent}.gb_Og .gb_Mf::before{display:none}.gb_Og.gb_Og .gb_Q{display:inline-block}.gb_Og.gb_nb .gb_oe.gb_U{padding-right:15px}.gb_T.gb_Og .gb_S.gb_R{display:-webkit-flex;display:flex}.gb_Og.gb_T #gbqf{display:block}.gb_Og #gbq{height:0;position:absolute}.gb_Og.gb_nb{z-index:987}sentinel{}#gbq .gbgt-hvr,#gbq .gbgt:focus{background-color:transparent;background-image:none}.gbqfh#gbq1{display:none}.gbxx{display:none !important}#gbq{line-height:normal;position:relative;top:0px;white-space:nowrap}#gbq{left:0;width:100%}#gbq2{top:0px;z-index:986}#gbq4{display:inline-block;max-height:29px;overflow:hidden;position:relative}.gbqfh#gbq2{z-index:985}.gbqfh#gbq2{margin:0;margin-left:0 !important;padding-top:0;position:relative;top:310px}.gbqfh #gbqf{margin:auto;min-width:534px;padding:0 !important}.gbqfh #gbqfbw{display:none}.gbqfh #gbqfbwa{display:block}.gbqfh #gbqf{max-width:572px;min-width:572px}.gbqfh .gbqfqw{border-right-width:1px}
.gbii::before{content:url(https://ssl.gstatic.com/gb/images/silhouette_27.png)}.gbip::before{content:url(https://ssl.gstatic.com/gb/images/silhouette_96.png)}@media (min-resolution:1.25dppx),(-o-min-device-pixel-ratio:5/4),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gbii::before{content:url(https://ssl.gstatic.com/gb/images/silhouette_27.png)}.gbip::before{content:url(https://ssl.gstatic.com/gb/images/silhouette_96.png)}}
.gbii{background-image:url(https://ssl.gstatic.com/gb/images/silhouette_27.png)}.gbip{background-image:url(https://ssl.gstatic.com/gb/images/silhouette_96.png)}@media (min-resolution:1.25dppx),(-o-min-device-pixel-ratio:5/4),(-webkit-min-device-pixel-ratio:1.25),(min-device-pixel-ratio:1.25){.gbii{background-image:url(https://ssl.gstatic.com/gb/images/silhouette_27.png)}.gbip{background-image:url(https://ssl.gstatic.com/gb/images/silhouette_96.png)}}
</style><style id="gstyle">body{color:#000;margin:0}body{background:#fff}a.gb1,a.gb2,a.gb3,.link{color:#1a0dab !important}.ts{border-collapse:collapse}.ts td{padding:0}.g{line-height:1.2;text-align:left}.ti,.bl{display:inline}.ti{display:inline-table}#rhs_block{padding-bottom:15px}a:link,.w,#prs a:visited,#prs a:active,.q:active,.q:visited,.kl:active,.tbotu{color:#1a0dab}.mblink:visited,a:visited{color:#609}.cur,.b{font-weight:bold}.j{width:42em;font-size:82%}.s{max-width:48em}.sl{font-size:82%}.hd{position:absolute;width:1px;height:1px;top:-1000em;overflow:hidden}.f,.f a:link,.m{color:#666}.c h2{color:#666}.mslg cite{display:none}.ng{color:#dd4b39}h1,ol,ul,li{margin:0;padding:0}.g,body,html,input,.std,h1{font-size:small;font-family:arial,sans-serif}.c h2,h1{font-weight:normal}.blk a{color:#000}#nav a{display:block}#nav .i{color:#a90a08;font-weight:bold}.csb,.ss,.micon,.close_btn,.mbi{background:url(/images/nav_logo242.png) no-repeat;overflow:hidden}.csb,.ss{background-position:0 0;height:40px;display:block}.mbi{background-position:-153px -70px;display:inline-block;float:left;height:13px;margin-right:3px;margin-top:4px;width:13px}#nav td{padding:0;text-align:center}.ch{cursor:pointer}h3,.med{font-size:medium;font-weight:normal;margin:0;padding:0}#res h3{font-size:18px}.e{margin:2px 0 .75em}.slk div{padding-left:12px;text-indent:-10px}.blk{border-top:1px solid #6b90da;background:#f0f7f9}#cnt{clear:both}#res{padding-right:1em;margin:0 16px}.xsm{font-size:x-small}ol li{list-style:none}.sm li{margin:0}.gl,#foot a,.nobr{white-space:nowrap}#foot #navcnt a{color:#4285f4;font-weight:normal}#foot #navcnt .cur{color:rgba(0,0,0,0.87);font-weight:normal}.sl,.r{display:inline;font-weight:normal;margin:0}.r{font-size:medium}h4.r{font-size:small}.vshid{display:none}.gic{position:relative;overflow:hidden;z-index:0}.nwd{font-size:10px;padding:0 16px 30px 16px;text-align:center}#rhs{display:block;margin-left:712px;padding-bottom:10px;min-width:268px}#nyc{bottom:0;display:none;left:0;margin-left:663px;min-width:317px;overflow:hidden;position:fixed;right:0;visibility:visible}.mdm #nyc{margin-left:683px}.mdm #rhs{margin-left:732px}.big #nyc{margin-left:743px}.big #rhs{margin-left:792px;}body .big #subform_ctrl{margin-left:229px}.rhslink{width:68px}.rhsdw .rhslink{width:156px}.rhsimg{width:70px}.rhsimg.rhsdw{width:158px}.rhsimg.rhsn1st{margin-left:16px}#rhs .scrt.rhsvw,#rhs table.rhsvw{border:0}#nyc .rhsvw{border:0;padding-left:0;padding-right:0}#rhs .rhsvw{border:1px solid #ebebeb;padding-left:15px;padding-right:15px;position:relative;width:424px;}#nyc .rhsvw{width:424px}#center_col .rhsl4,#center_col .rhsl5,#center_col .rhsn5{display:none}#rhs .rhstc4 .rhsvw,#nyc.rhstc4 .rhsvw{width:336px}#rhs .rhstc3 .rhsvw,#nyc.rhstc3 .rhsvw{width:248px}.rhstc4 .rhsg4,.rhstc3 .rhsg4,.rhstc3 .rhsg3{background:none !important;display:none !important}.rhstc5 .rhsl5,.rhstc5 .rhsl4,.rhstc4 .rhsl4{background:none !important;display:none !important}.rhstc4 .rhsn4{background:none !important;display:none !important}.nrgt{margin-left:22px}.mslg .vsc{border:1px solid transparent;border-radius:2px;border-radius:2px;transition:opacity .2s ease;margin-top:2px;padding:3px 0 3px 5px;transition:opacity .2s ease;width:294px}.mslg>td{padding-right:6px;padding-top:4px}button.vspib{display:none}div.vspib{background:transparent;bottom:0;cursor:default;height:auto;margin:0;min-height:40px;padding-left:9px;padding-right:4px;position:absolute;right:-37px;top:-2px;width:28px;z-index:3}.nyc_open div.vspib{z-index:103}div.vspib:focus{outline:none}.vspii .vspiic{background:url(/images/nav_logo242.png);background-position:-3px -260px;width:15px;height:13px;margin-left:6px;margin-top:-7px;opacity:.3;position:absolute;top:50%;visibility:hidden}.vsh .vsc:hover .vspii .vspiic{visibility:visible}.vsh .vspib .vspii:hover .vspiic{opacity:1;visibility:visible;transition:opacity .25s ease}.vsh .vsdth .vspiic{opacity:1;visibility:visible;transition:opacity 1.5s ease}.nyc_open.vsh .vsdth .vspiic,.nyc_open.vsh .vspib .vspii:hover .vspiic{transition:0}.vspib:focus .vspiic{opacity:1;visibility:visible}.vsh .vspib:focus .vspiic{opacity:.3;visibility:hidden}.vso .vspiic,.vso .vspib:focus .vspiic{opacity:1;visibility:visible}.vspii{border:1px solid transparent;border-radius:2px;border-right:none;cursor:default;user-select:none;user-select:none}.vsh.nyc_opening .vsc:hover .vspii,.vsh.nyc_open .vsc:hover .vspii,.vso .vspii{background-color:#fafafa;border-color:#e6e6e6;height:100%}.vso .vspib{padding-right:0}.vsh.nyc_open .mslg .vsc:hover,.vsh.nyc_opening .mslg .vsc:hover{border-right-color:#ebebeb}.nyc_open #nycx{background:url(/images/nav_logo242.png) no-repeat;background-position:-140px -230px;height:11px;width:11px}.vsc{display:inline-block;position:relative;width:100%}#res h3.r{display:block;overflow:hidden;text-overflow:ellipsis;text-overflow:ellipsis;white-space:nowrap}#res h3.inl{display:inline;white-space:normal}em{font-weight:bold;font-style:normal}ol,ul,li{border:0;margin:0;padding:0}.g{margin-top:0;margin-bottom:26px}.ibk{display:-moz-inline-box;display:inline-block;vertical-align:top}.tsw{width:595px}#cnt{min-width:833px;margin-left:0}.mw{max-width:1197px}.big .mw{max-width:1280px}#cnt #center_col,#cnt #foot{width:632px}.gbh{top:24px}#gbar{margin-left:8px;height:20px}#guser{margin-right:8px;padding-bottom:5px !important}.tsf-p{padding-left:150px;margin-right:46px;max-width:739px}.big .tsf-p{margin-right:322px;padding-left:150px}.mbi{margin-bottom:-1px}.uc{padding-left:8px;position:relative;margin-left:128px;}.ucm{padding-bottom:5px;padding-top:5px;margin-bottom:8px}.col{float:left}#leftnavc,#center_col,#rhs{position:relative}#center_col{margin-left:138px;margin-right:264px;}.mdm #center_col{margin-left:138px;}.big #center_col{margin-left:138px;}#subform_ctrl{font-size:11px;margin-right:480px;position:relative;z-index:99}#subform_ctrl a.gl{color:#1a0dab}#center_col{clear:both}#res{border:0;margin:0;padding:0 16px;}#extrares{padding:0 16px}#ires{margin-top:6px}.micon,.close_btn{border:0}.close_btn{background-position:-138px -84px;float:right;height:14px;width:14px}.mitem{border-bottom:1px solid transparent;line-height:29px;opacity:1.0;}.mitem .kl{padding-left:16px}.mitem .kl:hover,.msel .kls:hover{color:#222;display:block}.mitem>.kl{color:#222;display:block}.msel{color:#dd4b39;cursor:pointer}.msel .kls{border-left:5px solid #dd4b39;padding-left:11px}.mitem>.kl,.msel{font-size:13px}#tbd{display:block;min-height:1px}.tbt{font-size:13px;line-height:1.2}.tbnow{white-space:nowrap}.tbos,.tbots{font-weight:bold}.tbst{margin-top:8px}#iszlt_sel.tbcontrol_vis{margin-left:0}.tbpc,.tbpo{font-size:13px}.tbpc,.tbo .tbpo{display:inline}.tbo .tbpc,.tbpo,#set_location_section{display:none}.lco #set_location_section{display:block}#cdr_opt{padding-left:8px;text-indent:0}.tbou #cdr_frm{display:none}#cdr_frm,#cdr_min,#cdr_max{color:rgb(102,102,102)}#cdr_min,#cdr_max{font-family:arial,sans-serif;width:100%}#cdr_opt label{display:block;font-weight:normal;margin-right:2px;white-space:nowrap}a:link,.w,.q:active,.q:visited{cursor:pointer}.osl a,.gl a,#tsf a,a.mblink,a.gl,a.fl,.slk a,.bc a,.flt,a.flt u,.blg a,#appbar a{text-decoration:none}.osl a:hover,.gl a:hover,#tsf a:hover,a.mblink:hover,a.gl:hover,a.fl:hover,.slk a:hover,.bc a:hover,.flt:hover,a.flt:hover u,.tbotu:hover,.blg a:hover{text-decoration:underline}#tads a,#tadsb a,#res a,#rhs a,#taw a{text-decoration:none}#brs a,.nsa,.tbt a,.tbotu:hover,#tbpi,#nycntg a:hover,.fl,.navend span,#botstuff a,.flt:hover u,.mlocsel span,#rhs .gl a,#nav a.pn{text-decoration:none}#ss-box a:hover{text-decoration:none}.osl{color:#777}#gbi,#gbg{border-color:#a2bff0 #558be3 #558be3 #a2bff0}#gbi a.gb2:hover,#gbg a.gb2:hover,.mi:hover{background-color:#558be3}#guser a.gb2:hover,.mi:hover,.mi:hover *{color:#fff !important}#guser{color:#000}#imagebox_bigimages .th{border:0}.vsc:hover .lupin,.intrlu:hover .lupin,.lupin.luhovm,#ires:hover .vsc:hover .lupin.luhovm{background-image:url(/images/mappins_red.png) !important;}#ires:hover .lupin.luhovm{background-image:url(/images/mappins_grey.png) !important;}.vsc:hover .lucir,.intrlu:hover .lucir,.lucir.luhovm,#ires:hover .vsc:hover .lucir.luhovm{background-image:url(/images/mapcircles_red.png) !important;}#ires:hover .lucir.luhovm{background-image:url(/images/mapcircles_grey.png) !important;}#foot .ftl{margin-right:12px}#foot{visibility:hidden}#fll a,#bfl a{color:#1a0dab;margin:0 12px;text-decoration:none}.stp{margin:7px 0 17px}body{color:#222}.s{color:#545454}.s .st em,.st.s.std em{color:#6a6a6a}.s a:visited em{color:#609}.s a:active em{color:#dd4b39}.sfcc{width:833px;}#tsf{width:833px;}.big .sfcc{max-width:1129px}.big #tsf{width:1109px}#topstuff .obp{padding-top:6px}.slk{margin-top:6px !important}.st{line-height:1.4;word-wrap:break-word}.kt{border-spacing:2px 0;margin-top:1px}.esw{vertical-align:text-bottom;}.cpbb,.kpbb,.kprb,.kpgb,.kpgrb,.ksb{-moz-border-radius:2px;border-radius:2px;cursor:default;font-family:arial,sans-serif;font-size:11px;font-weight:bold;height:27px;line-height:27px;margin:2px 0;min-width:54px;padding:0 8px;text-align:center;-moz-transition:all 0.218s;transition:all 0.218s,visibility 0s;-moz-user-select:none;}.ab_button{-moz-border-radius:2px;border-radius:2px;cursor:default;font-family:arial,sans-serif;font-size:11px;font-weight:bold;height:27px;line-height:27px;margin:2px 0;min-width:54px;padding:0 8px;text-align:center;-moz-transition:all 0.218s;transition:all 0.218s,visibility 0s;-moz-user-select:none;}#top_nav .ab_button{background:none;border:none;font:inherit;height:auto;margin:0;min-width:0;padding:0;width:auto}#top_nav .ab_button:hover{-moz-box-shadow:none;box-shadow:none;-moz-transition:none;transition:none}.ab_button.left{-moz-border-radius:2px 0 0 2px;border-radius:2px 0 0 2px;border-right-color:transparent;margin-right:0}.ab_button.right{-moz-border-radius:0 2px 2px 0;border-radius:0 2px 2px 0;margin-left:-1px}.kbtn-small{min-width:26px;width:26px}.ksb{background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#f5f5f5,#f1f1f1);background-image:linear-gradient(top,#f5f5f5,#f1f1f1);border:1px solid #dcdcdc;border:1px solid rgba(0,0,0,0.1);color:#444;}.ab_button{background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#f5f5f5,#f1f1f1);background-image:linear-gradient(top,#f5f5f5,#f1f1f1);border:1px solid #dcdcdc;border:1px solid rgba(0,0,0,0.1);color:#444;}a.ksb,.div.ksb{color:#444;text-decoration:none;cursor:default}a.ab_button{color:#444;text-decoration:none;cursor:default}.cpbb:hover,.kpbb:hover,.kprb:hover,.kpgb:hover,.kpgrb:hover,.ksb:hover{-moz-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);-moz-transition:all 0.0s;transition:all 0.0s}.ab_button:hover{-moz-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);-moz-transition:all 0.0s;transition:all 0.0s}#hdtb-tls:hover{-moz-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);-moz-transition:all 0.0s;transition:all 0.0s}.ksb:hover{background-color:#f8f8f8;background-image:-moz-linear-gradient(top,#f8f8f8,#f1f1f1);background-image:linear-gradient(top,#f8f8f8,#f1f1f1);border:1px solid #c6c6c6;color:#222;}.ab_button:hover{background-color:#f8f8f8;background-image:-moz-linear-gradient(top,#f8f8f8,#f1f1f1);background-image:linear-gradient(top,#f8f8f8,#f1f1f1);border:1px solid #c6c6c6;color:#222;}#hdtb-tls:hover{background-color:#f8f8f8;background-image:-moz-linear-gradient(top,#f8f8f8,#f1f1f1);background-image:linear-gradient(top,#f8f8f8,#f1f1f1);border:1px solid #c6c6c6;color:#222;}.ksb:active{background-color:#f6f6f6;background-image:-moz-linear-gradient(top,#f6f6f6,#f1f1f1);background-image:linear-gradient(top,#f6f6f6,#f1f1f1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);}#hdtb-tls:active{background-color:#f6f6f6;background-image:-moz-linear-gradient(top,#f6f6f6,#f1f1f1);background-image:linear-gradient(top,#f6f6f6,#f1f1f1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);}.ksb.ksbs,.ksb.ksbs:hover{background-color:#eee;background-image:-moz-linear-gradient(top,#eee,#e0e0e0);background-image:linear-gradient(top,#eee,#e0e0e0);border:1px solid #ccc;-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);color:#222;margin:0}.ksb.sbm{height:20px;line-height:18px;min-width:35px}.ksb.sbf{height:21px;line-height:21px;min-width:35px}.ksb.mini{-moz-box-sizing:content-box;box-sizing:content-box;height:17px;line-height:17px;min-width:0}.ksb.left{-webkit-border-radius:2px 0 0 2px}.ksb.mid{-webkit-border-radius:0;margin-left:-1px}.ksb.right{-webkit-border-radius:0 2px 2px 0;margin-left:-1px}.ktf{-moz-border-radius:1px;-moz-box-sizing:content-box;background-color:#fff;border:1px solid #d9d9d9;border-top:1px solid #c0c0c0;box-sizing:content-box;color:#333;display:inline-block;height:29px;line-height:27px;padding-left:8px;vertical-align:top}.ktf:hover{-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);border:1px solid #b9b9b9;border-top:1px solid #a0a0a0;box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.ktf:focus{-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.3);border:1px solid #4d90fe;box-shadow:inset 0 1px 2px rgba(0,0,0,0.3);outline:none}.ktf.mini{font-size:11px;height:17px;line-height:17px;padding:0 2px}.sbc,.sbm.sbc,.sbf.sbc{padding:0 2px;min-width:30px}#sbfrm_l{visibility:inherit !important}#rcnt{margin-top:3px;}#appbar,#rhscol{min-width:1100px}#top_nav{min-width:1100px}#appbar{background:white;-webkit-box-sizing:border-box;width:100%}.ab_wrp{height:57px;border-bottom:1px solid #ebebeb}#main{width:100%}#ab_name,#ab_shopping{color:#dd4b39;font:20px "Arial";margin-left:15px;position:absolute;top:17px}#ab_name a{color:#999}#ab_shopping a{color:#dd4b39}#ab_ctls{float:right;position:relative;right:29px;z-index:3;line-height:1;padding-top:28px}#sslock{background:url(images/srpr/safesearchlock_transparent.png) right top no-repeat;height:40px;position:absolute;right:0;top:0;width:260px;-moz-user-select:none;}.ab_ctl{display:inline-block;position:relative;margin-left:16px;}.ab_ctl.action-menu{margin-top:1px;vertical-align:middle}#hdtbSum .ab_ctl{vertical-align:baseline}a.ab_button,a.ab_button:visited{display:inline-block;color:#444;margin-top:1px}a.ab_button:hover{color:#222}#appbar a.ab_button:active,a.ab_button.selected,a.ab_button.selected:hover{color:#333}.ab_button:focus{outline:none}.ab_button.selected:focus{border-color:#ccc}.ab_button:hover>span.ab_icon{opacity:0.9}#ab_opt_icon{background-position:-42px -259px;margin-top:-2px;border-radius:50%;display:inline-block;padding:4px;vertical-align:middle}#ab_opt_icon:hover{color:#777}.selected #ab_opt_icon,#ab_opt_icon:active{background:rgba(0,0,0,0.1)}.ab_icon{background:url(/images/nav_logo242.png) no-repeat;display:inline-block;opacity:0.667;vertical-align:middle}.ab_dropdown{background:#fff;border:1px solid #dcdcdc;border:1px solid rgba(0,0,0,0.2);font-size:13px;padding:6px 0;position:absolute;right:0;top:32px;white-space:nowrap;z-index:3;-moz-transition:opacity 0.218s;transition:opacity 0.218s;-moz-box-shadow:0 2px 4px rgba(0,0,0,0.2);box-shadow:0 2px 4px rgba(0,0,0,0.2)}.ab_dropdown:focus,.ab_dropdownitem:focus,.ab_dropdownitem a:focus{outline:none}.ab_dropdownitem{margin:0;padding:0;-moz-user-select:none;}.ab_dropdownitem.selected{background-color:#eee}.ab_dropdownitem.checked{background-image:url(//ssl.gstatic.com/ui/v1/menu/checkmark.png);background-position:left center;background-repeat:no-repeat}.ab_dropdownitem.disabled{cursor:default;border:1px solid #f3f3f3;border:1px solid rgba(0,0,0,0.05);pointer-events:none}a.ab_dropdownitem.disabled{color:#b8b8b8}.ab_dropdownitem.active{-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.ab_arrow{background:url(//ssl.gstatic.com/ui/v1/zippy/arrow_down.png);background-position:right center;background-repeat:no-repeat;display:inline-block;height:4px;margin-left:3px;margin-top:-3px;vertical-align:middle;width:7px}.ab_dropdownlnk,.ab_dropdownlnkinfo{display:block;padding:8px 20px 8px 16px}a.ab_dropdownlnk,a.ab_dropdownlnk:visited,a.ab_dropdownlnk:hover,#appbar a.ab_dropdownlnk:active{color:#333}a.ab_dropdownlnkinfo,a.ab_dropdownlnkinfo:visited,a.ab_dropdownlnkinfo:hover,#appbar a.ab_dropdownlnkinfo:active{color:#15c}.ab_dropdownchecklist{padding-left:30px}.ab_dropdownrule{border-top:1px solid #ebebeb;margin-bottom:10px;margin-top:9px}#top_nav{-moz-user-select:none;}.ksb.mini{margin-top:0px;}.ab_tnav_wrp{height:33px}#hdtb-msb>.hdtb-mitem:first-child,.ab_tnav_wrp,#cnt #center_col,.mw #center_col{margin-left:150px}.mw #rhs{margin-left:820px}.mw #nyc{margin-left:651px}.klnav.klleft{margin-left:14px !important}.tbt{margin-left:8px;margin-bottom:28px}#tbpi.pt.pi{margin-top:-20px}#tbpi.pi{margin-top:0}.tbo #tbpi.pt,.tbo #tbpi{margin-top:-20px}#tbpi.pt{margin-top:8px}#tbpi{margin-top:0}#tbrt{margin-top:-20px}.lnsep{border-bottom:1px solid #ebebeb;margin-bottom:14px;margin-left:10px;margin-right:4px;margin-top:14px}.tbos,.tbots,.tbotu{color:#dd4b39}.tbou>a.q,#tbpi,#tbtro,.tbt label,#set_location_section a{color:#222}.th{border:1px solid #ebebeb}#resultStats{line-height:33px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#resultStats{padding-left:16px;padding-top:0;padding-bottom:0;padding-right:8px}#subform_ctrl{margin-left:149px}.big #subform_ctrl{padding-right:2px;margin-left:229px}.mdm .mitem .kl{padding-left:28px}.big .mitem .kl{padding-left:44px}.mdm .msel .kls{padding-left:23px}.big .msel .kls{padding-left:39px}.obsmo .dp0,.dp1{display:none}.obsmo .dp1{display:inline}#obsmtc a,.rscontainer a{text-decoration:none}#obsmtc a:hover .ul,.rscontainer a:hover .ul{text-decoration:underline}.authorship_attr{white-space:nowrap}.currency input[type=text]{background-color:white;border:1px solid #d9d9d9;border-top:1px solid #c0c0c0;box-sizing:border-box;color:#333;display:inline-block;height:29px;line-height:27px;padding-left:8px;vertical-align:top;}.currency input[type=text]:hover{border:1px solid #b9b9b9;border-top:1px solid #a0a0a0;box-shadow:inset 0px 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0px 1px 2px rgba(0,0,0,0.1);}.currency input[type=text]:focus{border:1px solid #4d90fe;box-shadow:inset 0px 1px 2px rgba(0,0,0,0.3);outline:none;-moz-box-shadow:inset 0px 1px 2px rgba(0,0,0,0.3);}body.vasq #hdtbSum{height:58px}body.vasq #hdtb-msb .hdtb-mitem.hdtb-msel,body.vasq #hdtb-msb .hdtb-mitem.hdtb-msel-pre{height:15px;line-height:15px;padding:28px 16px 12px}#hdtb-msb .hdtb-mitem.hdtb-imb{height:15px;line-height:15px;padding-top:28px}body.vasq .ab_tnav_wrp{height:43px}body.vasq #topabar.vasqHeight{margin-top:-44px !important}body.vasq #resultStats{line-height:43px}body.vasq .hdtb-mn-o,body.vasq .hdtb-mn-c{top:38px}.ellip{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.QHTnWc{color:#006621;white-space:nowrap}hr{border:0;border-bottom:1px solid #ebebeb;margin:0;}.c6EXBf,.mKoLob{margin-left:16px}.c6EXBf,.Lj3pJf{margin-right:16px}.KH4S3b{margin:0 -16px}.pIpgAc{padding-top:1px;margin-bottom:-1px}.KKgUze{font-size:18px;line-height:20px;padding-top:1px;margin-bottom:-1px;}.hOOcvb{color:rgba(0,0,0,.54)}.OAX6kd{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}a.fdYsqf{color:#609}.ZINbbc{background-color:#fff;border-radius:2px;margin-bottom:26px;font-size:14px;line-height:20px;box-shadow:0 2px 2px 0 rgba(0,0,0,0.16),0 0 0 1px rgba(0,0,0,0.08);margin-left:-16px;margin-right:-16px;font-family:'Roboto',arial,sans-serif}#rhs .ZINbbc{margin:6px -32px 26px 2px}#rhs .rhstc5 .ZINbbc{width:454px}#rhs .rhstc4 .ZINbbc{width:366px}#rhs .rhstc3 .ZINbbc{width:278px}.ZINbbc>*:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.ZINbbc>*:last-child{border-bottom-left-radius:2px;border-bottom-right-radius:2px}.hs54Nc,.JTuIPc{padding-top:16px;padding-bottom:16px}.zjbNbe,.JTuIPc{padding-left:16px;padding-right:16px;}</style><script nonce="KYI43wH9N6dmn39dV/OrTw==">(function(){var b={gen204:"dcl",clearcut:4};var c=[function(){google.c&&google.tick("load",b)}];google.dclc=function(a){c.length?c.push(a):a()};function d(){for(var a;a=c.shift();)a()}window.addEventListener?(document.addEventListener("DOMContentLoaded",d,!1),window.addEventListener("load",d,!1)):window.attachEvent&&window.attachEvent("onload",d);}).call(this);(function(){window.rwt=function(){return!0};}).call(this);(window['gbar']=window['gbar']||{})._CONFIG=[[[0,"www.gstatic.com","og.og2.en_US.yvx_Tot7HP4.O","co.uk","en","1",1,[3,2,".40.","","1300102,3700062,3700288,3700440,3700521","189073909","0"],"40400","mvm4WrSkN4vfU4rYucAF",0,0,"og.og2.-1cecz0khs5rfr.L.F4.O","AA2YrTulH-59-jRxgdSXRcZIBFga0YdnfA","AA2YrTthq0yXcLxg2xHdFgHuTSbV26gN0A","",2,0,200,"GBR",null,null,"1","1",0],null,0,["m;/_/scs/abc-static/_/js/k=gapi.gapi.en.qJZuTTuHcJE.O/m=__features__/am=AAE/rt=j/d=1/rs=AHpOoo8glyl83aQK4S9K5v-KraVNbM7RrQ","https://apis.google.com","","","","",null,1,"es_plusone_gc_20180314.0_p0","en"],["1","gci_91f30755d6a6b787dcc2a4062e6e9824.js","googleapis.client:plusone:gapi.iframes","","en"],null,null,null,[0.009999999776482582,"co.uk","1",[null,"","w",null,1,5184000,1,0,"",0,1,"",0,0,null,0],null,[["","","0",0,0,-1]],null,0,null,null,["5061451","google\\.(com|ru|ca|by|kz|com\\.mx|com\\.tr)$",1]],null,[0,0,0,null,"","","",""],[1,0.001000000047497451,1],[1,0.1000000014901161,2,1],[0,"",null,"",0,"There was an error loading your Marketplace apps.","You have no Marketplace apps.",0,[1,"https://www.google.co.uk/webhp?tab=ww","Search","","0 -483px",null,0],null,null,null,0,null,null,0],[1],[0,1,["lg"],1,["lat"]],[["","","","","","","","","","","","","","","","","","","","def","","","","","","","","",""],[""]],null,null,null,[30,127,1,0,60],null,null,null,null,null,[1,1],null,[1,0.1000000014901161,0,40400,1,"GBR","en","189073909.0",1,0.001000000047497451],null,[""]]];(window['gbar']=window['gbar']||{})._LDD=["in","fot"];/* _GlobalPrefix_ */
this.gbar_=this.gbar_||{};(function(_){var window=this;
/* _Module_:_r */
try{
var ba,ha,ia,ja,qa,ra;_.aa="function"==typeof Object.create?Object.c
gitextract_vwfvhtj1/
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── build/
│ └── .gitignore
├── composer.json
├── phpcs.xml
├── phpunit.dist.xml
├── src/
│ ├── AdwordsResultItem.php
│ ├── AdwordsResultType.php
│ ├── AdwordsSectionResultSet.php
│ ├── Exception/
│ │ ├── GoogleCaptchaException.php
│ │ └── InvalidDOMException.php
│ ├── GoogleClient.php
│ ├── GoogleUrl.php
│ ├── GoogleUrlArchive.php
│ ├── GoogleUrlInterface.php
│ ├── GoogleUrlTrait.php
│ ├── NaturalResultType.php
│ ├── Page/
│ │ ├── GoogleCaptcha.php
│ │ ├── GoogleDom.php
│ │ ├── GoogleError.php
│ │ ├── GoogleSerp.php
│ │ └── NotFound.php
│ └── Parser/
│ ├── AbstractAdwordsParser.php
│ ├── AbstractParser.php
│ ├── Evaluated/
│ │ ├── AdwordsParser.php
│ │ ├── AdwordsSectionParser.php
│ │ ├── MobileAdwordsParser.php
│ │ ├── MobileAdwordsSectionParser.php
│ │ ├── MobileNaturalParser.php
│ │ ├── NaturalParser.php
│ │ └── Rule/
│ │ ├── Adwords/
│ │ │ ├── AdwordsItem.php
│ │ │ ├── AdwordsItemMobile.php
│ │ │ └── Shopping.php
│ │ └── Natural/
│ │ ├── AnswerBox.php
│ │ ├── Classical/
│ │ │ ├── ClassicalCardsResult.php
│ │ │ ├── ClassicalCardsResultO9g5cc.php
│ │ │ ├── ClassicalCardsResultZ1m.php
│ │ │ ├── ClassicalCardsResultZINbbc.php
│ │ │ ├── ClassicalCardsVideoResult.php
│ │ │ ├── ClassicalResult.php
│ │ │ └── ClassicalWithLargeVideo.php
│ │ ├── ComposedTopStories.php
│ │ ├── Divider.php
│ │ ├── Flight.php
│ │ ├── ImageGroup.php
│ │ ├── ImageGroupCarousel.php
│ │ ├── InTheNews.php
│ │ ├── KnowledgeCard.php
│ │ ├── Map.php
│ │ ├── MapLegacy.php
│ │ ├── MapMobile.php
│ │ ├── PeopleAlsoAsk.php
│ │ ├── SearchResultGroup.php
│ │ ├── TopStoriesCarousel.php
│ │ ├── TopStoriesVertical.php
│ │ ├── TweetsCarousel.php
│ │ ├── TweetsCarouselZ1m.php
│ │ └── VideoGroup.php
│ ├── ParserInterface.php
│ └── ParsingRuleInterface.php
├── stubs/
│ └── RelatedSearch.php
└── test/
├── bin/
│ ├── ci.bash
│ ├── phpcbf.bash
│ ├── phpcs.bash
│ └── test.bash
├── resources/
│ ├── pages-evaluated/
│ │ ├── 2018/
│ │ │ ├── 03/
│ │ │ │ ├── asian+massage.html
│ │ │ │ ├── plumber+london.html
│ │ │ │ ├── qui+est+homer+simpsons.html
│ │ │ │ └── super+u+paris.html
│ │ │ ├── 07/
│ │ │ │ └── foo+bar(page2).html
│ │ │ ├── 09/
│ │ │ │ └── 65b6be0a-7619-4018-97c9-989cdec53319.html
│ │ │ ├── 10/
│ │ │ │ ├── agence+web+nantes.html
│ │ │ │ └── who+is+homer+simpson.html
│ │ │ └── 11/
│ │ │ └── acheter+kobo.html
│ │ ├── 2019/
│ │ │ ├── 05/
│ │ │ │ └── plombier+paris.html
│ │ │ └── 07/
│ │ │ └── cheap+video+editing+software+mac.html
│ │ ├── adwords/
│ │ │ └── simpsons+poster.html
│ │ ├── alarmas+para+casa.html
│ │ ├── captcha.html
│ │ ├── cards-design.html
│ │ ├── flights.html
│ │ ├── github(with-vertical-top-stories).html
│ │ ├── github.html
│ │ ├── how+is+homer+simpsons.html
│ │ ├── inde(top-stories).html
│ │ ├── narendra+modi.html
│ │ ├── page-with-bkgrouped-results.html
│ │ ├── ransomware.html
│ │ ├── shop-near-paris.html
│ │ ├── simpsons(related).html
│ │ ├── simpsons+donut.html
│ │ ├── simpsons+movie+trailer.html
│ │ ├── simpsons.html
│ │ ├── simpsonsworld.html
│ │ └── with-DOMText.html
│ ├── pages-mobile/
│ │ ├── 2017/
│ │ │ ├── 11/
│ │ │ │ ├── buy+pen.html
│ │ │ │ └── donald+trump.html
│ │ │ └── 12/
│ │ │ ├── foo.html
│ │ │ └── who+is+homer+simpsons.html
│ │ ├── 2018/
│ │ │ ├── 03/
│ │ │ │ └── foo.html
│ │ │ ├── 07/
│ │ │ │ └── simpsons-episode-1.html
│ │ │ ├── 08/
│ │ │ │ └── new+construction+ct.html
│ │ │ └── 11/
│ │ │ └── 01/
│ │ │ └── plombier+nantes.html
│ │ ├── simpsons+donuts.html
│ │ ├── simpsons+homer.html
│ │ └── simpsons+world.html
│ ├── pages-raw/
│ │ └── simpsons.html
│ └── simple-dom.html
└── suites/
├── AdwordsResultItemTest.php
├── AdwordsSectionResultSetTest.php
├── GoogleClientTest.php
├── GoogleSerpTestCase.php
├── GoogleUrlTest.php
├── Page/
│ ├── GoogleCaptchaTest.php
│ ├── GoogleDomTest.php
│ └── GoogleSerpTest.php
└── Parser/
└── Evaluated/
├── AdwordsParserTest.php
├── NaturalParserTest.php
└── natural-parser-data/
├── 2018/
│ ├── 03/
│ │ ├── asian+massage.yml
│ │ ├── plumber+london.yml
│ │ ├── qui+est+homer+simpsons.yml
│ │ └── super+u+paris.yml
│ ├── 07/
│ │ └── foo+bar(page2).yml
│ ├── 09/
│ │ └── 65b6be0a-7619-4018-97c9-989cdec53319.yml
│ ├── 10/
│ │ ├── agence+web+nantes.yml
│ │ └── who+is+homer+simpson.yml
│ └── 11/
│ └── acheter+kobo.yml
├── 2019/
│ ├── 05/
│ │ └── plombier+paris.yml
│ └── 07/
│ └── cheap+video+editing+software+mac.yml
├── github(with-vertical-top-stories).yml
├── inde(top-stories).yml
├── mobile/
│ ├── 2017/
│ │ ├── 11/
│ │ │ ├── buy+pen.yml
│ │ │ └── mobile-donald+trump.yml
│ │ └── 12/
│ │ ├── foo.yml
│ │ └── who+is+homer+simpsons.yml
│ └── 2018/
│ ├── 03/
│ │ └── foo.yml
│ ├── 07/
│ │ └── simpsons-episode-1.yml
│ ├── 08/
│ │ └── new-construction-ct.yml
│ └── 11/
│ └── 01/
│ └── plombier+nantes.yml
├── mobile-simpsons+donuts.yml
├── mobile-simpsons+homer.yml
├── mobile-simpsons+world.yml
├── narendra+modi.yml
├── natural-cards.yml
├── naturals-data-with_bkgroups.yml
├── naturals-data.yml
├── ransomware.yml
├── simpsons+donuts.yml
├── simpsons+movie+trailer.yml
└── simpsonsworld.yml
SYMBOL INDEX (262 symbols across 65 files)
FILE: src/AdwordsResultItem.php
class AdwordsResultItem (line 12) | class AdwordsResultItem extends ProxyResult
method __construct (line 23) | public function __construct($location, ResultDataInterface $itemData)
method getTypes (line 29) | public function getTypes()
method is (line 36) | public function is($types)
FILE: src/AdwordsResultType.php
class AdwordsResultType (line 8) | class AdwordsResultType
FILE: src/AdwordsSectionResultSet.php
class AdwordsSectionResultSet (line 15) | class AdwordsSectionResultSet extends IndexedResultSet
method __construct (line 23) | public function __construct($location)
method addItem (line 33) | public function addItem(ResultDataInterface $item)
FILE: src/Exception/GoogleCaptchaException.php
class GoogleCaptchaException (line 14) | class GoogleCaptchaException extends CaptchaException
method __construct (line 17) | public function __construct(GoogleCaptcha $captchaResponse)
FILE: src/Exception/InvalidDOMException.php
class InvalidDOMException (line 10) | class InvalidDOMException extends Exception
method __construct (line 13) | public function __construct($message)
FILE: src/GoogleClient.php
class GoogleClient (line 28) | class GoogleClient
method __construct (line 33) | public function __construct(BrowserInterface $browser = null)
method query (line 48) | public function query(GoogleUrlInterface $googleUrl, BrowserInterface ...
FILE: src/GoogleUrl.php
class GoogleUrl (line 11) | class GoogleUrl extends Url implements GoogleUrlInterface
method __construct (line 24) | public function __construct(
method build (line 37) | public static function build(
method setLanguageRestriction (line 64) | public function setLanguageRestriction($lang)
method setPage (line 83) | public function setPage($pageNumber)
method setResultsPerPage (line 98) | public function setResultsPerPage($number)
method setSearchTerm (line 123) | public function setSearchTerm($search)
method setAutoCorrectionEnabled (line 133) | public function setAutoCorrectionEnabled($enabled)
method setResultType (line 148) | public function setResultType($resultType)
FILE: src/GoogleUrlArchive.php
class GoogleUrlArchive (line 15) | class GoogleUrlArchive extends UrlArchive implements GoogleUrlInterface
method __construct (line 19) | public function __construct(
method build (line 32) | public static function build(
FILE: src/GoogleUrlInterface.php
type GoogleUrlInterface (line 14) | interface GoogleUrlInterface extends UrlArchiveInterface
method getPage (line 21) | public function getPage();
method getLanguageRestriction (line 26) | public function getLanguageRestriction();
method getResultsPerPage (line 32) | public function getResultsPerPage();
method getResultType (line 38) | public function getResultType();
method getArchive (line 44) | public function getArchive();
method getAutoCorrectionEnabled (line 50) | public function getAutoCorrectionEnabled();
FILE: src/GoogleUrlTrait.php
type GoogleUrlTrait (line 16) | trait GoogleUrlTrait
method getParamValue (line 19) | abstract public function getParamValue($param, $defaultValue = null);
method buildUrl (line 20) | abstract public function buildUrl();
method getParamRawValue (line 21) | abstract public function getParamRawValue($param, $defaultValue = null);
method getHost (line 22) | abstract public function getHost();
method getPath (line 23) | abstract public function getPath();
method getScheme (line 24) | abstract public function getScheme();
method getParams (line 25) | abstract public function getParams();
method getHash (line 26) | abstract public function getHash();
method getPage (line 32) | public function getPage()
method getLanguageRestriction (line 41) | public function getLanguageRestriction()
method getResultsPerPage (line 50) | public function getResultsPerPage()
method getResultType (line 60) | public function getResultType()
method getAutoCorrectionEnabled (line 69) | public function getAutoCorrectionEnabled()
method getSearchTerm (line 78) | public function getSearchTerm()
method getArchive (line 83) | public function getArchive()
FILE: src/NaturalResultType.php
class NaturalResultType (line 8) | abstract class NaturalResultType
FILE: src/Page/GoogleCaptcha.php
class GoogleCaptcha (line 13) | class GoogleCaptcha implements CaptchaResponse
method __construct (line 25) | public function __construct(GoogleError $googleError)
method getErrorPage (line 33) | public function getErrorPage()
method getCaptchaType (line 39) | public function getCaptchaType()
method getData (line 50) | public function getData()
method getDetectedIp (line 55) | public function getDetectedIp()
FILE: src/Page/GoogleDom.php
class GoogleDom (line 17) | class GoogleDom extends WebPage
method __construct (line 38) | public function __construct($domString, GoogleUrlInterface $url)
method getJsonNodeProperty (line 57) | public function getJsonNodeProperty($propertyName, \DOMNode $node)
FILE: src/Page/GoogleError.php
class GoogleError (line 11) | class GoogleError extends WebPage
method isCaptcha (line 17) | public function isCaptcha()
FILE: src/Page/GoogleSerp.php
class GoogleSerp (line 16) | class GoogleSerp extends GoogleDom
method getLocation (line 23) | public function getLocation()
method getNaturalResults (line 38) | public function getNaturalResults()
method getAdwordsResults (line 58) | public function getAdwordsResults()
method getNumberOfResults (line 78) | public function getNumberOfResults()
method javascriptIsEvaluated (line 113) | public function javascriptIsEvaluated()
method getRelatedSearches (line 136) | public function getRelatedSearches()
method isMobile (line 173) | public function isMobile()
FILE: src/Page/NotFound.php
class NotFound (line 8) | class NotFound extends GoogleDom
FILE: src/Parser/AbstractAdwordsParser.php
class AbstractAdwordsParser (line 11) | abstract class AbstractAdwordsParser implements ParserInterface
method generateParsers (line 23) | abstract public function generateParsers();
method getParsers (line 28) | public function getParsers()
method parse (line 39) | public function parse(GoogleDom $googleDom)
FILE: src/Parser/AbstractParser.php
class AbstractParser (line 12) | abstract class AbstractParser implements ParserInterface
method generateRules (line 23) | abstract protected function generateRules();
method getParsableItems (line 29) | abstract protected function getParsableItems(GoogleDom $googleDom);
method getRules (line 35) | public function getRules()
method parse (line 48) | public function parse(GoogleDom $googleDom)
method createResultSet (line 60) | protected function createResultSet(GoogleDom $googleDom)
method parseGroups (line 72) | protected function parseGroups(DomNodeList $elementGroups, IndexedResu...
FILE: src/Parser/Evaluated/AdwordsParser.php
class AdwordsParser (line 12) | class AdwordsParser extends AbstractAdwordsParser
method generateParsers (line 18) | public function generateParsers()
FILE: src/Parser/Evaluated/AdwordsSectionParser.php
class AdwordsSectionParser (line 17) | class AdwordsSectionParser extends AbstractParser
method __construct (line 26) | public function __construct($pathToItems, $location)
method createResultSet (line 35) | protected function createResultSet(GoogleDom $googleDom)
method generateRules (line 43) | protected function generateRules()
method getParsableItems (line 54) | protected function getParsableItems(GoogleDom $googleDom)
FILE: src/Parser/Evaluated/MobileAdwordsParser.php
class MobileAdwordsParser (line 12) | class MobileAdwordsParser extends AbstractAdwordsParser
method generateParsers (line 18) | public function generateParsers()
FILE: src/Parser/Evaluated/MobileAdwordsSectionParser.php
class MobileAdwordsSectionParser (line 13) | class MobileAdwordsSectionParser extends AdwordsSectionParser
method generateRules (line 18) | protected function generateRules()
FILE: src/Parser/Evaluated/MobileNaturalParser.php
class MobileNaturalParser (line 30) | class MobileNaturalParser extends AbstractParser
method generateRules (line 36) | protected function generateRules()
method getParsableItems (line 60) | protected function getParsableItems(GoogleDom $googleDom)
FILE: src/Parser/Evaluated/NaturalParser.php
class NaturalParser (line 29) | class NaturalParser extends AbstractParser
method generateRules (line 35) | protected function generateRules()
method getParsableItems (line 59) | protected function getParsableItems(GoogleDom $googleDom)
FILE: src/Parser/Evaluated/Rule/Adwords/AdwordsItem.php
class AdwordsItem (line 16) | class AdwordsItem implements ParsingRuleInterface
method match (line 19) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parse (line 26) | public function parse(GoogleDom $googleDOM, \DomElement $node, Indexed...
FILE: src/Parser/Evaluated/Rule/Adwords/AdwordsItemMobile.php
class AdwordsItemMobile (line 16) | class AdwordsItemMobile implements ParsingRuleInterface
method match (line 22) | public function match(GoogleDom $dom, DomElement $node)
method parse (line 33) | public function parse(GoogleDom $googleDOM, \DomElement $node, Indexed...
FILE: src/Parser/Evaluated/Rule/Adwords/Shopping.php
class Shopping (line 16) | class Shopping implements ParsingRuleInterface
method match (line 19) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parse (line 28) | public function parse(GoogleDom $googleDOM, \DomElement $node, Indexed...
method parseItem (line 46) | public function parseItem(GoogleDom $googleDOM, \DOMNode $node)
FILE: src/Parser/Evaluated/Rule/Natural/AnswerBox.php
class AnswerBox (line 14) | class AnswerBox implements ParsingRuleInterface
method match (line 17) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parseNode (line 30) | protected function parseNode(GoogleDom $dom, \DOMElement $node)
method parse (line 78) | public function parse(GoogleDom $dom, \DOMElement $node, IndexedResult...
FILE: src/Parser/Evaluated/Rule/Natural/Classical/ClassicalCardsResult.php
class ClassicalCardsResult (line 14) | class ClassicalCardsResult extends ClassicalResult
method match (line 17) | public function match(GoogleDom $dom, DomElement $node)
method isLarge (line 44) | protected function isLarge(GoogleDom $dom, \DomElement $node)
FILE: src/Parser/Evaluated/Rule/Natural/Classical/ClassicalCardsResultO9g5cc.php
class ClassicalCardsResultO9g5cc (line 19) | class ClassicalCardsResultO9g5cc implements ParsingRuleInterface
method match (line 22) | public function match(GoogleDom $dom, DomElement $node)
method parse (line 34) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
method parseNode (line 44) | protected function parseNode(GoogleDom $dom, DomNodeInterface $node)
FILE: src/Parser/Evaluated/Rule/Natural/Classical/ClassicalCardsResultZ1m.php
class ClassicalCardsResultZ1m (line 21) | class ClassicalCardsResultZ1m implements ParsingRuleInterface
method match (line 24) | public function match(GoogleDom $dom, DomElement $node)
method parse (line 52) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
method parseNode (line 62) | protected function parseNode(GoogleDom $dom, DomElement $node)
FILE: src/Parser/Evaluated/Rule/Natural/Classical/ClassicalCardsResultZINbbc.php
class ClassicalCardsResultZINbbc (line 20) | class ClassicalCardsResultZINbbc implements ParsingRuleInterface
method match (line 23) | public function match(GoogleDom $dom, DomElement $node)
method parse (line 34) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
method parseNode (line 44) | protected function parseNode(GoogleDom $dom, DomNodeInterface $node)
FILE: src/Parser/Evaluated/Rule/Natural/Classical/ClassicalCardsVideoResult.php
class ClassicalCardsVideoResult (line 15) | class ClassicalCardsVideoResult extends ClassicalCardsResult
method match (line 18) | public function match(GoogleDom $dom, DomElement $node)
method parse (line 31) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
FILE: src/Parser/Evaluated/Rule/Natural/Classical/ClassicalResult.php
class ClassicalResult (line 17) | class ClassicalResult implements ParsingRuleInterface
method match (line 20) | public function match(GoogleDom $dom, DomElement $node)
method parseNode (line 30) | protected function parseNode(GoogleDom $dom, \DomElement $node)
method parseSiteLink (line 82) | protected function parseSiteLink(GoogleDom $dom, \DomElement $node)
method isLarge (line 116) | protected function isLarge(GoogleDom $dom, \DomElement $node)
method parse (line 121) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
FILE: src/Parser/Evaluated/Rule/Natural/Classical/ClassicalWithLargeVideo.php
class ClassicalWithLargeVideo (line 15) | class ClassicalWithLargeVideo implements ParsingRuleInterface
method match (line 18) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parse (line 29) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
FILE: src/Parser/Evaluated/Rule/Natural/ComposedTopStories.php
class ComposedTopStories (line 16) | class ComposedTopStories implements ParsingRuleInterface
method match (line 18) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parse (line 31) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
method parseNode (line 40) | private function parseNode(GoogleDom $dom, $node)
method parseVerticalResults (line 59) | private function parseVerticalResults(GoogleDom $dom, DomElement $node)
method parseCarouselResults (line 80) | private function parseCarouselResults(GoogleDom $dom, DomElement $node)
FILE: src/Parser/Evaluated/Rule/Natural/Divider.php
class Divider (line 12) | class Divider implements \Serps\SearchEngine\Google\Parser\ParsingRuleIn...
method match (line 15) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parse (line 25) | public function parse(GoogleDom $googleDOM, \DomElement $group, Indexe...
FILE: src/Parser/Evaluated/Rule/Natural/Flight.php
class Flight (line 14) | class Flight implements \Serps\SearchEngine\Google\Parser\ParsingRuleInt...
method match (line 17) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parse (line 25) | public function parse(GoogleDom $googleDOM, \DomElement $group, Indexe...
FILE: src/Parser/Evaluated/Rule/Natural/ImageGroup.php
class ImageGroup (line 16) | class ImageGroup implements \Serps\SearchEngine\Google\Parser\ParsingRul...
method match (line 19) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parse (line 27) | public function parse(GoogleDom $googleDOM, \DomElement $node, Indexed...
method parseItem (line 53) | private function parseItem(GoogleDom $googleDOM, \DOMElement $imgNode)
FILE: src/Parser/Evaluated/Rule/Natural/ImageGroupCarousel.php
class ImageGroupCarousel (line 16) | class ImageGroupCarousel implements \Serps\SearchEngine\Google\Parser\Pa...
method match (line 19) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parse (line 27) | public function parse(GoogleDom $googleDOM, \DomElement $node, Indexed...
method parseItem (line 59) | private function parseItem(GoogleDom $googleDOM, \DOMElement $imgNode)
FILE: src/Parser/Evaluated/Rule/Natural/InTheNews.php
class InTheNews (line 15) | class InTheNews implements \Serps\SearchEngine\Google\Parser\ParsingRule...
method match (line 18) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parse (line 30) | public function parse(GoogleDom $googleDOM, \DomElement $group, Indexe...
method parseItem (line 49) | protected function parseItem(GoogleDom $googleDOM, \DomElement $node)
FILE: src/Parser/Evaluated/Rule/Natural/KnowledgeCard.php
class KnowledgeCard (line 15) | class KnowledgeCard implements ParsingRuleInterface
method match (line 18) | public function match(GoogleDom $dom, DomElement $node)
method parse (line 26) | public function parse(GoogleDom $googleDOM, \DomElement $node, Indexed...
FILE: src/Parser/Evaluated/Rule/Natural/Map.php
class Map (line 15) | class Map implements ParsingRuleInterface
method match (line 18) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parse (line 26) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
method parseItem (line 51) | private function parseItem($localPack, GoogleDom $dom)
FILE: src/Parser/Evaluated/Rule/Natural/MapLegacy.php
class MapLegacy (line 21) | class MapLegacy implements ParsingRuleInterface
method match (line 24) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parse (line 32) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
method parseItem (line 59) | private function parseItem($localPack, GoogleDom $dom)
FILE: src/Parser/Evaluated/Rule/Natural/MapMobile.php
class MapMobile (line 15) | class MapMobile implements ParsingRuleInterface
method match (line 18) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parse (line 26) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
method parseItem (line 47) | private function parseItem($localPack, GoogleDom $dom)
FILE: src/Parser/Evaluated/Rule/Natural/PeopleAlsoAsk.php
class PeopleAlsoAsk (line 24) | class PeopleAlsoAsk implements ParsingRuleInterface
method match (line 27) | public function match(GoogleDom $dom, DomElement $node)
method parse (line 39) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
FILE: src/Parser/Evaluated/Rule/Natural/SearchResultGroup.php
class SearchResultGroup (line 15) | class SearchResultGroup implements ParsingRuleInterface
method match (line 17) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parse (line 26) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
FILE: src/Parser/Evaluated/Rule/Natural/TopStoriesCarousel.php
class TopStoriesCarousel (line 15) | class TopStoriesCarousel implements ParsingRuleInterface
method match (line 17) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parseNode (line 30) | private function parseNode(GoogleDom $dom, $node)
method parse (line 61) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
FILE: src/Parser/Evaluated/Rule/Natural/TopStoriesVertical.php
class TopStoriesVertical (line 15) | class TopStoriesVertical implements ParsingRuleInterface
method match (line 17) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parseNode (line 29) | private function parseNode(GoogleDom $dom, $node)
method parse (line 60) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
FILE: src/Parser/Evaluated/Rule/Natural/TweetsCarousel.php
class TweetsCarousel (line 14) | class TweetsCarousel implements ParsingRuleInterface
method match (line 17) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parse (line 26) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
FILE: src/Parser/Evaluated/Rule/Natural/TweetsCarouselZ1m.php
class TweetsCarouselZ1m (line 19) | class TweetsCarouselZ1m implements ParsingRuleInterface
method match (line 22) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parse (line 37) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
FILE: src/Parser/Evaluated/Rule/Natural/VideoGroup.php
class VideoGroup (line 18) | class VideoGroup implements ParsingRuleInterface
method match (line 21) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node)
method parse (line 29) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
FILE: src/Parser/ParserInterface.php
type ParserInterface (line 11) | interface ParserInterface
method parse (line 18) | public function parse(GoogleDom $googleDom);
FILE: src/Parser/ParsingRuleInterface.php
type ParsingRuleInterface (line 11) | interface ParsingRuleInterface
method match (line 17) | public function match(GoogleDom $dom, \Serps\Core\Dom\DomElement $node);
method parse (line 18) | public function parse(GoogleDom $dom, \DomElement $node, IndexedResult...
FILE: stubs/RelatedSearch.php
class RelatedSearch (line 9) | class RelatedSearch
FILE: test/suites/AdwordsResultItemTest.php
class AdwordsResultItemTest (line 14) | class AdwordsResultItemTest extends \PHPUnit_Framework_TestCase
method testIs (line 17) | public function testIs()
method testGetTypes (line 27) | public function testGetTypes()
FILE: test/suites/AdwordsSectionResultSetTest.php
class AdwordsSectionResultSetTest (line 15) | class AdwordsSectionResultSetTest extends \PHPUnit_Framework_TestCase
method testAddItem (line 18) | public function testAddItem()
FILE: test/suites/GoogleClientTest.php
class GoogleClientTest (line 25) | class GoogleClientTest extends \PHPUnit_Framework_TestCase
method testValidDom (line 28) | public function testValidDom()
method testInvalidHttpResponse (line 51) | public function testInvalidHttpResponse()
method testCaptchaDom (line 77) | public function testCaptchaDom()
FILE: test/suites/GoogleSerpTestCase.php
class GoogleSerpTestCase (line 14) | class GoogleSerpTestCase extends \PHPUnit_Framework_TestCase
method assertResultHasTypes (line 17) | public function assertResultHasTypes(
method assertResultDoesNotHaveTypes (line 53) | public function assertResultDoesNotHaveTypes(array $types, ResultDataI...
method assertResultHasData (line 61) | public function assertResultHasData(array $dataArray, $result, $curren...
method assertResultDataCount (line 99) | public function assertResultDataCount(array $dataArray, $result)
method assertResultHasDataMedia (line 116) | public function assertResultHasDataMedia(array $dataArray, $result)
FILE: test/suites/GoogleUrlTest.php
class GoogleUrlTest (line 17) | class GoogleUrlTest extends \PHPUnit_Framework_TestCase
method testConstruct (line 20) | public function testConstruct()
method testGetArchive (line 27) | public function testGetArchive()
method testLanguageRestriction (line 35) | public function testLanguageRestriction()
method testPage (line 49) | public function testPage()
method testResultsPerPage (line 67) | public function testResultsPerPage()
method testSearchTerm (line 89) | public function testSearchTerm()
method testResultType (line 99) | public function testResultType()
FILE: test/suites/Page/GoogleCaptchaTest.php
class GoogleCaptchaTest (line 16) | class GoogleCaptchaTest extends \PHPUnit_Framework_TestCase
method testCaptcha (line 19) | public function testCaptcha()
FILE: test/suites/Page/GoogleDomTest.php
class GoogleDomTest (line 15) | class GoogleDomTest extends \PHPUnit_Framework_TestCase
method getDom (line 21) | public function getDom()
method testGetXPath (line 27) | public function testGetXPath()
method testGetDom (line 33) | public function testGetDom()
method testXPathQuery (line 39) | public function testXPathQuery()
method testCssQuery (line 59) | public function testCssQuery()
method testGetUrl (line 71) | public function testGetUrl()
method testGetJsonNodeProperty (line 77) | public function testGetJsonNodeProperty()
FILE: test/suites/Page/GoogleSerpTest.php
class GoogleSerpTest (line 17) | class GoogleSerpTest extends \PHPUnit_Framework_TestCase
method getDomJavascript (line 23) | public function getDomJavascript()
method getDomNoJavascript (line 31) | public function getDomNoJavascript()
method testGetNumberOfResults (line 37) | public function testGetNumberOfResults()
method testGetLocation (line 47) | public function testGetLocation()
method testGetNaturalResults (line 52) | public function testGetNaturalResults()
method testGetAdwordsResults (line 67) | public function testGetAdwordsResults()
method testJavascriptEvaluated (line 78) | public function testJavascriptEvaluated()
method testRelatedSearches (line 84) | public function testRelatedSearches()
FILE: test/suites/Parser/Evaluated/AdwordsParserTest.php
class AdwordsParserTest (line 33) | class AdwordsParserTest extends \PHPUnit_Framework_TestCase
method testParserTopAndBottom (line 35) | public function testParserTopAndBottom()
FILE: test/suites/Parser/Evaluated/NaturalParserTest.php
class NaturalParserTest (line 57) | class NaturalParserTest extends GoogleSerpTestCase
method serpProvider (line 60) | public function serpProvider()
method testSerps (line 78) | public function testSerps($file)
method testResultWithMap (line 170) | public function testResultWithMap()
method testLargeResult (line 225) | public function testLargeResult()
method testNidGroup (line 261) | public function testNidGroup()
method testFlights (line 296) | public function testFlights()
method testAnswerBox (line 327) | public function testAnswerBox()
method testResultPosition (line 369) | public function testResultPosition()
method testResultWithDomText (line 395) | public function testResultWithDomText()
Copy disabled (too large)
Download .json
Condensed preview — 156 files, each showing path, character count, and a content snippet. Download the .json file for the full structured content (16,573K chars).
[
{
"path": ".editorconfig",
"chars": 415,
"preview": "; This file is for unifying the coding style for different editors and IDEs.\n; More information at http://editorconfig.o"
},
{
"path": ".gitattributes",
"chars": 379,
"preview": "/build export-ignore\n/script export-ignore\n/test export-ignore\n.codeclimate.yml "
},
{
"path": ".gitignore",
"chars": 28,
"preview": "composer.lock\n/vendor\n.idea\n"
},
{
"path": ".travis.yml",
"chars": 1376,
"preview": "language: php\n\ndist: trusty\nsudo: false\n\nmatrix:\n include:\n - php: 5.5\n env: PROCESS_CODECLIMATE=true\n - php: 5.6\n"
},
{
"path": "CHANGELOG.md",
"chars": 6905,
"preview": "# CHANGELOG\n\n## 0.4.7\n\n*2018-18-05*\n\n* Bug fix: \n * Fixed title for adwords results\n\n## 0.4.6\n\n*2018-30-10*\n\n* Featur"
},
{
"path": "CONTRIBUTING.md",
"chars": 1502,
"preview": "CONTRIBUTING\n============\n\nAny contribution is welcome.\n\n\nIssue\n-----\n\nIf you encounter an issue with the library you ar"
},
{
"path": "LICENSE",
"chars": 284,
"preview": "Copyright (c) 2015-today, Soufiane GHZAL <sghzal@gmail.com> and contributors\n\nUsage of the works is permitted provided t"
},
{
"path": "README.md",
"chars": 1760,
"preview": "SERPS - Search Engine: Google\n=============================\n\n[\nSCRIPTDIR=$(dirname \"$SCRIPTFILE\")\n\n\necho -e \"\\e[34m\"\necho \"========"
},
{
"path": "test/bin/phpcbf.bash",
"chars": 186,
"preview": "#!/bin/bash\n\nSCRIPTFILE=$(readlink -f \"$0\")\nSCRIPTDIR=$(dirname \"$SCRIPTFILE\")\n\ncd $SCRIPTDIR/../.. && $SCRIPTDIR/../../"
},
{
"path": "test/bin/phpcs.bash",
"chars": 257,
"preview": "#!/bin/bash\n\nSCRIPTFILE=$(readlink -f \"$0\")\nSCRIPTDIR=$(dirname \"$SCRIPTFILE\")\n\n\nif [ -n \"$1\" ]; then report=$1; else re"
},
{
"path": "test/bin/test.bash",
"chars": 229,
"preview": "#!/bin/bash\n\nSCRIPTFILE=$(readlink -f \"$0\")\nSCRIPTDIR=$(dirname \"$SCRIPTFILE\")\n\n\nif [ -n \"$1\" ]; then filter=$1; else fi"
},
{
"path": "test/resources/pages-evaluated/2018/03/asian+massage.html",
"chars": 352612,
"preview": "<!DOCTYPE html>\n<?xml encoding=\"UTF-8\"><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"en-GB\"><h"
},
{
"path": "test/resources/pages-evaluated/2018/03/plumber+london.html",
"chars": 423847,
"preview": "\n<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta content=\"/images"
},
{
"path": "test/resources/pages-evaluated/2018/03/qui+est+homer+simpsons.html",
"chars": 489286,
"preview": "\n<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta content=\"/images"
},
{
"path": "test/resources/pages-evaluated/2018/03/super+u+paris.html",
"chars": 408600,
"preview": "<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta content=\"/images/"
},
{
"path": "test/resources/pages-evaluated/2018/07/foo+bar(page2).html",
"chars": 314994,
"preview": "<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta content=\"/images/"
},
{
"path": "test/resources/pages-evaluated/2018/09/65b6be0a-7619-4018-97c9-989cdec53319.html",
"chars": 619090,
"preview": "<!DOCTYPE html>\n<?xml encoding=\"UTF-8\"><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"de\"><head"
},
{
"path": "test/resources/pages-evaluated/2018/10/agence+web+nantes.html",
"chars": 384372,
"preview": "<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta content=\"/images/"
},
{
"path": "test/resources/pages-evaluated/2018/10/who+is+homer+simpson.html",
"chars": 543283,
"preview": "<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta content=\"/images/"
},
{
"path": "test/resources/pages-evaluated/2018/11/acheter+kobo.html",
"chars": 489020,
"preview": "<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta charset=\"UTF-8\"><"
},
{
"path": "test/resources/pages-evaluated/2019/05/plombier+paris.html",
"chars": 421084,
"preview": "\n<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta content=\"/images"
},
{
"path": "test/resources/pages-evaluated/2019/07/cheap+video+editing+software+mac.html",
"chars": 642718,
"preview": "\n<!DOCTYPE html>\n<?xml encoding=\"UTF-8\"><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"en\"><hea"
},
{
"path": "test/resources/pages-evaluated/adwords/simpsons+poster.html",
"chars": 539910,
"preview": "<html itemscope=\"\" itemtype=\"http://schema.org/WebPage\" lang=\"en-AU\"><head><meta content=\"/images/branding/googleg/1x/go"
},
{
"path": "test/resources/pages-evaluated/alarmas+para+casa.html",
"chars": 327190,
"preview": "<html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"es\"><head><meta content=\"/images/branding/google"
},
{
"path": "test/resources/pages-evaluated/captcha.html",
"chars": 2955,
"preview": "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n<html>\n<head><meta http-equiv=\"content-type\" content=\"te"
},
{
"path": "test/resources/pages-evaluated/cards-design.html",
"chars": 396074,
"preview": "<!DOCTYPE html>\n<?xml encoding=\"UTF-8\"><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"nl\"><head"
},
{
"path": "test/resources/pages-evaluated/flights.html",
"chars": 317801,
"preview": "<html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta content=\"/images/branding/google"
},
{
"path": "test/resources/pages-evaluated/github(with-vertical-top-stories).html",
"chars": 359987,
"preview": "<!DOCTYPE html>\n<?xml encoding=\"UTF-8\"><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head"
},
{
"path": "test/resources/pages-evaluated/github.html",
"chars": 311918,
"preview": "<html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta content=\"/images/branding/google"
},
{
"path": "test/resources/pages-evaluated/how+is+homer+simpsons.html",
"chars": 402035,
"preview": "<html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"en-GB\"><head><meta content=\"/images/branding/goo"
},
{
"path": "test/resources/pages-evaluated/inde(top-stories).html",
"chars": 557007,
"preview": "<!DOCTYPE html>\n<html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta content=\"/images"
},
{
"path": "test/resources/pages-evaluated/narendra+modi.html",
"chars": 440585,
"preview": "\n<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"en-GB\"><head><meta content=\"/ima"
},
{
"path": "test/resources/pages-evaluated/page-with-bkgrouped-results.html",
"chars": 414401,
"preview": "<!DOCTYPE html>\n<?xml encoding=\"UTF-8\"><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"ru\"><head"
},
{
"path": "test/resources/pages-evaluated/ransomware.html",
"chars": 370495,
"preview": "\n<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"en-GB\"><head><meta content=\"/ima"
},
{
"path": "test/resources/pages-evaluated/shop-near-paris.html",
"chars": 382010,
"preview": "<html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"en-FR\"><head><meta content=\"/images/branding/goo"
},
{
"path": "test/resources/pages-evaluated/simpsons(related).html",
"chars": 628060,
"preview": "<!DOCTYPE html>\n<html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"en-FR\"><head><meta content=\"/ima"
},
{
"path": "test/resources/pages-evaluated/simpsons+donut.html",
"chars": 496811,
"preview": "<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"en-AU\"><head><meta content=\"/imag"
},
{
"path": "test/resources/pages-evaluated/simpsons+movie+trailer.html",
"chars": 289803,
"preview": "<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta content=\"/images/"
},
{
"path": "test/resources/pages-evaluated/simpsons.html",
"chars": 612449,
"preview": "<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"en-FR\"><head><meta content=\"/imag"
},
{
"path": "test/resources/pages-evaluated/simpsonsworld.html",
"chars": 310834,
"preview": "\n<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta content=\"/images"
},
{
"path": "test/resources/pages-evaluated/with-DOMText.html",
"chars": 299694,
"preview": "<!DOCTYPE html>\n<?xml encoding=\"UTF-8\"><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"en-CA\"><h"
},
{
"path": "test/resources/pages-mobile/2017/11/buy+pen.html",
"chars": 200900,
"preview": "<!DOCTYPE html>\n<?xml encoding=\"UTF-8\"><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"en-NZ\"><h"
},
{
"path": "test/resources/pages-mobile/2017/11/donald+trump.html",
"chars": 372928,
"preview": "\n<!DOCTYPE html>\n<?xml encoding=\"UTF-8\"><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"en\"><hea"
},
{
"path": "test/resources/pages-mobile/2017/12/foo.html",
"chars": 327496,
"preview": "\n<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta charset=\"UTF-8\">"
},
{
"path": "test/resources/pages-mobile/2017/12/who+is+homer+simpsons.html",
"chars": 329336,
"preview": "\n<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"en-FR\"><head><meta charset=\"UTF-"
},
{
"path": "test/resources/pages-mobile/2018/03/foo.html",
"chars": 441668,
"preview": "\n<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta charset=\"UTF-8\">"
},
{
"path": "test/resources/pages-mobile/2018/07/simpsons-episode-1.html",
"chars": 220921,
"preview": "\n<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta charset=\"UTF-8\">"
},
{
"path": "test/resources/pages-mobile/2018/08/new+construction+ct.html",
"chars": 132951,
"preview": "\n<!DOCTYPE html>\n<?xml encoding=\"UTF-8\"><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"en\"><hea"
},
{
"path": "test/resources/pages-mobile/2018/11/01/plombier+nantes.html",
"chars": 352340,
"preview": "<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta charset=\"UTF-8\"><"
},
{
"path": "test/resources/pages-mobile/simpsons+donuts.html",
"chars": 268864,
"preview": "\n<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta charset=\"UTF-8\">"
},
{
"path": "test/resources/pages-mobile/simpsons+homer.html",
"chars": 442584,
"preview": "<!DOCTYPE html>\n<html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta http-equiv=\"cont"
},
{
"path": "test/resources/pages-mobile/simpsons+world.html",
"chars": 187921,
"preview": "<!DOCTYPE html>\n<?xml encoding=\"UTF-8\"><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head"
},
{
"path": "test/resources/pages-raw/simpsons.html",
"chars": 91824,
"preview": "<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"fr\"><head><meta content=\"text/htm"
},
{
"path": "test/resources/simple-dom.html",
"chars": 288,
"preview": "<html>\n <body>\n <div class=\"foo bar\">\n <span class=\"foo\">foo bar - foo span</span>\n <spa"
},
{
"path": "test/suites/AdwordsResultItemTest.php",
"chars": 860,
"preview": "<?php\n/**\n * @license see LICENSE\n */\n\nnamespace Serps\\Test\\TDD\\SearchEngine\\Google;\n\nuse Serps\\Core\\Serp\\BaseResult;\nus"
},
{
"path": "test/suites/AdwordsSectionResultSetTest.php",
"chars": 725,
"preview": "<?php\n/**\n * @license see LICENSE\n */\n\nnamespace Serps\\Test\\TDD\\SearchEngine\\Google;\n\nuse Serps\\Core\\Serp\\BaseResult;\nus"
},
{
"path": "test/suites/GoogleClientTest.php",
"chars": 3986,
"preview": "<?php\n/**\n * @license see LICENSE\n */\n\nnamespace Serps\\Test\\TDD\\SearchEngine\\Google;\n\nuse \\InvalidArgumentException;\nuse"
},
{
"path": "test/suites/GoogleSerpTestCase.php",
"chars": 4997,
"preview": "<?php\n/**\n * @license see LICENSE\n */\n\nnamespace Serps\\Test\\SearchEngine\\Google;\n\nuse Serps\\Core\\Media\\File;\nuse Serps\\C"
},
{
"path": "test/suites/GoogleUrlTest.php",
"chars": 4113,
"preview": "<?php\n/**\n * @license see LICENSE\n */\n\nnamespace Serps\\Test\\TDD\\SearchEngine\\Google;\n\nuse Psr\\Http\\Message\\RequestInterf"
},
{
"path": "test/suites/Page/GoogleCaptchaTest.php",
"chars": 1046,
"preview": "<?php\n/**\n * @license see LICENSE\n */\n\nnamespace Serps\\Test\\TDD\\SearchEngine\\Google\\Page;\n\nuse Serps\\SearchEngine\\Google"
},
{
"path": "test/suites/Page/GoogleDomTest.php",
"chars": 3446,
"preview": "<?php\n/**\n * @license see LICENSE\n */\n\nnamespace Serps\\Test\\TDD\\SearchEngine\\Google\\Page;\n\nuse Serps\\Core\\Http\\Proxy;\nus"
},
{
"path": "test/suites/Page/GoogleSerpTest.php",
"chars": 3944,
"preview": "<?php\n/**\n * @license see LICENSE\n */\n\nnamespace Serps\\Test\\TDD\\SearchEngine\\Google\\Page;\n\nuse Serps\\Core\\Serp\\Composite"
},
{
"path": "test/suites/Parser/Evaluated/AdwordsParserTest.php",
"chars": 4657,
"preview": "<?php\n/**\n * @license see LICENSE\n */\n\nnamespace Serps\\Test\\TDD\\SearchEngine\\Google\\Parser\\Evaluated;\n\nuse Serps\\SearchE"
},
{
"path": "test/suites/Parser/Evaluated/NaturalParserTest.php",
"chars": 17439,
"preview": "<?php\n/**\n * @license see LICENSE\n */\n\nnamespace Serps\\Test\\TDD\\SearchEngine\\Google\\Parser\\Evaluated;\n\nuse Serps\\SearchE"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/2018/03/asian+massage.yml",
"chars": 927,
"preview": "# image-group recipes\nurl: https://www.google.fr/search?q=asian+massage\nfile: test/resources/pages-evaluated/2018/03/asi"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/2018/03/plumber+london.yml",
"chars": 1392,
"preview": "# image-group recipes\nurl: https://www.google.co.uk/search?q=plumber+london\nfile: test/resources/pages-evaluated/2018/03"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/2018/03/qui+est+homer+simpsons.yml",
"chars": 2036,
"preview": "# image-group recipes\nurl: https://www.google.fr/search?q=qui+est+homer+simpsons\nfile: test/resources/pages-evaluated/20"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/2018/03/super+u+paris.yml",
"chars": 879,
"preview": "# image-group recipes\nurl: https://www.google.fr/search?q=super+u+paris\nfile: test/resources/pages-evaluated/2018/03/sup"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/2018/07/foo+bar(page2).yml",
"chars": 1709,
"preview": "url: https://www.google.com.au/search?q=foo+bar\nfile: test/resources/pages-evaluated/2018/07/foo+bar(page2).html\n\ntest-m"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/2018/09/65b6be0a-7619-4018-97c9-989cdec53319.yml",
"chars": 4699,
"preview": "url: https://www.google.de/search?q=mobile+device+management\nfile: test/resources/pages-evaluated/2018/09/65b6be0a-7619-"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/2018/10/agence+web+nantes.yml",
"chars": 921,
"preview": "url: https://www.google.co.uk/search?q=agence+web+nantes\nfile: test/resources/pages-evaluated/2018/10/agence+web+nantes."
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/2018/10/who+is+homer+simpson.yml",
"chars": 1246,
"preview": "url: https://www.google.co.uk/search?q=who+is+homer+simpson\nfile: test/resources/pages-evaluated/2018/10/who+is+homer+si"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/2018/11/acheter+kobo.yml",
"chars": 3317,
"preview": "url: https://www.google.co.uk/search?q=acheter+kobo\nfile: test/resources/pages-evaluated/2018/11/acheter+kobo.html\n\ntest"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/2019/05/plombier+paris.yml",
"chars": 1080,
"preview": "# image-group recipes\nurl: https://www.google.com/search?q=plombier+paris\nfile: test/resources/pages-evaluated/2019/05/p"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/2019/07/cheap+video+editing+software+mac.yml",
"chars": 2108,
"preview": "url: https://www.google.co.uk/search?q=cheap+video+editing+software+mac+nantes\nfile: test/resources/pages-evaluated/2019"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/github(with-vertical-top-stories).yml",
"chars": 947,
"preview": "url: https://www.google.com.au/search?q=simpsons+donut\nfile: test/resources/pages-evaluated/github(with-vertical-top-sto"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/inde(top-stories).yml",
"chars": 1171,
"preview": "# image-group recipes\nurl: https://www.google.fr/search?q=inde&hl=fr_FR\nfile: test/resources/pages-evaluated/inde(top-st"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/mobile/2017/11/buy+pen.yml",
"chars": 1829,
"preview": "# image-group recipes\nurl: https://www.google.fr/search?q=buy+pen\nfile: test/resources/pages-mobile/2017/11/buy+pen.html"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/mobile/2017/11/mobile-donald+trump.yml",
"chars": 2185,
"preview": "# image-group recipes\nurl: https://www.google.fr/search?q=simpsons+world\nfile: test/resources/pages-mobile/2017/11/donal"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/mobile/2017/12/foo.yml",
"chars": 2021,
"preview": "# image-group recipes\nurl: https://www.google.fr/search?q=foo\nfile: test/resources/pages-mobile/2017/12/foo.html\n\ntest-m"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/mobile/2017/12/who+is+homer+simpsons.yml",
"chars": 1000,
"preview": "# image-group recipes\nurl: https://www.google.fr/search?q=who+is+homer+simpsons\nfile: test/resources/pages-mobile/2017/1"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/mobile/2018/03/foo.yml",
"chars": 2436,
"preview": "# image-group recipes\nurl: https://www.google.ca/search?q=foo\nfile: test/resources/pages-mobile/2018/03/foo.html\n\ntest-m"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/mobile/2018/07/simpsons-episode-1.yml",
"chars": 2020,
"preview": "# image-group recipes\nurl: https://www.google.ca/search?q=simpsons+episode+1\nfile: test/resources/pages-mobile/2018/07/s"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/mobile/2018/08/new-construction-ct.yml",
"chars": 1478,
"preview": "# image-group recipes\nurl: https://www.google.ca/search?q=new+construction+ct\nfile: test/resources/pages-mobile/2018/08/"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/mobile/2018/11/01/plombier+nantes.yml",
"chars": 5196,
"preview": "url: https://www.google.co.uk/search?q=plombier+nantes\nfile: test/resources/pages-mobile/2018/11/01/plombier+nantes.html"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/mobile-simpsons+donuts.yml",
"chars": 2556,
"preview": "# image-group recipes\nurl: https://www.google.fr/search?q=simpsons+donuts\nfile: test/resources/pages-mobile/simpsons+don"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/mobile-simpsons+homer.yml",
"chars": 1698,
"preview": "# image-group recipes\nurl: https://www.google.fr/search?q=simpsons+movie+trailer\nfile: test/resources/pages-mobile/simps"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/mobile-simpsons+world.yml",
"chars": 1271,
"preview": "# image-group recipes\nurl: https://www.google.fr/search?q=simpsons+world\nfile: test/resources/pages-mobile/simpsons+worl"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/narendra+modi.yml",
"chars": 687,
"preview": "url: https://www.google.co.uk/search?q=narendra+modi&cad=h\nfile: test/resources/pages-evaluated/narendra+modi.html\nresul"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/natural-cards.yml",
"chars": 2012,
"preview": "# image-group recipes\nurl: https://www.google.fr/search?q=simpsons&hl=en_US\nfile: test/resources/pages-evaluated/cards-d"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/naturals-data-with_bkgroups.yml",
"chars": 528,
"preview": "url: https://www.google.fr/search?q=titley+großbritannien+hotels&hl=de_DE\nfile: test/resources/pages-evaluated/page-with"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/naturals-data.yml",
"chars": 998,
"preview": "url: https://www.google.fr/search?q=simpsons&hl=en_US\nfile: test/resources/pages-evaluated/simpsons.html\n\ntest-methods:\n"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/ransomware.yml",
"chars": 895,
"preview": "# testing null user (see TWEETS_CAROUSEL)\n\nurl: https://www.google.co.uk/search?q=ransomware\nfile: test/resources/pages-"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/simpsons+donuts.yml",
"chars": 1300,
"preview": "# image-group recipes\nurl: https://www.google.com.au/search?q=simpsons+donut\nfile: test/resources/pages-evaluated/simpso"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/simpsons+movie+trailer.yml",
"chars": 870,
"preview": "# image-group recipes\nurl: https://www.google.fr/search?q=simpsons+movie+trailer\nfile: test/resources/pages-evaluated/si"
},
{
"path": "test/suites/Parser/Evaluated/natural-parser-data/simpsonsworld.yml",
"chars": 1561,
"preview": "url: https://www.google.fr/search?q=simpsonsworld\nfile: test/resources/pages-evaluated/simpsonsworld.html\nresults:\n\n\n -"
}
]
About this extraction
This page contains the full source code of the serp-spider/search-engine-google GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 156 files (15.4 MB), approximately 4.0M tokens, and a symbol index with 262 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.