SPA SEO를 크롤링 할 수있게 만드는 방법은 무엇입니까?


143

Google의 지침 에 따라 Google이 SPA를 크롤링 할 수 있도록 만드는 방법을 연구하고 있습니다. 비록 몇 가지 일반적인 설명이 있지만 실제 예제를 사용하여보다 철저한 단계별 자습서를 찾을 수 없었습니다. 이 작업을 마친 후 다른 사람들이 솔루션을 사용하고 더 향상시킬 수 있도록 솔루션을 공유하고 싶습니다. 컨트롤러 와 함께
사용 하고 서버 측에서 Phantomjs 를 사용 하고 클라이언트 측에서는 Durandal 을 사용합니다. 또한 클라이언트-서버 데이터 상호 작용을 위해 Breezejs 를 사용 합니다.이 모든 것을 강력히 권장하지만, 다른 플랫폼을 사용하는 사람들에게도 도움이 될만한 충분한 설명을 제공하려고 노력할 것입니다.MVCWebapipush-state


40
웹 애플리케이션 프로그래머는 자신의 앱을 SEO 용으로 크롤링 할 수있는 방법을 찾아야합니다. 이는 웹의 기본 요구 사항입니다. 이 작업은 프로그래밍 자체에 관한 것이 아니라 stackoverflow.com/help/on-topic에 설명 된대로 "프로그래밍 직업에 고유 한 실용적이고 응답 가능한 문제"의 주제와 관련이 있습니다. 웹 전체에 명확한 솔루션이없는 많은 프로그래머에게 문제가됩니다. 나는 다른 사람들을 돕기를 바라고 있었고 여기에 설명하는 데 몇 시간을 투자했지만 부정적인 점을 얻는 것이 다시 도움이되지는 않습니다.
beamish

3
뱀 기름 / 비밀 소스 SEO 부두 / 스팸이 아닌 프로그래밍에 중점을 둔다면 그것은 완벽하게 국소 적 일 수 있습니다. 또한 미래 독자들에게 장기적으로 유용 할 수있는 잠재력을 가지고있는 자체 답변을 좋아합니다. 이 질문과 답변 쌍은 두 가지 테스트를 모두 통과 한 것으로 보입니다. (일부 배경 세부 사항은 답변에 소개되는 것보다 질문을 더 잘 해결할 수 있지만 그 정도는 미미합니다)
Flexo

6
+1 투표를 완화합니다. Q / A가 블로그 게시물로 더 적합 할지라도 질문은 Durandal과 관련이 있으며 대답은 잘 연구되었습니다.
RainerAtSpirit

2
SEO는 오늘날 개발자의 일상 생활에서 중요한 부분이며 반드시 stackoverflow의 주제로 간주되어야한다는 데 동의합니다!
Kim D.

전체 프로세스를 직접 구현하는 것 외에도 기본적 으로이 문제를 서비스로 해결하는 SnapSearch snapsearch.io 를 사용해 볼 수 있습니다 .
CMCDragonkai

답변:


121

시작하기 전에, 당신이 구글에서 무엇을 이해하도록하십시오 필요 , 특히 사용을 하고 추한 URL을. 이제 구현을 보자.

고객 입장에서

클라이언트 측에는 AJAX 호출을 통해 서버와 동적으로 상호 작용하는 단일 html 페이지 만 있습니다. 그것이 바로 SPA의 문제입니다. a클라이언트 측의 모든 태그는 내 응용 프로그램에서 동적으로 만들어지며 나중에 서버에서 Google 로봇에 이러한 링크를 표시하는 방법을 살펴 보겠습니다. 이러한 각 a태그의 요구는이 할 수 pretty URLhref구글의 로봇이 크롤링 있도록 태그입니다. 당신은 원하지 않는 href우리가 부하에 새 페이지를 원하지 않을 수 있기 때문에, (우리가 나중에 보자, 서버가 구문 분석 할 수 있도록하려는에도 불구하고) 부분이 그것의 클라이언트 클릭 할 때 사용되는 AJAX 호출을 통해 일부 데이터가 페이지의 일부에 표시되도록하고 자바 스크립트를 통해 (예 : HTML5 사용 pushstate또는 Durandaljs) URL을 변경합니다 . 그래서, 우리는 둘 다hrefonclick사용자가 링크를 클릭 할 때 작업을 수행 할 뿐만 아니라 Google의 속성입니다 . 이제는 URL에 push-state아무것도 원하지 #않으므로 일반적인 a태그는 다음과 같이 보일 수 있습니다.
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>

