我试图开发一个简单的网页刮板。我想提取没有HTML代码的文本。它适用于普通HTML,但不适用于JavaScript代码添加文本的某些页面。

例如,如果一些JavaScript代码添加了一些文本,我不能看到它,因为当我调用:

response = urllib2.urlopen(request)

我得到了原始文本而没有添加的文本(因为JavaScript是在客户端执行的)。

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


当前回答

我个人更喜欢在单独的容器中使用scrapy和selenium和dockerizing。通过这种方式,你既可以轻松安装,也可以抓取几乎所有包含某种形式javascript的现代网站。这里有一个例子:

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

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)

真正的魔力发生在middleware .py中。重写下载中间件中的两个方法__init__和process_request,方法如下:

# 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文件中取消下一行的注释来启用这个中间件:

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

接下来是dockerization。从一个轻量级映像创建Dockerfile(我在这里使用python Alpine),复制你的项目目录到它,安装要求:

# 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中把所有这些都整合在一起:

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。如果你是第一次这样做,它将需要一段时间来获取最新的硒/独立铬和构建你的刮刀图像以及。

完成后,您可以检查容器是否使用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爬行my_spider运行你的刮板。

所有内容都在我的github页面上,你可以从这里获取

其他回答

Playwright-Python

还有一种选择是剧作家- Python,它是微软剧作家(本身是受木偶大师影响的浏览器自动化库)到Python的移植。

下面是选择一个元素并抓取它的文本的最小示例:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("http://whatsmyuseragent.org/")
    ua = page.query_selector(".user-agent");
    print(ua.text_content())
    browser.close()

这似乎是一个很好的解决方案,从一个伟大的博客文章

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

尝试直接访问API

在抓取中常见的场景是网页从API端点异步请求数据。一个最小的例子是以下网站:

身体< > < >脚本 fetch(“https://jsonplaceholder.typicode.com/posts/1”) .then(res => { if (!res.ok)抛出错误(res.status); 返回res.json (); }) .then(data => { //页面加载后通过JS动态注入数据 document.body.innerText = data.title; }) .catch(err => console.error(err)) ; > < /脚本 身体< / >

在许多情况下,API将受到CORS或访问令牌的保护,或速率限制过高,但在其他情况下,它是公开可访问的,您可以完全绕过网站。对于CORS问题,你可以在任何地方尝试CORS。

一般的过程是使用浏览器的开发人员工具的网络选项卡来搜索页面发出的请求,以获得您想要抓取的数据的关键字/子字符串。通常,您会看到一个不受保护的API请求端点,该端点带有一个JSON有效负载,您可以直接使用urllib或请求模块访问该有效负载。上面的可运行代码片段就是这种情况,你可以用它来练习。点击“运行片段”后,下面是我如何在我的网络选项卡中找到端点:

这个例子是虚构的;从静态标记来看,端点URL可能不明显,因为它可以被动态组装、缩小并隐藏在数十个其他请求和端点之下。网络请求还将显示任何相关的请求有效负载细节,例如您可能需要的访问令牌。

在获取端点URL和相关细节后,使用标准HTTP库在Python中构建一个请求并请求数据:

>>> import requests
>>> res = requests.get("https://jsonplaceholder.typicode.com/posts/1")
>>> data = res.json()
>>> data["title"]
'sunt aut facere repellat provident occaecati excepturi optio reprehenderit'

当你可以摆脱它时,这往往比使用Selenium、Pyppeteer、Scrapy或其他流行的抓取库更容易、更快、更可靠。

如果您很不幸,数据没有通过API请求以良好的格式返回数据,那么它可能是原始浏览器负载的一部分,在<script>标记中,作为JSON字符串或(更可能的是)JS对象。例如:

<body> <script> var someHardcodedData = { 用户 ID: 1, 编号: 1, 题目: “Sunt aut facere repellat provident occaecati excepturi optio reprehenderit”, Body: 'quia et suscipit\nsuscipit recusandae con sequuntur expedita et\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto' }; document.body.textContent = someHardcodedData.title; </script> </body>

没有一种万能的方法来获取这些数据。基本技术是使用BeautifulSoup访问<script>标记文本,然后应用正则表达式或解析来提取对象结构、JSON字符串或数据可能采用的任何格式。下面是上面所示的示例结构的概念证明:

import json
import re
from bs4 import BeautifulSoup

# pretend we've already used requests to retrieve the data, 
# so we hardcode it for the purposes of this example
text = """
<body>
<script>
  var someHardcodedData = {
    userId: 1,
    id: 1,
    title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', 
    body: 'quia et suscipit\nsuscipit recusandae con sequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto'
  };
  document.body.textContent = someHardcodedData.title;
</script>
</body>
"""
soup = BeautifulSoup(text, "lxml")
script_text = str(soup.select_one("script"))
pattern = r"title: '(.*?)'"
print(re.search(pattern, script_text, re.S).group(1))

看看下面这些解析JS对象的资源,它们不是很有效的JSON:

如何将原始javascript对象转换为python字典? 如何修复JSON键值没有双引号?

以下是一些使用API绕过抓取的额外案例研究/概念证明:

如何使用Python beautifulsoup将yelp评论和星级评分刮到CSV Beautiful Soup对现有元素返回None 从BeautifulSoup Python中提取数据 通过POST收集Bandcamp粉丝(使用一种混合方法,即向网站发出初始请求,从使用BeautifulSoup的标记中提取一个令牌,然后在对JSON端点的第二个请求中使用)

如果所有这些都失败了,请尝试本线程中列出的许多动态抓取库中的一个。

我最近使用requests_html库来解决这个问题。

他们的扩展文档在readthedocs。IO非常好(跳过pypi.org上的带注释的版本)。如果您的用例是基本的,那么您可能会取得一些成功。

from requests_html import HTMLSession
session = HTMLSession()
response = session.request(method="get",url="www.google.com/")
response.html.render()

如果你在使用response.html.render()呈现你需要的数据时遇到麻烦,你可以将一些javascript传递给呈现函数来呈现你需要的特定js对象。这是从他们的文档中复制的,但这可能正是你需要的:

如果指定了script,它将在 运行时。例子:

script = """
    () => {
        return {
            width: document.documentElement.clientWidth,
            height: document.documentElement.clientHeight,
            deviceScaleFactor: window.devicePixelRatio,
        }
    } 
"""

返回执行脚本的返回值,如果有的话:

>>> response.html.render(script=script)
{'width': 800, 'height': 600, 'deviceScaleFactor': 1}

In my case, the data I wanted were the arrays that populated a javascript plot but the data wasn't getting rendered as text anywhere in the html. Sometimes its not clear at all what the object names are of the data you want if the data is populated dynamically. If you can't track down the js objects directly from view source or inspect, you can type in "window" followed by ENTER in the debugger console in the browser (Chrome) to pull up a full list of objects rendered by the browser. If you make a few educated guesses about where the data is stored, you might have some luck finding it there. My graph data was under window.view.data in the console, so in the "script" variable passed to the .render() method quoted above, I used:

return {
    data: window.view.data
}

我个人更喜欢在单独的容器中使用scrapy和selenium和dockerizing。通过这种方式,你既可以轻松安装,也可以抓取几乎所有包含某种形式javascript的现代网站。这里有一个例子:

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

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)

真正的魔力发生在middleware .py中。重写下载中间件中的两个方法__init__和process_request,方法如下:

# 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文件中取消下一行的注释来启用这个中间件:

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

接下来是dockerization。从一个轻量级映像创建Dockerfile(我在这里使用python Alpine),复制你的项目目录到它,安装要求:

# 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中把所有这些都整合在一起:

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。如果你是第一次这样做,它将需要一段时间来获取最新的硒/独立铬和构建你的刮刀图像以及。

完成后,您可以检查容器是否使用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爬行my_spider运行你的刮板。

所有内容都在我的github页面上,你可以从这里获取