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后,请确保当前路径中的
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它说:
没有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生成的内容。
运行启动服务器:
安装scrapy-splash插件:
假设我们已经创建了一个Scrapy项目(如果没有,让我们创建一个),我们将按照指南并更新
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'
最后,我们可以使用
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 quoteSplashRequest 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:
请求页面的网址:
1 2 3 4 | from requests_html import HTMLSession session = HTMLSession() r = session.get(a_page_url) |
渲染响应以获取Javascript生成的位:
1 | r.html.render() |
最后,该模块似乎提供了抓取功能。
或者,我们可以尝试将BeautifulSoup与我们刚刚渲染的
也许硒可以做到这一点。
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 |
如果您曾经使用过
您也可以访问https://html.python-requests.org/了解有关此模块的更多信息,或者如果您只对渲染JavaScript感兴趣,那么您可以访问https://html.python-requests.org/?#javascript - 支持直接学习如何使用模块使用Python呈现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。这是一个例子:
使用
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中。通过以下方式覆盖下载器中间件
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,} |
接下来进行码头化。从轻量级图像创建
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 |
最后在
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 |
运行
完成后,您可以检查容器是否正在运行
使用
整个事情都在我的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) |