'category'및 'subCategory'는 아마도 '통신'및 '전화'또는 '컴퓨터'와 같은 다른 문구 일 것입니다. 그리고 가전 제품 상점을위한 '노트북'. 분명히 많은 다른 범주와 하위 범주가있을 것입니다. 보다시피, 링크는과 같은 특정 '스토어'페이지에 대한 추가 매개 변수가 아닌 카테고리, 하위 카테고리 및 제품에 직접 연결됩니다 http://www.xyz.com/store/category/subCategory/product111. 더 짧고 간단한 링크를 선호하기 때문입니다. 내 '페이지'중 하나와 이름이 같은 카테고리가 없습니다. 즉 '
AJAX ( onclick부분) 를 통해 데이터를로드하는 방법에 대해서는 설명하지 않고 Google에서 검색하면 많은 좋은 설명이 있습니다. 여기서 언급하고 싶은 유일한 중요한 점은 사용자가이 링크를 클릭하면 브라우저의 URL이 다음과 같이 표시된다는 것입니다.
http://www.xyz.com/category/subCategory/product111. 그리고 이것은 URL이 서버로 전송되지 않습니다! 이것은 클라이언트와 서버 간의 모든 상호 작용이 AJAX를 통해 이루어지고 전혀 링크가없는 SPA입니다. 모든 '페이지'는 클라이언트 측에서 구현되며 다른 URL은 서버를 호출하지 않습니다 (서버는 이러한 URL을 다른 사이트에서 사이트로 외부 링크로 사용하는 경우 이러한 URL을 처리하는 방법을 알아야합니다. 나중에 서버 측에서 볼 수 있습니다). 이제 이것은 Durandal에 의해 훌륭하게 처리됩니다. 나는 그것을 강력히 추천하지만 다른 기술을 선호한다면이 부분을 건너 뛸 수도 있습니다. 당신이 그것을 선택하고 나처럼 웹용 MS Visual Studio Express 2012를 사용 하고 있다면 Durandal Starter Kit를 설치할 수 있습니다 shell.js.

define(['plugins/router', 'durandal/app'], function (router, app) {
    return {
        router: router,
        activate: function () {
            router.map([
                { route: '', title: 'Store', moduleId: 'viewmodels/store', nav: true },
                { route: 'about', moduleId: 'viewmodels/about', nav: true }
            ])
                .buildNavigationModel()
                .mapUnknownRoutes(function (instruction) {
                    instruction.config.moduleId = 'viewmodels/store';
                    instruction.fragment = instruction.fragment.replace("!/", ""); // for pretty-URLs, '#' already removed because of push-state, only ! remains
                    return instruction;
                });
            return router.activate({ pushState: true });
        }
    };
});

여기에 주목해야 할 몇 가지 중요한 사항이 있습니다.

  1. 첫 번째 경로 ( route:'')는 추가 데이터가없는 URL입니다 (예 :) http://www.xyz.com. 이 페이지에서는 AJAX를 사용하여 일반 데이터를로드합니다. a이 페이지 에는 실제로 태그 가 전혀 없을 수 있습니다 . Google의 봇이 어떻게해야하는지 알 수 있도록 다음 태그를 추가하려고합니다
    <meta name="fragment" content="!">.. 이 태그는 Google의 봇이 www.xyz.com?_escaped_fragment_=나중에 볼 URL을 변환하게합니다 .
  2. '정보'경로는 웹 애플리케이션에서 원하는 다른 '페이지'링크에 대한 예일뿐입니다.
  3. 이제 까다로운 부분은 '범주'경로가 없으며 여러 가지 범주가있을 수 있다는 것입니다. 사전 정의 된 경로가없는 범주는 없습니다. 이것은 mapUnknownRoutes알려지지 않은 경로를 'store'경로에 매핑하고 '!'를 제거합니다. pretty URLGoogle의 검색 엔진 에서 생성 된 경우 URL에서 'store'경로는 'fragment'속성의 정보를 가져와 AJAX 호출을 통해 데이터를 가져 와서 표시하고 URL을 로컬로 변경합니다. 내 응용 프로그램에서는 모든 호출에 대해 다른 페이지를로드하지 않습니다. 이 데이터와 관련된 페이지 부분 만 변경하고 URL을 로컬로 변경합니다.
  4. pushState:trueDurandal이 푸시 상태 URL을 사용하도록 지시 하는 것을 주목하십시오 .

