使用Python的web抓取JavaScript页面

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

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

response = urllib2.urlopen(request)

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

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

431664 次浏览

听起来好像你真正要找的数据可以通过主页面上的一些javascript调用的辅助URL访问。

虽然您可以尝试在服务器上运行javascript来处理这个问题,但更简单的方法可能是使用Firefox加载页面,并使用查尔斯Firebug之类的工具来准确识别辅助URL是什么。然后,您可以直接查询该URL以获得您感兴趣的数据。

EDIT 2021年9月:phantomjs也不再维护

EDIT 30/Dec/2017:这个答案出现在谷歌搜索的顶部结果中,所以我决定更新它。老答案仍然在最后。

dryscape不再维护,开发人员推荐的库dryscape仅适用于Python 2。我发现使用Selenium的python库和Phantom JS作为web驱动程序足够快,也很容易完成工作。

一旦你安装了幻影JS,确保phantomjs二进制文件在当前路径中可用:

phantomjs --version
# result:
2.1.1
< p > #的例子 为了给出一个例子,我用下面的HTML代码创建了一个示例页面。(链接): < / p >
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Javascript scraping test</title>
</head>
<body>
<p id='intro-text'>No javascript support</p>
<script>
document.getElementById('intro-text').innerHTML = 'Yay! Supports javascript';
</script>
</body>
</html>

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

#抓取没有JS支持:

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支持:

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支持:

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>

这似乎也是一个很好的解决方案,从很棒的博文

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

也许可以做到。

from selenium import webdriver
import time


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

你也可以使用webdriver执行javascript。

from selenium import webdriver


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

或者将值存储在变量中

result = driver.execute_script('var text = document.title ; return text')
你会想在你的脚本中使用urllib, requests, beautifulSoup和selenium web驱动程序来处理页面的不同部分(举几个例子) 有时候你只需要这些模块中的一个就可以得到你所需要的 有时你会需要两个、三个或所有这些模块 有时候你需要关闭浏览器上的js 有时候你在脚本中需要头信息 没有一个网站可以以同样的方式被抓取,也没有一个网站可以永远以同样的方式被抓取,而不必修改你的爬虫程序,通常需要几个月的时间。但它们都可以被刮!有志者事竟成
如果你需要不断地抓取数据到未来,只需抓取所有你需要的数据并将其存储在.dat文件中,使用pickle.
只要继续搜索如何尝试这些模块,并复制和粘贴您的错误到谷歌.

Selenium是抓取JS和Ajax内容的最佳工具。

查看本文中的使用Python从网络中提取数据

$ pip install selenium

然后下载Chrome webdriver。

from selenium import webdriver


browser = webdriver.Chrome()


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


nav = browser.find_element_by_id("mainnav")


print(nav.text)

容易,对吧?

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

你也可以访问https://html.python-requests.org/来了解关于这个模块的更多信息,或者如果你只对呈现JavaScript感兴趣,那么你可以访问https://html.python-requests.org/?#javascript-support来直接学习如何使用Python使用模块来呈现JavaScript。

本质上,一旦你正确安装了Requests-HTML模块,下面的例子,也就是如上图所示,展示了你如何使用这个模块来抓取一个网站,并呈现网站中包含的JavaScript:

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视频,它演示了模块如何工作。

把BeautifulSoup和Selenium混合在一起对我来说效果很好。

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")

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

我个人更喜欢在单独的容器中使用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运行的,还可以检查硒容器的名称是否与我们传递给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页面上,你可以从在这里得到它

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

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

由于selenium已经在本线程中多次提到(有时也提到了它的速度有多慢),我将列出其他两种可能的解决方案。


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

我们需要:

  1. Docker安装在我们的机器。与其他解决方案相比,这是一个优势,因为它利用了一个独立于操作系统的平台。

  2. 安装启动遵循为我们相应的操作系统列出的指令。
    引用splash文档:

    Splash是一个javascript渲染服务。它是一个轻量级的web浏览器,带有一个HTTP API,在Python 3中使用Twisted和QT5实现。

    本质上,我们将使用Splash来渲染Javascript生成的内容

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

  4. 安装scrapy-splash plugin: pip install scrapy-splash

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

    然后转到你的scrapy项目的settings.py并设置这些中间件:

    DOWNLOADER_MIDDLEWARES = {
    'scrapy_splash.SplashCookiesMiddleware': 723,
    'scrapy_splash.SplashMiddleware': 725,
    'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
    }
    

    Splash服务器的URL(如果你使用Win或OSX,这应该是docker机器的URL: 如何从主机获取Docker容器的IP地址?):

    SPLASH_URL = 'http://localhost:8050'
    

    最后你还需要设置这些值:

    DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'
    HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'
    
    李< /引用> < / >
  6. 最后,我们可以使用SplashRequest:

    在普通的爬行器中,您可以使用Request对象打开url。如果你想打开的页面包含JS生成的数据,你必须使用SplashRequest(或SplashFormRequest)来呈现页面。这里有一个简单的例子:

    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将URL呈现为html并返回可以在回调(解析)方法中使用的响应。

    李< /引用> < / >

