使用Python抓取Web页面

Web-scraping JavaScript page with Python

我正在尝试开发一个简单的网络刮刀。 我想在没有HTML代码的情况下提取文本。 事实上,我实现了这个目标,但我已经看到在加载JavaScript的某些页面中我没有获得好的结果。

例如,如果某些JavaScript代码添加了一些文本,我看不到它,因为当我打电话时

1
response = urllib2.urlopen(request)

我没有添加原始文本(因为JavaScript在客户端中执行)。

所以,我正在寻找一些解决这个问题的想法。


编辑30/2017年12月:这个答案出现在谷歌搜索的最佳结果中,所以我决定更新它。旧答案仍在最后。

dryscape不再维护了,dryscape开发人员推荐的库只是Python 2。我发现使用Selenium的python库和Phantom JS作为一个足够快的Web驱动程序,很容易完成工作。

安装Phantom JS后,请确保当前路径中的phantomjs二进制文件可用:

1
2
3
phantomjs --version
# result:
2.1.1

举个例子,我创建了一个包含以下HTML代码的示例页面。 (链接):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  Javascript scraping test
</head>
<body>
  <p id='intro-text'>No javascript support
</p>
 
     document.getElementById('intro-text').innerHTML = 'Yay! Supports javascript';
   
</body>
</html>

没有javascript它说:No javascript support和javascript:Yay! Supports javascript

没有JS支持的刮痧:

1
2
3
4
5
6
7
8
import requests
from bs4 import BeautifulSoup
response = requests.get(my_url)
soup = BeautifulSoup(response.text)
soup.find(id="intro-text")
# Result:
<p id="intro-text">No javascript support
</p>

使用JS支持进行刮擦:

1
2
3
4
5
6
7
from selenium import webdriver
driver = webdriver.PhantomJS()
driver.get(my_url)
p_element = driver.find_element_by_id(id_='intro-text')
print(p_element.text)
# result:
'Yay! Supports javascript'

您还可以使用Python库dryscrape来抓取javascript驱动的网站。

使用JS支持进行刮擦:

1
2
3
4
5
6
7
8
9
10
import dryscrape
from bs4 import BeautifulSoup
session = dryscrape.Session()
session.visit(my_url)
response = session.body()
soup = BeautifulSoup(response)
soup.find(id="intro-text")
# Result:
<p id="intro-text">Yay! Supports javascript
</p>


我们没有得到正确的结果,因为任何javascript生成的内容都需要在DOM上呈现。当我们获取HTML页面时,我们获取javascript,DOM未经修改的初始化。

因此,我们需要在抓取页面之前呈现javascript内容。

由于硒已经多次在这个帖子中提到过(有时也提到它有多慢),我将列出另外两个可能的解决方案。

解决方案1:这是一个非常好的教程,介绍如何使用Scrapy抓取javascript生成的内容,我们将遵循这一点。