이것이 클라이언트 측에서 필요한 전부입니다. 해시 된 URL로도 구현할 수 있습니다 (Durandal에서는 간단히 제거하십시오 pushState:true). 더 복잡한 부분 (적어도 나를 위해 ...)은 서버 부분이었습니다.

서버 측

내가 사용하고 MVC 4.5있는 서버 측에서 WebAPI컨트롤러. 서버는 실제로 3의 URL 종류 처리해야합니다 : 구글에 의해 생성 된 것들 - 모두 prettyugly또한 클라이언트의 브라우저에 나타나는 것과 동일한 형식의 '간단한'URL. 이 작업을 수행하는 방법을 살펴 보겠습니다.

예쁜 URL과 '간단한'URL은 존재하지 않는 컨트롤러를 참조하려고하는 것처럼 서버에서 먼저 해석됩니다. 서버는 비슷한 것을보고 http://www.xyz.com/category/subCategory/product111'category'라는 컨트롤러를 찾습니다. 그래서 web.config다음 줄을 추가하여 특정 오류 처리 컨트롤러로 리디렉션합니다.

<customErrors mode="On" defaultRedirect="Error">
    <error statusCode="404" redirect="Error" />
</customErrors><br/>

이제 URL을 다음과 같이 변환합니다 http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111. AJAX를 통해 데이터를로드 할 클라이언트로 URL을 전송하고 싶기 때문에 여기서 속임수는 컨트롤러를 참조하지 않는 것처럼 기본 '인덱스'컨트롤러를 호출하는 것입니다. 모든 'category'및 'subCategory'매개 변수 앞에 URL에 해시를 추가 하여 이를 수행합니다 . 해시 된 URL에는 기본 '인덱스'컨트롤러를 제외하고 특수 컨트롤러가 필요하지 않으며 데이터가 클라이언트로 전송 된 다음 해시를 제거하고 해시 다음에 정보를 사용하여 AJAX를 통해 데이터를로드합니다. 오류 처리기 컨트롤러 코드는 다음과 같습니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;

using System.Web.Routing;

namespace eShop.Controllers
{
    public class ErrorController : ApiController
    {
        [HttpGet, HttpPost, HttpPut, HttpDelete, HttpHead, HttpOptions, AcceptVerbs("PATCH"), AllowAnonymous]
        public HttpResponseMessage Handle404()
        {
            string [] parts = Request.RequestUri.OriginalString.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries);
            string parameters = parts[ 1 ].Replace("aspxerrorpath=","");
            var response = Request.CreateResponse(HttpStatusCode.Redirect);
            response.Headers.Location = new Uri(parts[0].Replace("Error","") + string.Format("#{0}", parameters));
            return response;
        }
    }
}