我们暂且称这是实验性的(2018年5月) 此解决方案仅适用于Python版本3.6(目前)。

你知道请求模块吗(好吧,谁不知道)?< br > 现在它有了一个网络爬行的小兄弟:requests-HTML:

这个库旨在使解析HTML(例如抓取网页)尽可能简单和直观。

  1. 安装requests-html: pipenv install requests-html

  2. 向页面的url请求:

    from requests_html import HTMLSession
    
    
    session = HTMLSession()
    r = session.get(a_page_url)
    
  3. Render the response to get the Javascript generated bits:

    r.html.render()
    

Finally, the module seems to offer scraping capabilities.
Alternatively, we can try the well-documented way of using BeautifulSoup with the r.html object we just rendered.

使用PyQt5

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)

如前所述,Selenium是呈现JavaScript结果的好选择:

from selenium.webdriver import Firefox
from selenium.webdriver.firefox.options import Options


options = Options()
options.headless = True
browser = Firefox(executable_path="/usr/local/bin/geckodriver", options=options)


url = "https://www.example.com"
browser.get(url)

西班牙凉菜汤是一个非常容易解析渲染html的库:

from gazpacho import Soup


soup = Soup(browser.page_source)
soup.find("a").attrs['href']

我最近使用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,它将在 运行时。例子:< / p >
script = """
() => {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
deviceScaleFactor: window.devicePixelRatio,
}
}
"""

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

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

在我的例子中,我想要的数据是填充javascript图的数组,但数据没有在html中呈现为文本。如果数据是动态填充的,有时根本不清楚你想要的数据的对象名称是什么。如果你不能直接从view source或inspect中追踪js对象,你可以输入“;window"然后在浏览器(Chrome)的调试器控制台中输入,以调出由浏览器呈现的完整对象列表。如果您对数据存储在哪里做了一些有根据的猜测,您可能会幸运地在那里找到它。我的图形数据在控制台的window.view.data下,因此在“;script"变量传递给上面引用的.render()方法,我使用:

return {
data: window.view.data
}

尝试直接访问API

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

<body>
<script>
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then(res => {
if (!res.ok) throw Error(res.status);
    

return res.json();
})
.then(data => {
// inject data dynamically via JS after page load
document.body.innerText = data.title;
})
.catch(err => console.error(err))
;
</script>
</body>

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

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

示例网络选项卡显示远程URL端点发现与搜索

这个例子是虚构的;从静态标记来看,端点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 = {
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>

没有一种万能的方法来获取这些数据。基本技术是使用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:

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

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

Pyppeteer

你可以考虑Pyppeteer,它是Chrome/Chromium驱动程序前端傀儡师的Python端口。

下面是一个简单的例子,展示了如何使用pyppeterer动态地访问被注入到页面中的数据:

import asyncio
from pyppeteer import launch


async def main():
browser = await launch({"headless": True})
[page] = await browser.pages()


# normally, you go to a live site...
#await page.goto("http://www.example.com")
# but for this example, just set the HTML directly:
await page.setContent("""
<body>
<script>
// inject content dynamically with JS, not part of the static HTML!
document.body.innerHTML = `<p>hello world</p>`;
</script>
</body>
""")
print(await page.content()) # shows that the `<p>` was inserted


# evaluate a JS expression in browser context and scrape the data
expr = "document.querySelector('p').textContent"
print(await page.evaluate(expr, force_expr=True)) # => hello world


await browser.close()


asyncio.get_event_loop().run_until_complete(main())

看到皮皮提尔的参考文件

简单快捷的解决方案:

我也遇到过同样的问题。我想刮一些数据是用JavaScript构建的。如果我只用BeautifulSoup从这个网站抓取文本,那么我就以文本中的标签结束。 我想渲染这个标签,并将从中抓取信息。 此外,我不想使用像Scrapy和selenium这样的沉重框架

所以,我发现得到方法请求模块接受url,它实际上呈现脚本标签。

例子:

import requests
custom_User_agent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"
url = "https://www.abc.xyz/your/url"
response = requests.get(url, headers={"User-Agent": custom_User_agent})
html_text = response.text

这将呈现加载站点和呈现标签。

希望这将有助于作为快速和简单的解决方案,渲染网站加载脚本标签。

Playwright-Python

还有一个选项是playwright-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()