我们需要什么:

  • Docker安装在我们的机器上。到目前为止,这是优于其他解决方案的优势,因为它使用独立于操作系统的平台。

  • 按照我们相应的OS列出的说明安装Splash。从splash文档中引用:

    Splash is a javascript rendering service. It’s a lightweight web browser with an HTTP API, implemented in Python 3 using Twisted and QT5.

    基本上我们将使用Splash来呈现Javascript生成的内容。

  • 运行启动服务器:sudo docker run -p 8050:8050 scrapinghub/splash

  • 安装scrapy-splash插件:pip install scrapy-splash

  • 假设我们已经创建了一个Scrapy项目(如果没有,让我们创建一个),我们将按照指南并更新settings.py

    Then go to your scrapy project’s settings.py and set these middlewares:

    1
    2
    3
    4
    5
    DOWNLOADER_MIDDLEWARES = {
          'scrapy_splash.SplashCookiesMiddleware': 723,
          'scrapy_splash.SplashMiddleware': 725,
          'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
    }

    The URL of the Splash server(if you’re using Win or OSX this should be the URL of the docker machine: How to get a Docker container's IP address from the host?):

    1
    SPLASH_URL = 'http://localhost:8050'

    And finally you need to set these values too:

    1
    2
    DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'
    HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'
  • 最后,我们可以使用SplashRequest

    In a normal spider you have Request objects which you can use to open URLs. If the page you want to open contains JS generated data you have to use SplashRequest(or SplashFormRequest) to render the page. Here’s a simple example:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class MySpider(scrapy.Spider):
        name ="jsscraper"
        start_urls = ["http://quotes.toscrape.com/js/"]

        def start_requests(self):
            for url in self.start_urls:
            yield SplashRequest(
                url=url, callback=self.parse, endpoint='render.html'
            )

        def parse(self, response):
            for q in response.css("div.quote"):
            quote = QuoteItem()
            quote["author"] = q.css(".author::text").extract_first()
            quote["quote"] = q.css(".text::text").extract_first()
            yield quote

    SplashRequest renders the URL as html and returns the response which you can use in the callback(parse) method.

  • 解决方案2:让我们暂时称呼这个实验(2018年5月)......
    此解决方案仅适用于Python的3.6版本(目前)。

    你知道请求模块(谁没有)?
    现在它有一个网络抓取小兄弟:requests-HTML:

    This library intends to make parsing HTML (e.g. scraping the web) as simple and intuitive as possible.

  • 安装requests-html:pipenv install requests-html

  • 请求页面的网址:

    1
    2
    3
    4
    from requests_html import HTMLSession

    session = HTMLSession()
    r = session.get(a_page_url)
  • 渲染响应以获取Javascript生成的位:

    1
    r.html.render()
  • 最后,该模块似乎提供了抓取功能。
    或者,我们可以尝试将BeautifulSoup与我们刚刚渲染的r.html对象一起使用。


    也许硒可以做到这一点。

    1
    2
    3
    4
    5
    6
    7
    from selenium import webdriver
    import time

    driver = webdriver.Firefox()
    driver.get(url)
    time.sleep(5)
    htmlSource = driver.page_source


    如果您曾经使用过Requests模块用于python,我最近发现开发人员创建了一个名为Requests-HTML的新模块,现在它也能够呈现JavaScript。

    您也可以访问https://html.python-requests.org/了解有关此模块的更多信息,或者如果您只对渲染JavaScript感兴趣,那么您可以访问https://html.python-requests.org/?#javascript - 支持直接学习如何使用模块使用Python呈现JavaScript。

    基本上,一旦您正确安装Requests-HTML模块,以下示例(如上面的链接所示)显示了如何使用此模块来抓取网站并呈现网站中包含的JavaScript:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from requests_html import HTMLSession
    session = HTMLSession()

    r = session.get('http://python-requests.org/')

    r.html.render()

    r.html.search('Python 2 will retire in only {months} months!')['months']

    '<time>25</time>' #This is the result.

    我最近从YouTube视频中了解到了这一点。点击这里!观看YouTube视频,演示该模块的工作原理。


    这似乎也是一个很好的解决方案,来自一篇很棒的博客文章

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    import sys  
    from PyQt4.QtGui import *  
    from PyQt4.QtCore import *  
    from PyQt4.QtWebKit import *  
    from lxml import html

    #Take this class for granted.Just use result of rendering.
    class Render(QWebPage):  
      def __init__(self, url):  
        self.app = QApplication(sys.argv)  
        QWebPage.__init__(self)  
        self.loadFinished.connect(self._loadFinished)  
        self.mainFrame().load(QUrl(url))  
        self.app.exec_()  

      def _loadFinished(self, result):  
        self.frame = self.mainFrame()  
        self.app.quit()  

    url = 'http://pycoders.com/archive/'  
    r = Render(url)  
    result = r.frame.toHtml()
    # This step is important.Converting QString to Ascii for lxml to process

    # The following returns an lxml element tree
    archive_links = html.fromstring(str(result.toAscii()))
    print archive_links

    # The following returns an array containing the URLs
    raw_links = archive_links.xpath('//div[@class="campaign"]/a/@href')
    print raw_links

    听起来您正在寻找的数据可以通过主页面上的某些javascript调用的辅助URL进行访问。

    虽然您可以尝试在服务器上运行javascript来处理这个问题,但更简单的方法可能是使用Firefox加载页面并使用像Charles或Firebug这样的工具来确定辅助URL的确切内容。然后,您可以直接查询该URL,以获取您感兴趣的数据。


    Selenium是抓取JS和Ajax内容的最佳选择。

    查看本文以使用Python从Web中提取数据

    1
    $ pip install selenium

    然后下载Chrome webdriver。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    from selenium import webdriver

    browser = webdriver.Chrome()

    browser.get("https://www.python.org/")

    nav = browser.find_element_by_id("mainnav")

    print(nav.text)

    容易,对吗?


    您也可以使用webdriver执行javascript。

    1
    2
    3
    4
    5
    from selenium import webdriver

    driver = webdriver.Firefox()
    driver.get(url)
    driver.execute_script('document.title')

    或将值存储在变量中

    1
    result = driver.execute_script('var text = document.title ; return var')


    您将在脚本中为页面的不同部分使用urllib,requests,beautifulSoup和selenium web驱动程序(仅举几例)。
    有时您只需使用其中一个模块就能获得所需的功能。
    有时您需要两个,三个或所有这些模块。
    有时您需要关闭浏览器上的js。
    有时您需要在脚本中使用标题信息。
    没有任何网站可以被删除相同的方式,没有网站可以永远以相同的方式被删除,而无需修改您的爬虫,通常几个月后。但他们都可以被刮掉!有遗嘱的地方肯定有办法。
    如果您需要持续不断地将数据写入未来,只需抓取您需要的所有内容并将其存储在带有pickle的.dat文件中。
    只需继续搜索如何使用这些模块进行尝试,并将错误复制并粘贴到Google中。


    我个人更喜欢在单独的容器中使用scrapy和selenium和dockerizing。通过这种方式,您可以安装最小的麻烦和爬行现代网站,几乎所有网站都包含一种或另一种形式的javascript。这是一个例子:

    使用scrapy startproject创建刮刀并编写蜘蛛,骨架可以像这样简单:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import scrapy


    class MySpider(scrapy.Spider):
        name = 'my_spider'
        start_urls = ['https://somewhere.com']

        def start_requests(self):
            yield scrapy.Request(url=self.start_urls[0])


        def parse(self, response):

            # do stuff with results, scrape items etc.
            # now were just checking everything worked

            print(response.body)

    真正的魔法发生在middlewares.py中。通过以下方式覆盖下载器中间件__init__process_request中的两个方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    # import some additional modules that we need
    import os
    from copy import deepcopy
    from time import sleep

    from scrapy import signals
    from scrapy.http import HtmlResponse
    from selenium import webdriver

    class SampleProjectDownloaderMiddleware(object):

    def __init__(self):
        SELENIUM_LOCATION = os.environ.get('SELENIUM_LOCATION', 'NOT_HERE')
        SELENIUM_URL = f'http://{SELENIUM_LOCATION}:4444/wd/hub'
        chrome_options = webdriver.ChromeOptions()

        # chrome_options.add_experimental_option("mobileEmulation", mobile_emulation)
        self.driver = webdriver.Remote(command_executor=SELENIUM_URL,
                                       desired_capabilities=chrome_options.to_capabilities())


    def process_request(self, request, spider):

        self.driver.get(request.url)

        # sleep a bit so the page has time to load
        # or monitor items on page to continue as soon as page ready
        sleep(4)

        # if you need to manipulate the page content like clicking and scrolling, you do it here
        # self.driver.find_element_by_css_selector('.my-class').click()

        # you only need the now properly and completely rendered html from your page to get results
        body = deepcopy(self.driver.page_source)

        # copy the current url in case of redirects
        url = deepcopy(self.driver.current_url)

        return HtmlResponse(url, body=body, encoding='utf-8', request=request)

    不要忘记通过取消注释settings.py文件中的下一行来启用此middlware:

    1
    2
    DOWNLOADER_MIDDLEWARES = {
    'sample_project.middlewares.SampleProjectDownloaderMiddleware': 543,}

    接下来进行码头化。从轻量级图像创建Dockerfile(我在这里使用python Alpine),将项目目录复制到它,安装要求:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # Use an official Python runtime as a parent image
    FROM python:3.6-alpine

    # install some packages necessary to scrapy and then curl because it's  handy for debugging
    RUN apk --update add linux-headers libffi-dev openssl-dev build-base libxslt-dev libxml2-dev curl python-dev

    WORKDIR /my_scraper

    ADD requirements.txt /my_scraper/

    RUN pip install -r requirements.txt

    ADD . /scrapers

    最后在docker-compose.yaml中将它们整合在一起:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    version: '2'
    services:
      selenium:
        image: selenium/standalone-chrome
        ports:
          -"4444:4444"
        shm_size: 1G

      my_scraper:
        build: .
        depends_on:
          -"selenium"
        environment:
          - SELENIUM_LOCATION=samplecrawler_selenium_1
        volumes:
          - .:/my_scraper
        # use this command to keep the container running
        command: tail -f /dev/null

    运行docker-compose up -d。如果您是第一次这样做,它需要一段时间才能获取最新的selenium / standalone-chrome并构建您的刮刀图像。

    完成后,您可以检查容器是否正在运行docker ps,并检查selenium容器的名称是否与传递给我们的scraper容器的环境变量的名称相匹配(此处为SELENIUM_LOCATION=samplecrawler_selenium_1)。

    使用docker exec -ti YOUR_CONTAINER_NAME sh输入您的刮刀容器,对我的命令是docker exec -ti samplecrawler_my_scraper_1 sh,cd进入正确的目录并使用scrapy crawl my_spider运行刮刀。

    整个事情都在我的github页面上,你可以从这里得到它


    BeautifulSoup和Selenium混合使用对我来说非常好。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    from selenium import webdriver
    from selenium.webdriver.common.by import By
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC
    from bs4 import BeautifulSoup as bs

    driver = webdriver.Firefox()
    driver.get("http://somedomain/url_that_delays_loading")
        try:
            element = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.ID,"myDynamicElement"))) #waits 10 seconds until element is located. Can have other wait conditions  such as visibility_of_element_located or text_to_be_present_in_element

            html = driver.page_source
            soup = bs(html,"lxml")
            dynamic_text = soup.find_all("p", {"class":"class_name"}) #or other attributes, optional
        else:
            print("Couldnt locate element")

    附:你可以在这里找到更多的等待条件


    使用PyQt5

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    from PyQt5.QtWidgets import QApplication
    from PyQt5.QtCore import QUrl
    from PyQt5.QtWebEngineWidgets import QWebEnginePage
    import sys
    import bs4 as bs
    import urllib.request


    class Client(QWebEnginePage):
        def __init__(self,url):
            global app
            self.app = QApplication(sys.argv)
            QWebEnginePage.__init__(self)
            self.html =""
            self.loadFinished.connect(self.on_load_finished)
            self.load(QUrl(url))
            self.app.exec_()

        def on_load_finished(self):
            self.html = self.toHtml(self.Callable)
            print("Load Finished")

        def Callable(self,data):
            self.html = data
            self.app.quit()

    #url =""
    #client_response = Client(url)
    #print(client_response.html)