그러나 추악한 URL은 어떻습니까? 이들은 구글의 봇에 의해 생성되며 사용자가 브라우저에서 보는 모든 데이터를 포함하는 일반 HTML을 반환해야합니다. 이를 위해 나는 phantomjs를 사용 합니다 . Phantom은 브라우저가 클라이언트 쪽에서 서버 쪽에서 수행하는 작업을 수행하는 헤드리스 브라우저입니다. 다시 말해, 팬텀은 URL을 통해 웹 페이지를 가져 오는 방법과 모든 자바 스크립트 코드 실행 (AJAX 호출을 통한 데이터 가져 오기 포함)을 분석하고 반영하는 HTML을 제공하는 방법을 알고 있습니다. DOM. MS Visual Studio Express를 사용하는 경우 많은 사람들이이 링크 를 통해 팬텀을 설치하려고합니다 .
그러나 먼저 못생긴 URL이 서버로 전송 될 때이를 파악해야합니다. 이를 위해 다음 파일을 'App_start'폴더에 추가했습니다.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace eShop.App_Start
{
    public class AjaxCrawlableAttribute : ActionFilterAttribute
    {
        private const string Fragment = "_escaped_fragment_";

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var request = filterContext.RequestContext.HttpContext.Request;

            if (request.QueryString[Fragment] != null)
            {

                var url = request.Url.ToString().Replace("?_escaped_fragment_=", "#");

                filterContext.Result = new RedirectToRouteResult(
                    new RouteValueDictionary { { "controller", "HtmlSnapshot" }, { "action", "returnHTML" }, { "url", url } });
            }
            return;
        }
    }
}

이것은 'App_start'의 'filterConfig.cs'에서도 호출됩니다.

using System.Web.Mvc;
using eShop.App_Start;

namespace eShop
{
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            filters.Add(new AjaxCrawlableAttribute());
        }
    }
}

보시다시피, 'AjaxCrawlableAttribute'는 추악한 URL을 'HtmlSnapshot'이라는 컨트롤러로 라우팅하며이 컨트롤러는 다음과 같습니다.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace eShop.Controllers
{
    public class HtmlSnapshotController : Controller
    {
        public ActionResult returnHTML(string url)
        {
            string appRoot = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);

            var startInfo = new ProcessStartInfo
            {
                Arguments = String.Format("{0} {1}", Path.Combine(appRoot, "seo\\createSnapshot.js"), url),
                FileName = Path.Combine(appRoot, "bin\\phantomjs.exe"),
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                RedirectStandardInput = true,
                StandardOutputEncoding = System.Text.Encoding.UTF8
            };
            var p = new Process();
            p.StartInfo = startInfo;
            p.Start();
            string output = p.StandardOutput.ReadToEnd();
            p.WaitForExit();
            ViewData["result"] = output;
            return View();
        }

    }
}

관련 내용 view은 한 줄의 코드로 매우 간단
@Html.Raw( ViewBag.result )
합니다. 컨트롤러에서 볼 수 있듯이 phantom createSnapshot.js은 내가 만든 폴더 아래에 이름이 지정된 자바 스크립트 파일을로드합니다 seo. 이 자바 스크립트 파일은 다음과 같습니다.

var page = require('webpage').create();
var system = require('system');

var lastReceived = new Date().getTime();
var requestCount = 0;
var responseCount = 0;
var requestIds = [];
var startTime = new Date().getTime();

page.onResourceReceived = function (response) {
    if (requestIds.indexOf(response.id) !== -1) {
        lastReceived = new Date().getTime();
        responseCount++;
        requestIds[requestIds.indexOf(response.id)] = null;
    }
};
page.onResourceRequested = function (request) {
    if (requestIds.indexOf(request.id) === -1) {
        requestIds.push(request.id);
        requestCount++;
    }
};

function checkLoaded() {
    return page.evaluate(function () {
        return document.all["compositionComplete"];
    }) != null;
}
// Open the page
page.open(system.args[1], function () { });

var checkComplete = function () {
    // We don't allow it to take longer than 5 seconds but
    // don't return until all requests are finished
    if ((new Date().getTime() - lastReceived > 300 && requestCount === responseCount) || new Date().getTime() - startTime > 10000 || checkLoaded()) {
        clearInterval(checkCompleteInterval);
        var result = page.content;
        //result = result.substring(0, 10000);
        console.log(result);
        //console.log(results);
        phantom.exit();
    }
}
// Let us check to see if the page is finished rendering
var checkCompleteInterval = setInterval(checkComplete, 300);

먼저 :-)에서 기본 코드를 얻은 페이지 에 대해 Thomas Davis 에게 감사드립니다 .
여기서 이상한 점을 알 수 있습니다. 팬텀은 checkLoaded()함수가 true를 반환 할 때까지 페이지를 계속 다시로드 합니다. 왜 그런 겁니까? 내 특정 SPA가 여러 데이터를 가져 와서 내 페이지의 DOM에 배치하기 위해 여러 AJAX 호출을 수행하고 팬텀은 DOM의 HTML 반영을 다시 반환하기 전에 모든 호출이 완료된 시점을 알 수 없기 때문입니다. 내가 여기에서 한 일은 마지막 AJAX 호출 후입니다. <span id='compositionComplete'></span>이 태그가 있으면 DOM이 완료되었음을 알 수 있습니다. Durandal의 compositionComplete이벤트 에 대한 응답으로이 작업을 수행합니다. 여기를 참조 하십시오.이상. 이것이 10 초 안에 일어나지 않으면 포기합니다 (최대 1 초가 걸립니다). 반환 된 HTML에는 사용자가 브라우저에서 볼 수있는 모든 링크가 포함되어 있습니다. <script>HTML 스냅 샷에 존재 하는 태그가 올바른 URL을 참조하지 않기 때문에 스크립트가 제대로 작동 하지 않습니다. 이것은 자바 스크립트 팬텀 파일에서도 변경 될 수 있지만 HTML snapshort는 a링크 를 가져 오고 자바 스크립트를 실행하지 않기 위해 Google에서만 사용하기 때문에 이것이 불필요하다고 생각하지 않습니다 . 이 링크 예쁜 URL을 참조하며, 사실 브라우저에서 HTML 스냅 샷을 보려고하면 자바 스크립트 오류가 발생하지만 모든 링크가 제대로 작동하고 이번에는 예쁜 URL로 서버로 다시 연결됩니다. 완전히 작동하는 페이지를 얻는 중입니다.
이거 야. 이제 서버는 서버와 클라이언트 모두에서 푸시 상태를 활성화하여 예쁘고 못생긴 URL을 처리하는 방법을 알고 있습니다. 모든 추악한 URL은 팬텀을 사용하여 동일한 방식으로 처리되므로 각 유형의 통화에 대해 별도의 컨트롤러를 만들 필요가 없습니다.
변경하려는 것을 선호하는 한 가지는 일반적인 '카테고리 / 서브 카테고리 / 제품'호출을하는 것이 아니라 링크를 다음과 같이 보이도록 '저장소'를 추가하는 것 http://www.xyz.com/store/category/subCategory/product111입니다. 이것은 내 솔루션에서 모든 유효하지 않은 URL이 실제로 '인덱스'컨트롤러를 호출하는 것처럼 취급된다는 문제를 피할 수 있으며 web.config위에서 언급 한 것 외에도 '저장소'컨트롤러 내에서 처리 할 수 ​​있다고 가정합니다. .


빠른 질문이 있습니다. ive가 지금 작동한다고 생각하지만 내 사이트를 Google에 제출하고 Google, 사이트 맵 등에 대한 링크를 제공하면 Google에 mysite.com/# 을 제공해야합니다 ! 또는 mysite.com 과 Google이 메타 태그에 이스케이프 처리 되어 escaped_fragment 를 추가 합니까?
ccorrin

ccorrin-내 지식을 최대한 활용하여 Google에 아무것도 제공 할 필요가 없습니다. Google 봇은 사이트를 찾아 사이트에서 예쁜 URL을 찾습니다 (URL을 포함하지 않을 수 있으므로 메타 태그를 추가하는 홈 페이지를 잊지 마십시오). escaped_fragment를 포함하는 못생긴 URL은 항상 Google에 의해서만 추가됩니다. HTML 안에 절대 넣어서는 안됩니다. 및 지원 :-) 주셔서 감사합니다
beamish

고마워요 Bjorn & Sandra :-)이 문서의 더 나은 버전을 작업 중입니다. 여기에는 페이지를 캐시하여 처리 속도를 높이고 URL에 포함되는 일반적인 용도로 페이지를 캐시하는 방법에 대한 정보도 포함되어 있습니다. 컨트롤러 이름; 준비가 되 자마자 게시하겠습니다
2012 년

이것은 훌륭한 설명입니다!. 나는 그것을 구현하고 내 로컬 호스트 devbox의 매력처럼 작동합니다. 문제는 사이트가 정지되어 얼마 후 502 오류가 발생하기 때문에 Azure 웹 사이트에 배포 할 때 발생합니다. phantomjs를 Azure에 배포하는 방법에 대한 아이디어가 있습니까? ... 감사합니다 ( testypv.azurewebsites.net/?_escaped_fragment_=home/about )
yagopv

Azure 웹 사이트에 대한 경험이 없지만 페이지가 완전히로드되는 검사 프로세스가 수행되지 않으므로 서버가 페이지를 다시로드하지 않고 계속해서 다시로드하려고 시도한다는 것입니다. 아마도 문제가있는 곳 일 것입니다 (이러한 검사에 시간 제한이있어서 거기에 없을 수도 있음)? '돌아 가기'를 시도하십시오. 'checkLoaded ()'의 첫 번째 줄로 차이가 있는지 확인하십시오.
beamish


4

다음은 8 월 14 일 런던에서 주최 한 Ember.js Training 클래스의 스크린 캐스트 녹화 링크입니다. 클라이언트 측 애플리케이션과 서버 측 애플리케이션 모두에 대한 전략을 설명하고, 이러한 기능을 구현하여 JavaScript 단일 페이지 앱에 JavaScript를 사용하지 않는 사용자에게도 정상적인 성능 저하를 제공하는 방법에 대한 실시간 데모를 제공합니다. .

PhantomJS를 사용하여 웹 사이트를 크롤링합니다.

간단히 말해서 필요한 단계는 다음과 같습니다.

  • 크롤링하려는 웹 응용 프로그램의 호스팅 된 버전이 있어야합니다.이 사이트에는 프로덕션 환경에있는 모든 데이터가 있어야합니다.
  • 웹 사이트를로드 할 JavaScript 응용 프로그램 (PhantomJS 스크립트)을 작성하십시오
  • 크롤링 할 URL 목록에 index.html (또는“/“) 추가
    • 크롤링 목록에 추가 된 첫 번째 URL을 팝
    • 페이지로드 및 DOM 렌더링
    • 로드 된 페이지에서 자신의 사이트로 연결되는 링크를 찾으십시오 (URL 필터링).
    • 크롤링 할 수없는 "크롤링 가능"URL 목록에이 링크를 추가하십시오.
    • 렌더링 된 DOM을 파일 시스템의 파일에 저장하지만 먼저 모든 스크립트 태그를 제거하십시오.
    • 마지막으로 크롤링 된 URL로 Sitemap.xml 파일을 작성하십시오.

이 단계가 완료되면 해당 페이지의 noscript-tag의 일부로 HTML의 정적 버전을 제공하기 위해 백엔드까지 수행합니다. 이렇게하면 앱이 원래 단일 페이지 앱인 경우에도 Google 및 기타 검색 엔진이 웹 사이트의 모든 단일 페이지를 크롤링 할 수 있습니다.

자세한 내용이있는 스크린 캐스트 링크 :

http://www.devcasts.io/p/spas-phantomjs-and-seo/#


0

프리 렌더라는 서비스를 사용하여 SPA를 프리 렌더링하기위한 고유 한 서비스를 사용하거나 작성할 수 있습니다. 그의 웹 사이트 prerender.io 및 그의 github 프로젝트 에서 확인할 수 있습니다 (PhantomJS를 사용 하고 웹 사이트를 렌더링 합니다).

시작하기가 매우 쉽습니다. 크롤러 요청을 서비스로 리디렉션하기 만하면 렌더링 된 HTML이 수신됩니다.


2
이 링크가 질문에 대한 답변을 제공 할 수 있지만 여기에 답변의 필수 부분을 포함시키고 참조 용 링크를 제공하는 것이 좋습니다. 링크 된 페이지가 변경되면 링크 전용 답변이 유효하지 않을 수 있습니다. - 검토에서
timgeb

2
네 말이 맞아 내 의견을 업데이트했습니다. 이제 더 정확한 내용이 되길 바랍니다.
gabrielperales

0

당신은 사용할 수 있습니다 http://sparender.com/ 제대로 크롤링하는 단일 페이지 응용 프로그램을 가능하게하는합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.