From d952db8ef8fe3bf5a418b9b156c0e19c2a899cd1 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 12 Apr 2026 17:59:00 +0200 Subject: [PATCH 01/12] feat(browsers): add a new feature to block ads This is working by aborting all requests to known ads domains. --- scrapling/cli.py | 11 + scrapling/core/_shell_signatures.py | 2 + scrapling/engines/_browsers/_types.py | 1 + scrapling/engines/_browsers/_validators.py | 9 + scrapling/engines/toolbelt/ad_domains.py | 3537 ++++++++++++++++++++ scrapling/engines/toolbelt/navigation.py | 29 +- scrapling/fetchers/chrome.py | 2 + scrapling/fetchers/stealth_chrome.py | 2 + tests/fetchers/test_utils.py | 126 +- 9 files changed, 3688 insertions(+), 31 deletions(-) create mode 100644 scrapling/engines/toolbelt/ad_domains.py diff --git a/scrapling/cli.py b/scrapling/cli.py index 8ecee2d..9ca0dd4 100644 --- a/scrapling/cli.py +++ b/scrapling/cli.py @@ -309,6 +309,11 @@ def _common_browser_options(f): default=True, help="Run browser in headless mode (default: True)", ), + option( + "--block-ads/--no-block-ads", + default=False, + help="Block requests to known ad and tracker domains (default: False)", + ), ] for decorator in decorators: f = decorator(f) @@ -498,6 +503,7 @@ def __build_browser_kwargs( real_chrome, proxy, parsed_headers, + block_ads, ) -> Dict[str, Any]: """Build shared kwargs dict for browser-based commands.""" kwargs: Dict[str, Any] = { @@ -507,6 +513,7 @@ def __build_browser_kwargs( "timeout": timeout, "locale": locale, "real_chrome": real_chrome, + "block_ads": block_ads, } if wait > 0: kwargs["wait"] = wait @@ -538,6 +545,7 @@ def fetch( proxy, extra_headers, ai_targeted, + block_ads, ): """Opens up a browser and fetch content using DynamicFetcher.""" parsed_headers, _ = _ParseHeaders(extra_headers, False) @@ -552,6 +560,7 @@ def fetch( real_chrome, proxy, parsed_headers, + block_ads, ) from scrapling.fetchers import DynamicFetcher @@ -597,6 +606,7 @@ def stealthy_fetch( allow_webgl, hide_canvas, ai_targeted, + block_ads, ): """Opens up a browser with advanced stealth features and fetch content using StealthyFetcher.""" parsed_headers, _ = _ParseHeaders(extra_headers, False) @@ -611,6 +621,7 @@ def stealthy_fetch( real_chrome, proxy, parsed_headers, + block_ads, ) kwargs.update( { diff --git a/scrapling/core/_shell_signatures.py b/scrapling/core/_shell_signatures.py index 9778318..ce509b6 100644 --- a/scrapling/core/_shell_signatures.py +++ b/scrapling/core/_shell_signatures.py @@ -58,6 +58,7 @@ _FETCH_PARAMS = { "cdp_url": Optional[str], "useragent": Optional[str], "extra_flags": Optional[List[str]], + "block_ads": bool, } _STEALTHY_FETCH_PARAMS = { @@ -84,6 +85,7 @@ _STEALTHY_FETCH_PARAMS = { "cdp_url": Optional[str], "useragent": Optional[str], "extra_flags": Optional[List[str]], + "block_ads": bool, "allow_webgl": bool, "hide_canvas": bool, "block_webrtc": bool, diff --git a/scrapling/engines/_browsers/_types.py b/scrapling/engines/_browsers/_types.py index 2741407..932b2e2 100644 --- a/scrapling/engines/_browsers/_types.py +++ b/scrapling/engines/_browsers/_types.py @@ -88,6 +88,7 @@ class PlaywrightSession(TypedDict, total=False): useragent: Optional[str] extra_flags: Optional[List[str]] blocked_domains: Optional[Set[str]] + block_ads: bool retries: int retry_delay: int | float capture_xhr: str | None diff --git a/scrapling/engines/_browsers/_validators.py b/scrapling/engines/_browsers/_validators.py index 69b3766..8d47061 100644 --- a/scrapling/engines/_browsers/_validators.py +++ b/scrapling/engines/_browsers/_validators.py @@ -85,6 +85,7 @@ class PlaywrightConfig(Struct, kw_only=True, frozen=False, weakref=True): useragent: Optional[str] = None extra_flags: Optional[List[str]] = None blocked_domains: Optional[Set[str]] = None + block_ads: bool = False retries: RetriesCount = 3 retry_delay: Seconds = 1 capture_xhr: str | None = None @@ -127,6 +128,14 @@ class PlaywrightConfig(Struct, kw_only=True, frozen=False, weakref=True): if validation_msg: raise ValueError(validation_msg) + if self.block_ads: + from scrapling.engines.toolbelt.ad_domains import AD_DOMAINS + + if self.blocked_domains: + self.blocked_domains = self.blocked_domains | set(AD_DOMAINS) + else: + self.blocked_domains = set(AD_DOMAINS) + class StealthConfig(PlaywrightConfig, kw_only=True, frozen=False, weakref=True): allow_webgl: bool = True diff --git a/scrapling/engines/toolbelt/ad_domains.py b/scrapling/engines/toolbelt/ad_domains.py new file mode 100644 index 0000000..026a936 --- /dev/null +++ b/scrapling/engines/toolbelt/ad_domains.py @@ -0,0 +1,3537 @@ +""" +Built-in ad/tracker domain list for use with block_ads=True. + +Source: Peter Lowe's ad and tracking server list https://pgl.yoyo.org/adservers/ +Used config: https://pgl.yoyo.org/adservers/serverlist.php?hostformat=plain&showintro=0&startyear=2000&mimetype=plaintext +""" + +AD_DOMAINS: frozenset = frozenset( + { + "101com.com", + "180hits.de", + "180searchassistant.com", + "1rx.io", + "2020mustang.com", + "207.net", + "247media.com", + "24log.com", + "24pm-affiliation.com", + "2linkpath.com", + "2mdn.net", + "2o7.net", + "2znp09oa.com", + "30ads.com", + "3337723.com", + "33across.com", + "360yield.com", + "3lift.com", + "3o9s.short.gy", + "4d5.net", + "4info.com", + "4jnzhl0d0.com", + "50websads.com", + "518ad.com", + "6sc.co", + "777partner.com", + "77tracking.com", + "7bpeople.com", + "7cnq.net", + "82o9v830.com", + "a-ads.com", + "a.mktw.net", + "a.muloqot.uz", + "a.sakh.com", + "a.ucoz.net", + "a.ucoz.ru", + "a.vartoken.com", + "a.vdo.ai", + "a.vfghd.com", + "a.vfgtb.com", + "a.xanga.com", + "a11.click", + "a135.wftv.com", + "a5.overclockers.ua", + "aa-metrics.beauty.hotpepper.jp", + "aa-metrics.recruit-card.jp", + "aa-metrics.trip-ai.jp", + "aaddzz.com", + "aax-eu-dub.amazon.com", + "aaxads.com", + "abacho.net", + "abc-ads.com", + "ablink.comms.trainline.com", + "ablink.info.wise.com", + "ablink.news.emails-puregym.com", + "ablinks.mail.hinge.co", + "aboardlevel.com", + "absolutering.com", + "absorbingband.com", + "abstractedauthority.com", + "abtasty.com", + "ac.rnm.ca", + "accountsdoor.com", + "acebunny.com", + "acemlnb.com", + "acobt.tech", + "acridtwist.com", + "actionsplash.com", + "actonsoftware.com", + "actualdeals.com", + "actuallysheep.com", + "actuallysnake.com", + "acuityads.com", + "acuityplatform.com", + "acustomizedgift.com", + "ad-balancer.at", + "ad-balancer.net", + "ad-cupid.com", + "ad-delivery.net", + "ad-pay.de", + "ad-rotator.com", + "ad-score.com", + "ad-server.gulasidorna.se", + "ad-space.net", + "ad-up.com", + "ad.71i.de", + "ad.a8.net", + "ad.abcnews.com", + "ad.abctv.com", + "ad.aboutwebservices.com", + "ad.abum.com", + "ad.admitad.com", + "ad.allboxing.ru", + "ad.altervista.org", + "ad.amgdgt.com", + "ad.anuntis.com", + "ad.auditude.com", + "ad.bitmedia.io", + "ad.bizo.com", + "ad.bondage.com", + "ad.centrum.cz", + "ad.cgi.cz", + "ad.choiceradio.com", + "ad.cooks.com", + "ad.digitallook.com", + "ad.dnoticias.pt", + "ad.domainfactory.de", + "ad.exyws.org", + "ad.grafika.cz", + "ad.gt", + "ad.hbv.de", + "ad.hyena.cz", + "ad.iinfo.cz", + "ad.infoseek.com", + "ad.intl.xiaomi.com", + "ad.jetsoftware.com", + "ad.keenspace.com", + "ad.lgappstv.com", + "ad.liveinternet.ru", + "ad.lupa.cz", + "ad.mediastorm.hu", + "ad.mg", + "ad.missena.io", + "ad.musicmatch.com", + "ad.myapple.pl", + "ad.mynetreklam.com.streamprovider.net", + "ad.nachtagenten.de", + "ad.nettvservices.com", + "ad.nttnavi.co.jp", + "ad.nwt.cz", + "ad.period-calendar.com", + "ad.profiwin.de", + "ad.prv.pl", + "ad.reachlocal.com", + "ad.simgames.net", + "ad.style", + "ad.technoratimedia.com", + "ad.tv2.no", + "ad.universcine.com", + "ad.usatoday.com", + "ad.virtual-nights.com", + "ad.wavu.hu", + "ad.weatherbug.com", + "ad.wsod.com", + "ad.wz.cz", + "ad.xiaomi.com", + "ad.xmovies8.si", + "ad.xrea.com", + "ad.ztylez.com", + "ad0.bigmir.net", + "ad01.mediacorpsingapore.com", + "ad1.emule-project.org", + "ad1.kde.cz", + "ad2.iinfo.cz", + "ad2.lupa.cz", + "ad2.netriota.hu", + "ad2.nmm.de", + "ad2.xrea.com", + "ad3.iinfo.cz", + "ad3.xrea.com", + "ad4game.com", + "ad4mat.com", + "ad4mat.de", + "ad4mat.net", + "adabra.com", + "adaction.de", + "adadvisor.net", + "adalliance.io", + "adap.tv", + "adapt.tv", + "adbilty.me", + "adblade.com", + "adblade.org", + "adblockanalytics.com", + "adbooth.net", + "adbot.com", + "adbrite.com", + "adbroker.de", + "adbutler.com", + "adbuyer3.lycos.com", + "adcampo.com", + "adcannyads.com", + "adcash.com", + "adcast.deviantart.com", + "adcel.co", + "adcell.de", + "adcenter.net", + "adclick.com", + "adclient1.tucows.com", + "adclixx.net", + "adcolony.com", + "adcomplete.com", + "adconion.com", + "adcontent.gamespy.com", + "adcovery.com", + "adcycle.com", + "add.newmedia.cz", + "addfreestats.com", + "addme.com", + "addressfriend.com", + "ade.clmbtech.com", + "adecn.com", + "adeimptrck.com", + "ademails.com", + "adengage.com", + "adetracking.com", + "adexchangegate.com", + "adexchangeprediction.com", + "adexpose.com", + "adext.inkclub.com", + "adf.ly", + "adfeed.marchex.com", + "adflight.com", + "adforce.com", + "adform.com", + "adform.net", + "adformdsp.net", + "adhaven.com", + "adhese.be", + "adhese.com", + "adhigh.net", + "adhub.media", + "adhunter.media", + "adimage.guardian.co.uk", + "adimages.been.com", + "adimages.carsoup.com", + "adimages.go.com", + "adimages.homestore.com", + "adimages.omroepzeeland.nl", + "adimages.sanomawsoy.fi", + "adimg.com.com", + "adimg.uimserv.net", + "adimg1.chosun.com", + "adimgs.sapo.pt", + "adingo.jp", + "adinjector.net", + "adinterax.com", + "adisfy.com", + "adition.com", + "adition.de", + "adition.net", + "adizio.com", + "adjix.com", + "adjug.com", + "adjuggler.com", + "adjuggler.yourdictionary.com", + "adjust.com", + "adjustnetwork.com", + "adk2.com", + "adland.ru", + "adlegend.com", + "adlightning.com", + "adlog.com.com", + "adloox.com", + "adlooxtracking.com", + "adlure.net", + "adm.fwmrm.net", + "admailtiser.com", + "adman.gr", + "adman.otenet.gr", + "admanagement.ch", + "admanager.btopenworld.com", + "admanager.carsoup.com", + "admanmedia.com", + "admantx.com", + "admarketplace.net", + "admarvel.com", + "admaster.com.cn", + "admatchly.com", + "admedia.com", + "admeld.com", + "admeridianads.com", + "admex.com", + "admidadsp.com", + "adminder.com", + "adminshop.com", + "admixer.net", + "admized.com", + "admob.com", + "admonitor.com", + "adn.lrb.co.uk", + "adnami.io", + "adnet.asahi.com", + "adnet.biz", + "adnet.de", + "adnet.ru", + "adnetasia.com", + "adnetwork.net", + "adnetworkperformance.com", + "adnews.maddog2000.de", + "adnium.com", + "adnxs-simple.com", + "adnxs.com", + "adocean.pl", + "adonspot.com", + "adopsboost.com", + "adoptum.net", + "adoric-om.com", + "adorigin.com", + "adotmob.com", + "adpepper.dk", + "adpepper.nl", + "adperium.com", + "adpia.vn", + "adplxmd.com", + "adprofits.ru", + "adpushup.com", + "adrazzi.com", + "adreactor.com", + "adrecover.com", + "adrecreate.com", + "adremedy.com", + "adreporting.com", + "adrevolver.com", + "adriver.ru", + "adrolays.de", + "adrotate.de", + "adrotic.girlonthenet.com", + "adrta.com", + "ads-backend.chaincliq.com", + "ads-bilek.com", + "ads-dev.pinterest.com", + "ads-game-187f4.firebaseapp.com", + "ads-img.mozilla.org", + "ads-portal-cdn.vidaatv.net", + "ads-twitter.com", + "ads.365.mk", + "ads.5ci.lt", + "ads.73dpi.com", + "ads.aavv.com", + "ads.abovetopsecret.com", + "ads.aceweb.net", + "ads.acpc.cat", + "ads.acrosspf.com", + "ads.activestate.com", + "ads.adfox.ru", + "ads.administrator.de", + "ads.adred.de", + "ads.adstream.com.ro", + "ads.adultfriendfinder.com", + "ads.advance.net", + "ads.adverline.com", + "ads.alive.com", + "ads.alt.com", + "ads.amdmb.com", + "ads.amigos.com", + "ads.annabac.com", + "ads.apn.co.nz", + "ads.appsgeyser.com", + "ads.as4x.tmcs.net", + "ads.as4x.tmcs.ticketmaster.com", + "ads.asiafriendfinder.com", + "ads.avazu.net", + "ads.bb59.ru", + "ads.betfair.com", + "ads.bigchurch.com", + "ads.bigfoot.com", + "ads.bing.com", + "ads.bittorrent.com", + "ads.blog.com", + "ads.bluemountain.com", + "ads.boerding.com", + "ads.boylesports.com", + "ads.canalblog.com", + "ads.casinocity.com", + "ads.casumoaffiliates.com", + "ads.cbc.ca", + "ads.cc", + "ads.cc-dt.com", + "ads.centraliprom.com", + "ads.channel4.com", + "ads.cheabit.com", + "ads.citymagazine.si", + "ads.clasificadox.com", + "ads.co.com", + "ads.colombiaonline.com", + "ads.com.com", + "ads.comeon.com", + "ads.creative-serving.com", + "ads.cybersales.cz", + "ads.dada.it", + "ads.dailycamera.com", + "ads.deltha.hu", + "ads.dennisnet.co.uk", + "ads.desmoinesregister.com", + "ads.deviantart.com", + "ads.devmates.com", + "ads.digital-digest.com", + "ads.digitalmedianet.com", + "ads.digitalpoint.com", + "ads.doit.com.cn", + "ads.domeus.com", + "ads.eagletribune.com", + "ads.easy-forex.com", + "ads.economist.com", + "ads.elcarado.com", + "ads.electrocelt.com", + "ads.elitetrader.com", + "ads.emdee.ca", + "ads.emirates.net.ae", + "ads.epi.sk", + "ads.epltalk.com", + "ads.eu.msn.com", + "ads.fairfax.com.au", + "ads.fastcomgroup.it", + "ads.femmefab.nl", + "ads.ferianc.com", + "ads.filmup.com", + "ads.financialcontent.com", + "ads.flooble.com", + "ads.fool.com", + "ads.footymad.net", + "ads.forbes.net", + "ads.formit.cz", + "ads.fortunecity.com", + "ads.fotosidan.se", + "ads.friendfinder.com", + "ads.gamecity.net", + "ads.gamespyid.com", + "ads.gamigo.de", + "ads.gaming-universe.de", + "ads.gaming1.com", + "ads.getlucky.com", + "ads.gld.dk", + "ads.gmodules.com", + "ads.goyk.com", + "ads.gradfinder.com", + "ads.grindinggears.com", + "ads.gsm-exchange.com", + "ads.gsmexchange.com", + "ads.guardian.co.uk", + "ads.guardianunlimited.co.uk", + "ads.hbv.de", + "ads.hearstmags.com", + "ads.heartlight.org", + "ads.hollywood.com", + "ads.horsehero.com", + "ads.hsoub.com", + "ads.ibest.com.br", + "ads.ibryte.com", + "ads.icq.com", + "ads.ign.com", + "ads.imagistica.com", + "ads.imgur.com", + "ads.independent.com.mt", + "ads.infi.net", + "ads.internic.co.il", + "ads.ipowerweb.com", + "ads.itv.com", + "ads.jewishfriendfinder.com", + "ads.jobsite.co.uk", + "ads.justhungry.com", + "ads.kabooaffiliates.com", + "ads.kaktuz.net", + "ads.kelbymediagroup.com", + "ads.kinxxx.com", + "ads.kompass.com", + "ads.krawall.de", + "ads.leovegas.com", + "ads.lesbianpersonals.com", + "ads.liberte.pl", + "ads.linkedin.com", + "ads.livenation.com", + "ads.ma7.tv", + "ads.mail.bg", + "ads.massinfra.nl", + "ads.mcafee.com", + "ads.mediaodyssey.com", + "ads.mediasmart.es", + "ads.medienhaus.de", + "ads.meetcelebs.com", + "ads.mgnetwork.com", + "ads.miarroba.com", + "ads.mic.com", + "ads.mmania.com", + "ads.mobilebet.com", + "ads.mozilla.org", + "ads.msn.com", + "ads.multimania.lycos.fr", + "ads.muslimehelfen.org", + "ads.mvscoelho.com", + "ads.myadv.org", + "ads.ndtv1.com", + "ads.networksolutions.com", + "ads.newgrounds.com", + "ads.newmedia.cz", + "ads.newsint.co.uk", + "ads.newsquest.co.uk", + "ads.nj.com", + "ads.nola.com", + "ads.nordichardware.com", + "ads.nordichardware.se", + "ads.nyi.net", + "ads.nytimes.com", + "ads.nyx.cz", + "ads.nzcity.co.nz", + "ads.o2.pl", + "ads.oddschecker.com", + "ads.okcimg.com", + "ads.ole.com", + "ads.oneplace.com", + "ads.opensubtitles.org", + "ads.optusnet.com.au", + "ads.outpersonals.com", + "ads.oxyshop.cz", + "ads.passion.com", + "ads.paymonex.net", + "ads.pexi.nl", + "ads.pfl.ua", + "ads.phpclasses.org", + "ads.pinterest.com", + "ads.planet.nl", + "ads.pni.com", + "ads.pof.com", + "ads.powweb.com", + "ads.printscr.com", + "ads.prisacom.com", + "ads.prod.webservices.mozgcp.net", + "ads.program3.com", + "ads.psd2html.com", + "ads.pubmatic.com", + "ads.quoka.de", + "ads.radio1.lv", + "ads.recoletos.es", + "ads.rediff.com", + "ads.redlightcenter.com", + "ads.revjet.com", + "ads.reward-hunt.com", + "ads.samsung.com", + "ads.saymedia.com", + "ads.schmoozecom.net", + "ads.scifi.com", + "ads.seniorfriendfinder.com", + "ads.servebom.com", + "ads.shizmoo.com", + "ads.shopstyle.com", + "ads.sift.co.uk", + "ads.sjon.info", + "ads.smartclick.com", + "ads.socialtheater.com", + "ads.soft32.com", + "ads.soweb.gr", + "ads.space.com", + "ads.sun.com", + "ads.suomiautomaatti.com", + "ads.supplyframe.com", + "ads.syscdn.de", + "ads.themovienation.com", + "ads.thestar.com", + "ads.thrillsaffiliates.com", + "ads.tiktok.com", + "ads.tmcs.net", + "ads.todoti.com.br", + "ads.toplayaffiliates.com", + "ads.townhall.com", + "ads.travelaudience.com", + "ads.trinitymirror.co.uk", + "ads.tripod.com", + "ads.tripod.lycos.co.uk", + "ads.tripod.lycos.es", + "ads.tripod.lycos.it", + "ads.tripod.lycos.nl", + "ads.tso.dennisnet.co.uk", + "ads.twitter.com", + "ads.twojatv.info", + "ads.ultimate-guitar.com", + "ads.uncrate.com", + "ads.unison.bg", + "ads.usatoday.com", + "ads.uxs.at", + "ads.v-lazer.com", + "ads.verticalresponse.com", + "ads.vgchartz.com", + "ads.virtual-nights.com", + "ads.virtuopolitan.com", + "ads.vnumedia.com", + "ads.walkiberia.com", + "ads.watson.ch", + "ads.weather.ca", + "ads.web.de", + "ads.webinak.sk", + "ads.webmasterpoint.org", + "ads.whoishostingthis.com", + "ads.wiezoekje.nl", + "ads.wikia.nocookie.net", + "ads.wineenthusiast.com", + "ads.wwe.biz", + "ads.xhamster.com", + "ads.xtra.co.nz", + "ads.yahoo.com", + "ads.yimg.com", + "ads.yldmgrimg.net", + "ads.youtube.com", + "ads.yumenetworks.com", + "ads1-adnow.com", + "ads1.msn.com", + "ads1.virtual-nights.com", + "ads10.speedbit.com", + "ads180.com", + "ads2.brazzers.com", + "ads2.contentabc.com", + "ads2.femmefab.nl", + "ads2.gamecity.net", + "ads2.hsoub.com", + "ads2.virtual-nights.com", + "ads2.xnet.cz", + "ads2004.treiberupdate.de", + "ads24h.net", + "ads3-adnow.com", + "ads3.contentabc.com", + "ads3.gamecity.net", + "ads3.virtual-nights.com", + "ads4.gamecity.net", + "ads4.virtual-nights.com", + "ads4homes.com", + "ads5.virtual-nights.com", + "ads6.gamecity.net", + "ads7.gamecity.net", + "adsafeprotected.com", + "adsatt.abc.starwave.com", + "adsatt.abcnews.starwave.com", + "adsatt.espn.go.com", + "adsatt.espn.starwave.com", + "adsatt.go.starwave.com", + "adsby.bidtheatre.com", + "adsbydelema.com", + "adscale.de", + "adscholar.com", + "adscience.nl", + "adsco.re", + "adscpm.com", + "adsdaq.com", + "adsdk.yandex.ru", + "adsend.de", + "adsensecustomsearchads.com", + "adserve.ams.rhythmxchange.com", + "adserve.gkeurope.de", + "adserve.io", + "adserve.jbs.org", + "adserver.71i.de", + "adserver.adultfriendfinder.com", + "adserver.adverty.com", + "adserver.anawe.cz", + "adserver.ariase.org", + "adserver.bdoce.cl", + "adserver.betandwin.de", + "adserver.bing.com", + "adserver.bizedge.com", + "adserver.bizhat.com", + "adserver.break-even.it", + "adserver.cams.com", + "adserver.cdnstream.com", + "adserver.cherryfind.co.uk", + "adserver.com", + "adserver.diariodosertao.com.br", + "adserver.digitoday.com", + "adserver.echdk.pl", + "adserver.friendfinder.com", + "adserver.generationiron.com", + "adserver.hwupgrade.it", + "adserver.ilango.de", + "adserver.industryarena.com", + "adserver.info7.mx", + "adserver.irishwebmasterforum.com", + "adserver.janes.com", + "adserver.kontent.com", + "adserver.mobi", + "adserver.news.com.au", + "adserver.nydailynews.com", + "adserver.o2.pl", + "adserver.oddschecker.com", + "adserver.omroepzeeland.nl", + "adserver.otthonom.hu", + "adserver.pampa.com.br", + "adserver.piksel.mk", + "adserver.pl", + "adserver.portugalmail.net", + "adserver.pressboard.ca", + "adserver.sanomawsoy.fi", + "adserver.sciflicks.com", + "adserver.scr.sk", + "adserver.smgfiles.com", + "adserver.trojaner-info.de", + "adserver.tupolska.com", + "adserver.twitpic.com", + "adserver.virginmedia.com", + "adserver.waggonerguide.com", + "adserver01.de", + "adserverplus.com", + "adserverpub.com", + "adserversolutions.com", + "adserverxxl.de", + "adservice.google.com", + "adservice.google.com.mt", + "adserving.unibet.com", + "adservingfront.com", + "adservrs.com", + "adservrs.com.edgekey.net", + "adsfac.eu", + "adsfac.net", + "adsfac.us", + "adsflowprime.top", + "adshrink.it", + "adside.com", + "adsiduous.com", + "adskeeper.co.uk", + "adskeeper.com", + "adsklick.de", + "adsmart.net", + "adsmartracker.com", + "adsmetadata.startappservice.com", + "adsmogo.com", + "adsnative.com", + "adsoftware.com", + "adsolut.in", + "adspeed.net", + "adspirit.de", + "adsponse.de", + "adspredictiv.com", + "adspsp.com", + "adsroller.com", + "adsrv.deviantart.com", + "adsrv.eacdn.com", + "adsrv.iol.co.za", + "adsrv.kobi.tv", + "adsrv.moebelmarkt.tv", + "adsrv2.swidnica24.pl", + "adsrvr.org", + "adstacks.in", + "adstanding.com", + "adstat.4u.pl", + "adstest.weather.com", + "adstub.net", + "adsupply.com", + "adswizz.com", + "adsxyz.com", + "adsynergy.com", + "adsys.townnews.com", + "adsystem.simplemachines.org", + "adt598.com", + "adtech.com", + "adtech.de", + "adtechjp.com", + "adtechus.com", + "adtegrity.net", + "adthis.com", + "adthrive.com", + "adtiger.de", + "adtilt.com", + "adtng.com", + "adtology.com", + "adtoma.com", + "adtrace.org", + "adtrack.voicestar.com", + "adtraction.com", + "adtrade.net", + "adultadvertising.com", + "adv-adserver.com", + "adv.donejty.pl", + "adv.freeonline.it", + "adv.hwupgrade.it", + "adv.mpvc.it", + "adv.nexthardware.com", + "adv.webmd.com", + "adv.wp.pl", + "adv.yo.cz", + "advangelists.com", + "advariant.com", + "adventory.com", + "adventurousamount.com", + "advert.bayarea.com", + "advert.dyna.ultraweb.hu", + "adverticum.com", + "adverticum.net", + "advertipros.com", + "advertise.com", + "advertiserurl.com", + "advertising.com", + "advertisingbanners.com", + "advertisingbox.com", + "advertmarket.com", + "advertmedia.de", + "advertpro.ya.com", + "advertserve.com", + "advertwizard.com", + "advideo.uimserv.net", + "adview.com", + "advisormedia.cz", + "adviva.net", + "advnt.com", + "adwebone.com", + "adwhirl.com", + "adworldnetwork.com", + "adworx.at", + "adworx.nl", + "adx.gayboy.at", + "adxpansion.com", + "adxpose.com", + "adyoulike.com", + "adz.rashflash.com", + "adzbazar.com", + "adzerk.net", + "adzerk.s3.amazonaws.com", + "adzestocp.com", + "aerialapps.com", + "aerserv.com", + "af-ad.co.uk", + "affec.tv", + "affectionknit.com", + "affili.net", + "affiliate.1800flowers.com", + "affiliate.dtiserv.com", + "affiliate.travelnow.com", + "affiliate.treated.com", + "affiliatefuture.com", + "affiliates.allposters.com", + "affiliates.babylon.com", + "affiliates.globat.com", + "affiliates.streamray.com", + "affiliates.thinkhost.net", + "affiliates.thrixxx.com", + "affiliates.ultrahosting.com", + "affiliatetracking.com", + "affiliatetracking.net", + "affiliatewindow.com", + "afflnx.com", + "afftracking.justanswer.com", + "afraidlanguage.com", + "agencysignals.com", + "agkn.com", + "ah-ha.com", + "ahalogy.com", + "aheadday.com", + "aim4media.com", + "airplanecoffee.com", + "airpush.com", + "aistat.net", + "ak0gsh40.com", + "alchemist.go2cloud.org", + "alclick.com", + "alenty.com", + "alexa-sitestats.s3.amazonaws.com", + "algorix.co", + "aliasanvil.com", + "alikeaddition.com", + "alipromo.com", + "alluringbucket.com", + "aloofmetal.com", + "aloofvest.com", + "alphonso.tv", + "als-svc.nytimes.com", + "amazingcounters.com", + "amazon-adsystem.com", + "americash.com", + "amidsttruly.com", + "amung.us", + "analytics-production.hapyak.com", + "analytics.adpost.org", + "analytics.algoepico.net", + "analytics.bitrix.info", + "analytics.cloudron.io", + "analytics.ext.go-tellm.com", + "analytics.google.com", + "analytics.htmedia.in", + "analytics.icons8.com", + "analytics.inlinemanual.com", + "analytics.jst.ai", + "analytics.justuno.com", + "analytics.lucid.app", + "analytics.mailmunch.co", + "analytics.mobile.yandex.net", + "analytics.momentum-institut.at", + "analytics.myfinance.com", + "analytics.ostr.io", + "analytics.phando.com", + "analytics.picsart.com", + "analytics.pinterest.com", + "analytics.pointdrive.linkedin.com", + "analytics.poolshool.com", + "analytics.posttv.com", + "analytics.santander.co.uk", + "analytics.swiggy.com", + "analytics.tiktok.com", + "analytics.xelondigital.com", + "analytics.yahoo.com", + "analyticsapi.happypancake.net", + "ancientact.com", + "androiddownload.net", + "aniview.com", + "annonser.dagbladet.no", + "annoyedairport.com", + "annoyingacoustics.com", + "anrdoezrs.net", + "anstrex.com", + "anxiousapples.com", + "api.affiliations.site", + "api.amplitude.com", + "api.appmetrica.yandex.ru", + "api.eu.amplitude.com", + "api.intensifier.de", + "api.iterable.com", + "api.kameleoon.com", + "api.lab.amplitude.com", + "api.rudderlabs.com", + "api.swetrix.com", + "api2.amplitude.com", + "apolloprogram.io", + "app-analytics-v2.snapchat.com", + "app-analytics.snapchat.com", + "app-measurement.com", + "app.pendo.io", + "app2.salesmanago.pl", + "appboycdn.com", + "applovin.com", + "appsflyer.com", + "aps.hearstnp.com", + "apsalar.com", + "aptabase.com", + "apture.com", + "apu.samsungelectronics.com", + "aquaticowl.com", + "aralego.com", + "arc1.msn.com", + "archswimming.com", + "ard.xxxblackbook.com", + "aromamirror.com", + "as.webmd.com", + "as2.adserverhd.com", + "aserv.motorsgate.com", + "asewlfjqwlflkew.com", + "askdriver.com", + "aso1.net", + "assets1.exgfnetwork.com", + "assoc-amazon.com", + "aswpapius.com", + "aswpsdkus.com", + "async.gsyndication.com", + "at-adserver.alltop.com", + "at-o.net", + "atdmt.com", + "athena-ads.wikia.com", + "ato.mx", + "attractionbanana.com", + "attribution.report", + "atwola.com", + "auctionads.com", + "auctionads.net", + "aud.pubmatic.com", + "audience.media", + "audience2media.com", + "audienceinsights.com", + "audio.ad", + "audit.median.hu", + "audit.webinform.hu", + "augur.io", + "auto-bannertausch.de", + "avalonalbum.com", + "avazutracking.net", + "avenuea.com", + "avocet.io", + "awempire.com", + "awin1.com", + "awstrack.me", + "awzbijw.com", + "axonix.com", + "ay.delivery", + "ayads.co", + "b-s.tercept.com", + "b.videoamp.com", + "b3.videoamp.com", + "ba.afl.rakuten.co.jp", + "backbeatmedia.com", + "bagsurprise.com", + "banik.redigy.cz", + "banner.ambercoastcasino.com", + "banner.buempliz-online.ch", + "banner.cotedazurpalace.com", + "banner.easyspace.com", + "banner.elisa.net", + "banner.finzoom.ro", + "banner.goldenpalace.com", + "banner.inyourpocket.com", + "banner.linux.se", + "banner.media-system.de", + "banner.nixnet.cz", + "banner.noblepoker.com", + "banner.northsky.com", + "banner.reinstil.de", + "banner.tanto.de", + "banner.titan-dsl.de", + "banner10.zetasystem.dk", + "bannerads.de", + "bannerboxes.com", + "bannerconnect.com", + "bannerconnect.net", + "bannergrabber.internet.gr", + "bannerimage.com", + "bannermall.com", + "bannermanager.bnr.bg", + "bannerpower.com", + "banners.adultfriendfinder.com", + "banners.amigos.com", + "banners.asiafriendfinder.com", + "banners.babylon-x.com", + "banners.bol.com.br", + "banners.cams.com", + "banners.czi.cz", + "banners.dine.com", + "banners.direction-x.com", + "banners.freett.com", + "banners.friendfinder.com", + "banners.getiton.com", + "banners.iq.pl", + "banners.passion.com", + "banners.payserve.com", + "banners.resultonline.com", + "banners.sys-con.com", + "banners.thomsonlocal.com", + "banners.virtuagirlhd.com", + "bannerserver.com", + "bannershotlink.perfectgonzo.com", + "bannersng.yell.com", + "bannerspace.com", + "bannerswap.com", + "bannertrack.net", + "bannery.cz", + "bannieres.wdmedia.net", + "bans.bride.ru", + "baremetrics.com", + "barnesandnoble.bfast.com", + "basebanner.com", + "basketballbelieve.com", + "baskettexture.com", + "bastingart.com", + "bat.bing.com", + "bbelements.com", + "bbn.img.com.ua", + "beachfront.com", + "beacon.gu-web.net", + "beacon.netflix.com", + "beacons.gcp.gvt2.com", + "beacons.gvt2.com", + "bebi.com", + "beemray.com", + "begun.ru", + "behavioralengine.com", + "belstat.com", + "belstat.nl", + "benefits.sovendus.com", + "benfly.net", + "berp.com", + "bespoke.iln8.net", + "bestboundary.com", + "bestbuy.7tiv.net", + "bewilderedblade.com", + "bfmio.com", + "bhcumsc.com", + "bid.pubmatic.com", + "bidbarrel.cbsnews.com", + "bidclix.com", + "bidclix.net", + "bidpapers.com", + "bidr.io", + "bidsopt.com", + "bidswitch.net", + "bidtellect.com", + "bidvertiser.com", + "big-bang-ads.com", + "bigbangmedia.com", + "bigclicks.com", + "bigfishapparel.com", + "bigreal.org", + "bigworldfashion.com", + "billboard.cz", + "billtable.com", + "birthdaybelief.com", + "bitmedianetwork.com", + "bizible.com", + "bizographics.com", + "bizrate.com", + "bizzclick.com", + "bkrtx.com", + "bleachbubble.com", + "blingbucks.com", + "blis.com", + "blockadblock.com", + "blogads.com", + "blogcounter.de", + "blogherads.com", + "blogtoplist.se", + "blogtopsites.com", + "blueconic.com", + "blueconic.net", + "bluekai.com", + "bluelithium.com", + "bluenest.net", + "bluewhaleweb.com", + "blushingbeast.com", + "blushingbread.com", + "bm.annonce.cz", + "bn.bfast.com", + "bnrs.ilm.ee", + "boffoadsapi.com", + "boilingbeetle.com", + "boilingmadsoup.com", + "bongacash.com", + "boomads.com", + "boomtrain.com", + "boredcrown.com", + "boudja.com", + "bounceads.net", + "bounceexchange.com", + "bowie-cdn.fathomdns.com", + "box.anchorfree.net", + "bpath.com", + "bpu.samsungelectronics.com", + "braincash.com", + "brand-display.com", + "brandreachsys.com", + "brandybison.com", + "braze.eu", + "breadbalance.com", + "breakableinsurance.com", + "breaktime.com.tw", + "brealtime.com", + "bridgetrack.com", + "brightcom.com", + "brightinfo.com", + "brightmountainmedia.com", + "broadcastbed.com", + "broadstreetads.com", + "browser-http-intake.logs.datadoghq.com", + "browser-http-intake.logs.datadoghq.eu", + "bs.yandex.ru", + "btglss.net", + "btrll.com", + "bttrack.com", + "bubblyaction.com", + "buddycanvas.com", + "buysellads.com", + "buzzonclick.com", + "bwp.download.com", + "c.bigmir.net", + "c1exchange.com", + "c212.net", + "cakesdrum.com", + "calculatingcircle.com", + "calculatorstatement.com", + "call-ad-network-api.marchex.com", + "callousbrake.com", + "callrail.com", + "calmcactus.com", + "calypsocapsule.com", + "campaign.bharatmatrimony.com", + "caniamedia.com", + "capriciouscorn.com", + "captainbicycle.com", + "captainstick.com", + "carambo.la", + "carbonads.com", + "carbonads.net", + "cardmethod.com", + "caringcast.com", + "carsarace.com", + "cartstack.com", + "carvecakes.com", + "casalemedia.com", + "casalmedia.com", + "cash4members.com", + "cash4popup.de", + "cashcrate.com", + "cashengines.com", + "cashfiesta.com", + "cashpartner.com", + "cashstaging.me", + "casinopays.com", + "casinorewards.com", + "casinotraffic.com", + "cattlecommittee.com", + "causecherry.com", + "cautiouscredit.com", + "cbanners.virtuagirlhd.com", + "cbzxy.com", + "cdn.bannerflow.com", + "cdn.branch.io", + "cdn.freshmarketer.com", + "cdn.heapanalytics.com", + "cdn.keywee.co", + "cdn.mouseflow.com", + "cdn.onesignal.com", + "cdn.scarabresearch.com", + "cdn.segment.com", + "cdnondemand.org", + "cedato.com", + "celtra.com", + "centerpointmedia.com", + "cetrk.com", + "cgicounter.puretec.de", + "chairscrack.com", + "channelintelligence.com", + "chargecracker.com", + "chart.dk", + "chartbeat.com", + "chartbeat.net", + "chartboost.com", + "checkstat.nl", + "cherriescare.com", + "childlikecrowd.com", + "chubbycreature.com", + "citrusad.net", + "cityads.telus.net", + "cj.com", + "cjbmanagement.com", + "cjlog.com", + "cl.turkishairlines.com", + "cl0udh0st1ng.com", + "clck.ru", + "clevernt.com", + "click-1.pl", + "click.airmalta-mail.com", + "click.aliexpress.com", + "click.allkeyshop.com", + "click.bkdpt.com", + "click.cartsguru.io", + "click.ccg.nintendo.com", + "click.cision.com", + "click.classmates.com", + "click.comm.rcibank.co.uk", + "click.crm.ba.com", + "click.digital.metaquestmail.com", + "click.discord.com", + "click.e.bbcmail.co.uk", + "click.e.progressive.com", + "click.e.zoom.us", + "click.em.blizzard.com", + "click.email.bbc.com", + "click.email.lhh.com", + "click.email.microsoftemail.com", + "click.email.sonos.com", + "click.emails.argos.co.uk", + "click.emails.tuclothing.sainsburys.co.uk", + "click.fool.com", + "click.hookupinyourcity.com", + "click.hooligapps.com", + "click.i.southwesternrailway.com", + "click.infoblox.com", + "click.justwatch.com", + "click.kmindex.ru", + "click.liftoff.io", + "click.mail.hotels.com", + "click.mail.salesforce.com", + "click.mailing.ticketmaster.com", + "click.mkt.grab.com", + "click.news.vans.com", + "click.nl.npr.org", + "click.nvgaming.nvidia.com", + "click.redditmail.com", + "click.uber.com", + "click.v.visionlab.es", + "click2freemoney.com", + "click360v2-ingest.azurewebsites.net", + "click4.pro", + "clickadddilla.com", + "clickadz.com", + "clickagents.com", + "clickbank.com", + "clickbooth.com", + "clickboothlnk.com", + "clickbrokers.com", + "clickcease.com", + "clickcompare.co.uk", + "clickdensity.com", + "clickedyclick.com", + "clickfuse.com", + "clickhereforcellphones.com", + "clickngo.pro", + "clickngo.top", + "clickonometrics.pl", + "clicks.checkatrade.com", + "clicks.deliveroo.co.uk", + "clicks.equantum.com", + "clicks.eventbrite.com", + "clicks.monzo.com", + "clickserve.cc-dt.com", + "clicksinfo.thefork.co.uk", + "clicktag.de", + "clickthruserver.com", + "clickthrutraffic.com", + "clicktrack.pubmatic.com", + "clicktrack.ziyu.net", + "clicktracks.com", + "clicktrade.com", + "clickxchange.com", + "clickyab.com", + "clickz.com", + "clientgear.com", + "clientmetrics-pa.googleapis.com", + "clikerz.net", + "cliksolution.com", + "clixgalore.com", + "clk1005.com", + "clk1011.com", + "clk1015.com", + "clkrev.com", + "clksite.com", + "cloudflareinsights.com", + "cloudguppy.com", + "clrstm.com", + "cluster.adultworld.com", + "clustrmaps.com", + "cmp.dmgmediaprivacy.co.uk", + "cmvrclicks000.com", + "cnomy.com", + "cnt1.pocitadlo.cz", + "cnvlink.com", + "cny.yoyo.org", + "codeadnetwork.com", + "cognitiv.ai", + "cointraffic.io", + "coldbalance.com", + "collector-dev.cdp-dev.cnn.com", + "collector.cdp.cnn.com", + "colonize.com", + "colorfulpet.com", + "coloroptimizer.com", + "comfortablecheese.com", + "commissionmonster.com", + "communications.melitaltd.com", + "compactbanner.com", + "comparereaction.com", + "compilework.com", + "comprabanner.it", + "conditionchange.com", + "conductrics.com", + "confiant-integrations.net", + "confidencetalks.com", + "configv2.unityads.unity3d.com", + "connatix.com", + "connectad.io", + "connextra.com", + "consciouscheese.com", + "consensad.com", + "consensu.org", + "contadores.miarroba.com", + "content.acc-hd.de", + "content.ad", + "content22.online.citi.com", + "contextweb.com", + "contrack.link", + "converge-digital.com", + "conversantmedia.com", + "conversionbet.com", + "conversionruler.com", + "convertingtraffic.com", + "convrse.media", + "cookies.cmpnet.com", + "coolkidsdistrict.com", + "cootlogix.com", + "copperstills.net", + "copycarpenter.com", + "copyrightaccesscontrols.com", + "coremetrics.com", + "cosmosjackson.com", + "count.rin.ru", + "count.west263.com", + "counted.com", + "counter.bloke.com", + "counter.cnw.cz", + "counter.cz", + "counter.dreamhost.com", + "counter.mirohost.net", + "counter.mojgorod.ru", + "counter.rambler.ru", + "counter.search.bg", + "counter.snackly.co", + "counting.kmindex.ru", + "coupling-media.de", + "cowbelltime.com", + "coxmt.com", + "cozyhillside.com", + "cpalead.com", + "cpays.com", + "cpmstar.com", + "cpu.samsungelectronics.com", + "cpx.to", + "cpxinteractive.com", + "cqcounter.com", + "crabbychin.com", + "craktraffic.com", + "crashchance.com", + "crashlytics.com", + "crashlyticsreports-pa.googleapis.com", + "cratecamera.com", + "crawlability.com", + "crazyegg.com", + "crazypopups.com", + "creatives.livejasmin.com", + "creatopy.net", + "crimsonmeadow.com", + "criteo.com", + "criteo.net", + "critictruck.com", + "crowdedmass.com", + "crowdgravity.com", + "crsspxl.com", + "crta.dailymail.co.uk", + "crtv.mate1.com", + "crwdcntrl.net", + "crypto-loot.org", + "crystalboulevard.com", + "cs.co", + "curbminers.com", + "curiousmetal.com", + "curtaincows.com", + "curveshore.com", + "cushiondrum.com", + "customad.cnn.com", + "customads.co", + "customerevents.netflix.com", + "customers.kameleoon.com", + "cutechin.com", + "cxense.com", + "cyberbounty.com", + "cyclinghere.com", + "d-collect.jennifersoft.com", + "d-collector.jennifersoft.com", + "d.adroll.com", + "d1f0tbk1v3e25u.cloudfront.net", + "d2cmedia.ca", + "d81mfvml8p5ml.cloudfront.net", + "dabiaozhi.com", + "dacdn.visualwebsiteoptimizer.com", + "dacdn.vwo.com", + "dakic-ia-300.com", + "damageddistance.com", + "damdoor.com", + "dapper.net", + "data.namesakeoscilloscopemarquis.com", + "datapickles.com", + "datenow.link", + "daybreaklights.com", + "dc-storm.com", + "de17a.com", + "dealdotcom.com", + "decenterads.com", + "decisivebase.com", + "decisivedrawer.com", + "decknetwork.net", + "deepintent.com", + "delicatecascade.com", + "deloo.de", + "demandbase.com", + "demdex.net", + "deployads.com", + "desertgates.com", + "desiredirt.com", + "detailedgovernment.com", + "detectdiscovery.com", + "dev.visualwebsiteoptimizer.com", + "dewdroplagoon.com", + "dianomi.com", + "dicecandies.com", + "didtheyreadit.com", + "digestiondrawer.com", + "digital-ads.s3.amazonaws.com", + "direct-events-collector.spot.im", + "direct-promo.pro", + "directaclick.com", + "directorym.com", + "discountclick.com", + "discreetfield.com", + "displayvertising.com", + "disqusads.com", + "dist.belnk.com", + "distillery.wistia.com", + "districtm.ca", + "districtm.io", + "dmp.mall.tv", + "dmtracker.com", + "dmtracking.alibaba.com", + "dmtracking2.alibaba.com", + "dnsdelegation.io", + "do-global.com", + "dockdigestion.com", + "dogcollarfavourbluff.com", + "domaining.in", + "domdex.com", + "dotmetrics.net", + "dotomi.com", + "doubleclick.com", + "doubleclick.de", + "doubleclick.net", + "doublepimp.com", + "doubleverify.com", + "dpbolvw.net", + "dpu.samsungelectronics.com", + "dq95d35.com", + "dragonbats.com", + "drumcash.com", + "dsp.colpirio.com", + "dsp.io", + "dstillery.com", + "dustyhammer.com", + "dyntrk.com", + "e-eu.customeriomail.com", + "e-m.fr", + "e-planning.net", + "e.customeriomail.com", + "e.kde.cz", + "e37364.dscd.akamaiedge.net", + "eadexchange.com", + "eas.almamedia.fi", + "easyhits4u.com", + "ebayadvertising.com", + "ebuzzing.com", + "echoacloud.com", + "ecircle-ag.com", + "ecleneue.com", + "eclick.vn", + "eclicks.deliveroo.com", + "eclkspbn.com", + "ecoupons.com", + "edaa.eu", + "edgexads.com", + "eighthlayer.net", + "eiv.baidu.com", + "elasticchange.com", + "elderlytown.com", + "elitedollars.com", + "em1.yoursantander.co.uk", + "email-link.adtidy.info", + "email-link.adtidy.net", + "email-link.adtidy.org", + "email-links.crowdfireapp.com", + "email-open.adtidy.net", + "email-open.adtidy.org", + "email-trk.ihg-businessedge.com", + "email.mg1.substack.com", + "emailer.stockbit.com", + "emaillinks.soundiiz.com", + "emebo.io", + "emerse.com", + "emetriq.de", + "emjcd.com", + "eml.blackduck.com", + "emltrk.com", + "emodoinc.com", + "emptyescort.com", + "emxdigital.com", + "energeticladybug.com", + "engage.tines.com", + "engage.windows.com", + "engagebdr.com", + "engageya.com", + "engine.espace.netavenir.com", + "engineertrick.com", + "enginenetwork.com", + "enormousearth.com", + "enquisite.com", + "ensighten.com", + "entercasino.com", + "entrecard.s3.amazonaws.com", + "enviousthread.com", + "epom.com", + "epp.bih.net.ba", + "eqads.com", + "eqy.link", + "erne.co", + "ero-advertising.com", + "essaygiants.com", + "esty.com", + "et.educationdynamics.com", + "et.nytimes.com", + "etahub.com", + "etargetnet.com", + "etracker.com", + "etracker.de", + "eu-adcenter.net", + "eule1.pmu.fr", + "eulerian.net", + "eurekster.com", + "euros4click.de", + "eusta.de", + "evadav.com", + "evadavdsp.pro", + "evencoating.com", + "eventexistence.com", + "events-eu.freshsuccess.com", + "events-us.freshsuccess.com", + "everestads.net", + "everesttech.net", + "evergage.com", + "eversales.space", + "evs.sgmt.loom.com", + "evyy.net", + "exampleshake.com", + "exchange-it.com", + "exchangead.com", + "exchangeclicksonline.com", + "exelate.com", + "exelator.com", + "exhibitsneeze.com", + "exit76.com", + "exitexchange.com", + "exitfuel.com", + "exoclick.com", + "exosrv.com", + "experianmarketingservices.digital", + "explorads.com", + "exponea.com", + "exponential.com", + "exportdialog.com", + "express-submit.de", + "extractobservation.com", + "extreme-dm.com", + "extremetracking.com", + "eyeota.net", + "eyeviewads.com", + "eyewonder.com", + "ezula.com", + "f.pie.org", + "f7ds.liberation.fr", + "fabric.io", + "fadedsnow.com", + "fairfeeling.com", + "fakedisguise.com", + "fallaciousfifth.com", + "fallingshoals.com", + "fam-ad.com", + "farethief.com", + "farmergoldfish.com", + "fast-redirecting.com", + "fastclick.com", + "fastclick.com.edgesuite.net", + "fastclick.net", + "fastly-insights.com", + "faultycanvas.com", + "fave.co", + "fc.webmasterpro.de", + "feedbackresearch.com", + "feedjit.com", + "feedmob.com", + "femalecook.com", + "figsprotein.com", + "figurehunter.net", + "fillthemap.com", + "fimserve.com", + "findcommerce.com", + "findyourcasino.com", + "fireads.online", + "fireads.org", + "fireworkadservices.com", + "fireworkanalytics.com", + "fireworks-advertising.com", + "firstlightera.com", + "fishingtoolsbox.com", + "fixedfold.com", + "fjordsand.com", + "flairadscpc.com", + "flakyfeast.com", + "flashtalking.com", + "flashtexting.com", + "fleshlightcash.com", + "flexbanner.com", + "flimsycircle.com", + "flimsythought.com", + "floodprincipal.com", + "flourishinginnovation.com", + "floweryflavor.com", + "flowgo.com", + "flurry.com", + "fontserif.com", + "foo.cosmocode.de", + "foresee.com", + "forex-affiliate.net", + "forkcdn.com", + "fourlevelsgame.com", + "fpctraffic.com", + "fpjs.io", + "fqtag.com", + "fraysystems.com", + "free-counter.co.uk", + "freebanner.com", + "freecounterstat.com", + "freedomgrail.com", + "freelogs.com", + "freepay.com", + "freestats.com", + "freestats.tv", + "freewebcounter.com", + "freewheel.com", + "freewheel.tv", + "freezingbuilding.com", + "frequentflesh.com", + "freshrelevance.com", + "fronttoad.com", + "frtyj.com", + "frtyk.com", + "fudgegenie.com", + "fullrestore.net", + "fullstory.com", + "functionalcrown.com", + "functionalfeather.com", + "funklicks.com", + "funnelytics.io", + "funtoyplanet.com", + "furryfork.com", + "fusionads.net", + "fusionquest.com", + "futuristicfifth.com", + "futuristicframe.com", + "fuzzybasketball.com", + "fwcdn1.com", + "fwcdn2.com", + "fxstyle.net", + "g2.gumgum.com", + "ga.clearbit.com", + "gadsbee.com", + "galaxien.com", + "game-advertising-online.com", + "gamesites100.net", + "gamesites200.com", + "gammamaximum.com", + "gaug.es", + "gavvia.com", + "gearwom.de", + "geo.digitalpoint.com", + "geobanner.adultfriendfinder.com", + "georiot.com", + "geovisite.com", + "getclicky.com", + "getintent.com", + "getmyads.com", + "getxmlisi.com", + "giddycoat.com", + "glasscoyote.com", + "glisteningsign.com", + "globalismedia.com", + "gloriousbeef.com", + "gmads.net", + "gml.email", + "go-clicks.de", + "go-link.network", + "go-mpulse.net", + "go-rank.de", + "go.clickwww.com", + "go.dhs.gov", + "go.eu.sparkpostmail1.com", + "go.first.org", + "go.icann.org", + "go.scmagazine.com", + "go.usa.gov", + "go.xlirdr.com", + "go2affise.com", + "godseedband.com", + "goingplatinum.com", + "goldstats.com", + "gondolagnome.com", + "goodcontentservice.top", + "google-analytics.com", + "googleadservices.com", + "googleanalytics.com", + "googlesyndication.com", + "googletagmanager.com", + "googletagservices.com", + "goqon.com", + "gostats.com", + "gothamads.com", + "gotoyahoo.com", + "gotraffic.net", + "gp.dejanews.com", + "grandfatherguitar.com", + "granlite.com", + "grapeshot.co.uk", + "greystripe.com", + "groovespacing.com", + "grouchybrothers.com", + "groundtruth.com", + "growthrx.in", + "gscontxt.net", + "guardeddirection.com", + "guardedschool.com", + "gumyfui.com", + "gunggo.com", + "h-bid.com", + "h-trck.com", + "h0.t.hubspotemail.net", + "haikusoap.com", + "halcyoncanyon.com", + "halocolor.com", + "haltingbadge.com", + "hammerhearing.com", + "hamsterspot.com", + "handsomehose.com", + "handyfireman.com", + "handyincrease.com", + "haplesshydrant.com", + "harrenmedia.com", + "harrenmedianetwork.com", + "hayweb.net", + "hb.afl.rakuten.co.jp", + "hb.vntsm.com", + "hbb.afl.rakuten.co.jp", + "hbopenbid.pubmatic.com", + "heap.com", + "heimi-lwx.com", + "hellobar.com", + "helpcollar.com", + "hentaicounter.com", + "herbalaffiliateprogram.com", + "hexcan.com", + "hexusads.fluent.ltd.uk", + "heyos.com", + "hf5rbejvpwds.com", + "hfc195b.com", + "hgads.com", + "highnoongear.com", + "hightrafficads.com", + "hilariouszinc.com", + "histats.com", + "historytrade.com", + "hit-parade.com", + "hit.ua", + "hit.webcentre.lycos.co.uk", + "hitbox.com", + "hitcounters.miarroba.com", + "hitlist.ru", + "hitlounge.com", + "hitometer.com", + "hits-i.iubenda.com", + "hits.europuls.eu", + "hits.informer.com", + "hits.puls.lv", + "hits.sh", + "hits.theguardian.com", + "hits4me.com", + "hitslink.com", + "hittail.com", + "hlok.qertewrt.com", + "hocgeese.com", + "hollowafterthought.com", + "homelycrown.com", + "homepageking.de", + "honorableland.com", + "hostedads.realitykings.com", + "hotjar.com", + "hotlog.ru", + "hotrank.com.tw", + "hoverowl.com", + "hs-analytics.net", + "hs-banner.com", + "hsadspixel.net", + "hsleadflows.net", + "hsn.uqhv.net", + "htlbid.com", + "httpool.com", + "hubspotlinks.com", + "hueads.com", + "hueadsortb.com", + "hueadsxml.com", + "hurricanedigitalmedia.com", + "hustlercoach.com", + "hydraconcept.com", + "hydramedia.com", + "hyperbanner.net", + "hypertracker.com", + "hyprmx.com", + "hystericalcloth.com", + "i-i.lt", + "i305175.net", + "ia.iinfo.cz", + "iad.anm.co.uk", + "iadnet.com", + "ibillboard.com", + "icptrack.com", + "icywinter.com", + "id5-sync.com", + "idealadvertising.net", + "idevaffiliate.com", + "idtargeting.com", + "ientrymail.com", + "iesnare.com", + "ifa.tube8live.com", + "ilbanner.com", + "ilead.itrack.it", + "illustriousoatmeal.com", + "image2.pubmatic.com", + "image3.pubmatic.com", + "image4.pubmatic.com", + "image6.pubmatic.com", + "imagecash.net", + "images-pw.secureserver.net", + "img.prohardver.hu", + "imgpromo.easyrencontre.com", + "immensehoney.com", + "imonomy.com", + "imp.i312864.net", + "impossibleexpansion.com", + "imprese.cz", + "impressionmedia.cz", + "impressionmonster.com", + "improvedigital.com", + "imrworldwide.com", + "inclk.com", + "incognitosearches.com", + "incoming-telemetry.thunderbird.net", + "incoming.telemetry.mozilla.org", + "indexexchange.com", + "indexstats.com", + "indexww.com", + "indieclick.com", + "industrybrains.com", + "inetlog.ru", + "infinite-ads.com", + "infinityads.com", + "infoevent.startappservice.com", + "infolinks.com", + "inmobi.com", + "inner-active.com", + "inner-active.mobi", + "innovid.com", + "inquisitiveinvention.com", + "insgly.net", + "insidepsych.net", + "insightexpress.com", + "insightexpressai.com", + "inskinad.com", + "inspectlet.com", + "install.365-stream.com", + "instantmadness.com", + "insticator.com", + "intelliads.com", + "intelligenceadx.com", + "interactive.forthnet.gr", + "intercom-clicks.com", + "intergi.com", + "internalcondition.com", + "internetfuel.com", + "interreklame.de", + "ioam.de", + "ip.ro", + "ip193.cn", + "iperceptions.com", + "ipredictive.com", + "ipstack.com", + "irchan.com", + "ireklama.cz", + "is-tracking-pixel-api-prod.appspot.com", + "islandwebhelp.com", + "itop.cz", + "its-that-easy.com", + "ivwbox.de", + "ivykiosk.com", + "iyfbodn.com", + "iyfnzgb.com", + "j93557g.com", + "jads.co", + "jamexport.com", + "jcount.com", + "jdoqocy.com", + "jewelrysprings.com", + "jinkads.de", + "joetec.net", + "joyoussurprise.com", + "js-agent.newrelic.com", + "js-api.otherlevels.com", + "js-tags.otherlevels.com", + "js.iterable.com", + "js.users.51.la", + "jsecoin.com", + "jsrdn.com", + "jubilantglimmer.com", + "juiceblocks.com", + "juicyads.com", + "juicyads.me", + "jumptap.com", + "jungroup.com", + "justicejudo.com", + "justpremium.com", + "justrelevant.com", + "justwowjars.com", + "k.iinfo.cz", + "kameleoon.eu", + "kanoodle.com", + "kargo.com", + "kernellife.com", + "kickoffo.site", + "kindads.com", + "kindlereunion.com", + "kissmetrics.com", + "kittentacos.com", + "kittycatking.com", + "klclick.com", + "klclick1.com", + "kliks.nl", + "knitstamp.com", + "knorex.com", + "knottyswing.com", + "komoona.com", + "kompasads.com", + "kontera.com", + "kost.tv", + "kpu.samsungelectronics.com", + "krxd.net", + "ktu.sv2.biz", + "kueezrtb.com", + "kvsadman.com", + "l1.britannica.com", + "lakesecure.com", + "lameletters.com", + "landkarts.com", + "larati.net", + "largebrass.com", + "laughcloth.com", + "launchbit.com", + "layer-ad.de", + "layer-ads.de", + "lazybumblebee.com", + "lbn.ru", + "lead02.com", + "leadboltads.net", + "leadclick.com", + "leadinfo.net", + "leadingedgecash.com", + "leadplace.fr", + "leadspace.com", + "leadzupc.com", + "leaplunchroom.com", + "leftliquid.com", + "lemmatechnologies.com", + "lemnisk.co", + "lever-analytics.com", + "lfeeder.com", + "lfstmedia.com", + "lgsmartad.com", + "li.alibris.com", + "li.azstarnet.com", + "li.dailycaller.com", + "li.gatehousemedia.com", + "li.gq.com", + "li.hearstmags.com", + "li.livingsocial.com", + "li.mw.drhinternet.net", + "li.onetravel.com", + "li.patheos.com", + "li.pmc.com", + "li.realtor.com", + "li.ziffimages.com", + "liadm.com", + "libraryfacts.com", + "lifeimpressions.net", + "liftdna.com", + "ligatus.com", + "ligatus.de", + "lightspeedcash.com", + "lightstep.medium.systems", + "lijit.com", + "limecodesign.com", + "link-booster.de", + "link.axios.com", + "link.beelivery.com", + "link.email.davidlloydclubs.co.uk", + "link.email.usmagazine.com", + "link.go.chase", + "link.sbstck.com", + "link.team.hyperoptic.com", + "link.theatlantic.com", + "link.uk.expediamail.com", + "linkbuddies.com", + "linkexchange.com", + "linkprice.com", + "linkrain.com", + "linkreferral.com", + "links-ranking.de", + "links.email.crunchbase.com", + "links.housekeep.com", + "links.prosservice.fr", + "links.zoopla.co.uk", + "linksoutside.com", + "linkstable.com", + "linkstorms.com", + "linkswaper.com", + "linksynergy.com", + "linktarget.com", + "linkvertise.com", + "liquidad.narrowcastmedia.com", + "litix.io", + "live.trmzum.com", + "liveadexchanger.com", + "liveintent.com", + "livelylaugh.com", + "liverail.com", + "livingsleet.com", + "lizardslaugh.com", + "lkqd.com", + "lnks.gd", + "loading321.com", + "loadsurprise.com", + "locked4.com", + "lockerdome.com", + "locolava.com", + "log.btopenworld.com", + "log.logrocket.io", + "log.pinterest.com", + "log.videocampaign.co", + "logger.snackly.co", + "logs.roku.com", + "logs.spilgames.com", + "logsss.com", + "logua.com", + "look.djfiln.com", + "look.ichlnk.com", + "look.opskln.com", + "look.ufinkln.com", + "loopme.com", + "loudlunch.com", + "lowest-prices.eu", + "lucidmedia.com", + "luckyorange.com", + "ludicrousarch.com", + "lyricshook.com", + "lytics.io", + "lzjl.com", + "m.trb.com", + "m2.ai", + "m32.media", + "m4n.nl", + "m6r.eu", + "mackeeperapp.mackeeper.com", + "madclient.uimserv.net", + "madcpms.com", + "madinad.com", + "madisonavenue.com", + "madvertise.de", + "magicadz.co", + "magicaljoin.com", + "magsrv.com", + "mail-ads.google.com", + "maltiverse.lt.acemlnc.com", + "manageadv.cblogs.eu", + "mantisadnetwork.com", + "mapcommand.com", + "marinsm.com", + "markedmeasure.com", + "marketing.888.com", + "marketing.desertcart.com", + "marketing.net.brillen.de", + "marketing.net.home24.de", + "marketing.net.occhiali24.it", + "marketing.nyi.net", + "marketing.osijek031.com", + "marketingcloudapis.com", + "marketingsolutions.yahoo.com", + "marketo.com", + "marlowpillow.sjv.io", + "marriedbelief.com", + "mas.sector.sk", + "matchcraft.com", + "matheranalytics.com", + "mathtag.com", + "matomo.activate.cz", + "matomo.crossiety.app", + "mautic.com", + "max.i12.de", + "maximiser.net", + "maxonclick.com", + "mbs.megaroticlive.com", + "mcdlks.com", + "mcs-va.tiktok.com", + "mcs-va.tiktokv.com", + "meadowlullaby.com", + "measlymiddle.com", + "measure.office.com", + "measuremap.com", + "meatydime.com", + "media-adrunner.mycomputer.com", + "media.funpic.de", + "media.net", + "media01.eu", + "media6degrees.com", + "mediaarea.eu", + "mediabridge.cc", + "mediafuse.com", + "mediageneral.com", + "mediaiqdigital.com", + "mediamath.com", + "mediamgr.ugo.com", + "mediaplazza.com", + "mediaplex.com", + "mediascale.de", + "mediaserver.bwinpartypartners.it", + "mediasmart.io", + "mediasquare.fr", + "mediatext.com", + "mediavine.com", + "mediavoice.com", + "mediax.angloinfo.com", + "mediaz.angloinfo.com", + "medleyads.com", + "medyanetads.com", + "meetrics.net", + "megacash.de", + "megastats.com", + "megawerbung.de", + "meltmilk.com", + "memorizeneck.com", + "mercuryace.com", + "merequartz.com", + "messagelists.com", + "metadsp.co.uk", + "metaffiliation.com", + "metajaws.com", + "metanetwork.com", + "methodcash.com", + "metrics-logger.spot.im", + "metrics.api.drift.com", + "metrics.articulate.com", + "metrics.cnn.com", + "metrics.foxnews.com", + "metrics.getrockerbox.com", + "metrics.gfycat.com", + "metrics.govexec.com", + "metrics.icloud.com", + "metrics.mzstatic.com", + "metrilo.com", + "mfadsrvr.com", + "mg2connext.com", + "mgid.com", + "microstatic.pl", + "microticker.com", + "milotree.com", + "mineinvoice.com", + "minewhat.com", + "minibilling.com", + "mintegral.com", + "mintfunnel.co", + "mittencattle.com", + "mix2ads.com", + "mixedreading.com", + "mixpanel.com", + "mkto-ab410147.com", + "mktoresp.com", + "ml314.com", + "mlm.de", + "mlsend.com", + "mltrk.io", + "mmismm.com", + "mmstat.com", + "mmtro.com", + "mntzrlt.net", + "moartraffic.com", + "moat.com", + "moatads.com", + "moatpixel.com", + "mobclix.com", + "mobfox.com", + "mobileanalytics.us-east-1.amazonaws.com", + "mobilefuse.com", + "modernpricing.com", + "mon-va.byteoversea.com", + "mon.byteoversea.com", + "monarchads.com", + "monetate.net", + "monetizer101.com", + "monkeyapes.com", + "monsterpops.com", + "mookie1.com", + "mopub.com", + "morefolks.com", + "motionlessmeeting.com", + "motionspots.com", + "mousestats.com", + "movad.net", + "movemeal.com", + "mparticle.com", + "mpstat.us", + "mr-rank.de", + "mrskincash.com", + "mstrlytcs.com", + "mtrcs.samba.tv", + "mtree.com", + "munchkin.marketo.net", + "mundanenail.com", + "mushroomgods.com", + "musiccounter.ru", + "muteknife.com", + "muwmedia.com", + "mxptint.net", + "myads.company", + "myads.net", + "myads.telkomsel.com", + "myaffiliateprogram.com", + "mybbc-analytics.files.bbci.co.uk", + "mybloglog.com", + "mybuys.com", + "mycounter.ua", + "mydas.mobi", + "mylead-tracking.tracknow.info", + "mylead.global", + "mylink-today.com", + "mypagerank.net", + "mypowermall.com", + "mysketchpad.com", + "mystat-in.net", + "mystat.pl", + "mytop-in.net", + "n69.com", + "naj.sk", + "nakedly.ai", + "nappyattack.com", + "nappyneck.com", + "nastydollars.com", + "nativeroll.tv", + "navegg.com", + "navigator.io", + "navrcholu.cz", + "ncaudienceexchange.com", + "ndparking.com", + "nebulacrescent.com", + "nedstatbasic.net", + "needyneedle.com", + "neighborlywatch.com", + "nend.net", + "neocounter.neoworx-blog-tools.net", + "nervoussummer.com", + "net-filter.com", + "netaffiliation.com", + "netagent.cz", + "netclickstats.com", + "netdirect.nl", + "netech.postaffiliatepro.com", + "netmera-web.com", + "netmera.com", + "netmng.com", + "netpool.netbookia.net", + "netshelter.net", + "neudesicmediagroup.com", + "newads.bangbros.com", + "newnet.qsrch.com", + "newnudecash.com", + "news-cdn.site", + "newsadsppush.com", + "newsbotnet.com", + "newt1.adultadworld.com", + "newt1.adultworld.com", + "nexac.com", + "nexage.com", + "ng3.ads.warnerbros.com", + "nitroclicks.com", + "nmtracking.netflix.com", + "noiselessplough.com", + "nondescriptcrowd.com", + "nondescriptnote.com", + "nondescriptstocking.com", + "novem.pl", + "nowaymail.com", + "npttech.com", + "nr-data.net", + "nr.mmcdn.com", + "nr.static.mmcdn.com", + "ns1p.net", + "ntv.io", + "ntvk1.ru", + "nullitics.com", + "nuseek.com", + "nzaza.com", + "o2.mouseflow.com", + "o333o.com", + "oafishobservation.com", + "oas.benchmark.fr", + "oas.repubblica.it", + "oas.roanoke.com", + "oas.toronto.com", + "oas.uniontrib.com", + "oascentral.chicagobusiness.com", + "oascentral.fortunecity.com", + "oascentral.register.com", + "objecthero.com", + "obscenesidewalk.com", + "oceancloudhosts.com", + "oclasrv.com", + "odbierz-bony.ovp.pl", + "oewa.at", + "offaces-butional.com", + "offer.fyber.com", + "offer.sponsorpay.com", + "offerforge.com", + "offermatica.com", + "offshoregeology.com", + "ogads-pa.googleapis.com", + "oglasi.posjetnica.com", + "ogury.com", + "ojrq.net", + "omg10.com", + "omnijay.com", + "omniture.com", + "omtrdc.net", + "onaudience.com", + "onclickads.net", + "onegg.site", + "onestat.com", + "onestatfree.com", + "online-metrix.net", + "online.miarroba.com", + "onlinecash.com", + "onlinecashmethod.com", + "onlinerewardcenter.com", + "onlinestarten.net", + "onscroll.com", + "onthe.io", + "opads.us", + "open.oneplus.net", + "openad.tf1.fr", + "openad.travelnow.com", + "openads.friendfinder.com", + "openads.org", + "openadsnetwork.com", + "openbid.pubmatic.com", + "openx.angelsgroup.org.uk", + "openx.cairo360.com", + "openx.net", + "openx.skinet.cz", + "openx.smcaen.fr", + "openx2.kytary.cz", + "operationchicken.com", + "opienetwork.com", + "opmnstr.com", + "oppuz.com", + "optimallimit.com", + "optimizely.com", + "optimost.com", + "optionsnomad.com", + "optmd.com", + "optmnstr.com", + "optmstr.com", + "optnmstr.com", + "optnx.com", + "orbsrv.com", + "orientedargument.com", + "orionember.com", + "ota.cartrawler.com", + "otto-images.developershed.com", + "ourdreamstaticpages.pages.dev", + "outbrain.com", + "overconfidentfood.com", + "overkick.com", + "overture.com", + "ow.pubmatic.com", + "owebmoney.ru", + "owlsr.us", + "owneriq.net", + "oxado.com", + "oxcash.com", + "oxen.hillcountrytexas.com", + "p-n.io", + "p7cloud.net", + "paa-reporting-advertising.amazon", + "page-checker.eu", + "pagead.l.google.com", + "pagefair.com", + "pagerank-ranking.de", + "pageranktop.com", + "painstakingpickle.com", + "paleleaf.com", + "panickypancake.com", + "panoramicplane.com", + "parachutehome.sjv.io", + "parchedsofa.com", + "parentpicture.com", + "parsely.com", + "parsimoniouspolice.com", + "partner-ads.com", + "partner.pelikan.cz", + "partnerad.l.google.com", + "partnerads.ysm.yahoo.com", + "partnercash.de", + "partnerhut.com", + "partnerlinks.io", + "partners.priceline.com", + "partplanes.com", + "passeura.com", + "paychat.fuse-cloud.com", + "paycounter.com", + "paypopup.com", + "pbnet.ru", + "pbterra.com", + "pc-tc.s3-eu-west-1.amazonaws.com", + "pcash.imlive.com", + "peep-auktion.de", + "peer39.com", + "pennyweb.com", + "pepperjamnetwork.com", + "percentmobile.com", + "perfectaudience.com", + "perfiliate.com", + "performancerevenue.com", + "performancerevenues.com", + "performancing.com", + "permutive.com", + "personagraph.com", + "pgl.example.com", + "pgl.example0101", + "pgmediaserve.com", + "pgpartner.com", + "pheedo.com", + "phoenix-adrunner.mycomputer.com", + "piano.io", + "pimproll.com", + "ping.ublock.org", + "pipedream.wistia.com", + "pippio.com", + "piquantpigs.com", + "pix.spot.im", + "pixel.condenastdigital.com", + "pixel.keywee.co", + "pixel.sojern.com", + "pixel.watch", + "pixel.yabidos.com", + "placed.com", + "placeframe.com", + "placidactivity.com", + "plausible.avris.it", + "plausibleio.workers.dev", + "play4traffic.com", + "playhaven.com", + "pleasantpump.com", + "plista.com", + "plotrabbit.com", + "pltraffic8.com", + "pluckypocket.com", + "plugrush.com", + "pocketfaucet.com", + "poemprompt.com", + "pointlessprofit.com", + "pointroll.com", + "pokkt.com", + "polishedfolly.com", + "polo.feathr.co", + "popads.net", + "popcash.net", + "popmixradio.com", + "popmyads.com", + "popplantation.com", + "popub.com", + "popunder.ru", + "popunhot1.blogspot.com", + "popup.msn.com", + "popupmoney.com", + "popupnation.com", + "popuptraffic.com", + "porngraph.com", + "porntrack.com", + "possibleboats.com", + "possiblepencil.com", + "post.spmailtechno.com", + "postback.iqm.com", + "postrelease.com", + "ppc.adhere.marchex.com", + "pr-star.de", + "praddpro.de", + "prchecker.info", + "prebid.org", + "predictad.com", + "premium-offers.com", + "presetrabbits.com", + "prettyeasycafe.com", + "previousplayground.com", + "prf.hn", + "priceypies.com", + "pricklydebt.com", + "prideproms.com", + "primetime.net", + "privatecash.com", + "prmtracking.com", + "pro-market.net", + "probablepartner.com", + "processplantation.com", + "proext.com", + "profero.com", + "profitrumour.com", + "programattik.com", + "projectwonderful.com", + "promo.badoink.com", + "promobenef.com", + "promos.bwin.it", + "promos.fling.com", + "promote.pair.com", + "pronetadvertising.com", + "propellerads.com", + "propellerclick.com", + "proper.io", + "props.id", + "protectcrev.com", + "protectpool.com", + "protectsubrev.com", + "protestcopy.com", + "proton-tm.com", + "protraffic.com", + "provenpixel.com", + "prpops.com", + "prsitecheck.com", + "prufenzo.xyz", + "pstmrk.it", + "pub.chez.com", + "pub.club-internet.fr", + "pub.hardware.fr", + "pub.network", + "pub.realmedia.fr", + "pubdirecte.com", + "publicidad.elmundo.es", + "publicidees.com", + "publicsofa.com", + "pubmine.com", + "pubnative.net", + "puffyloss.com", + "puffypaste.com", + "puffypull.com", + "puffypurpose.com", + "pureclarity.net", + "pushance.com", + "pushengage.com", + "pushno.com", + "pushtrack.co", + "px.dynamicyield.com", + "px.gfycat.com", + "pxf.io", + "pxl-mailtracker.com", + "pxl.iqm.com", + "pymx5.com", + "q.azcentral.com", + "q1connect.com", + "qa-analytics.com", + "qctop.com", + "ql.tc", + "qnsr.com", + "qrlsx.com", + "quantcast.com", + "quantcount.com", + "quantserve.com", + "quantummetric.com", + "quarterserver.de", + "quickkoala.io", + "quietknowledge.com", + "quiltruler.com", + "quinst.com", + "quirkysugar.com", + "quisma.com", + "quizzicalzephyr.com", + "r.drinksdirect.net", + "r.logrocket.io", + "r.marketing.dubaisothebys.com", + "r.msn.com", + "r.scoota.co", + "r.sibmail.havasit.com", + "r1.arts-mail.com", + "r1.visualwebsiteoptimizer.com", + "r2.visualwebsiteoptimizer.com", + "r3.visualwebsiteoptimizer.com", + "rabbitrifle.com", + "rackforstorage.com", + "radar.cedexis.com", + "radiate.com", + "radiateprose.com", + "rads.realadmin.pl", + "railwayreason.com", + "rambunctiousflock.com", + "rampidads.com", + "randkuj.xyz", + "rankchamp.de", + "ranking-charts.de", + "ranking-hits.de", + "ranking-links.de", + "rankingscout.com", + "rankyou.com", + "rapidcounter.com", + "raresummer.com", + "rate.ru", + "ratings.lycos.com", + "rayjump.com", + "rcadserver.com", + "re-direct.pl", + "reachjunction.com", + "reactx.com", + "readingguilt.com", + "readymoon.com", + "realcastmedia.com", + "realclever.com", + "realclix.com", + "realmedia-a800.d4p.net", + "realsrv.com", + "realtechnetwork.com", + "realtracker.com", + "rebelhen.com", + "rebelswing.com", + "rec5.visualwebsiteoptimizer.com", + "recapture.io", + "receptivereaction.com", + "recoco.it", + "reconditerake.com", + "record.bonniergaming.com", + "record.mrwin.com", + "redirectingat.com", + "redirectvoluum.com", + "redrection.pro", + "redshell.io", + "reduxmedia.com", + "referralware.com", + "referrer.disqus.com", + "regularplants.com", + "reklam.rfsl.se", + "reklama.mironet.cz", + "reklamcsere.hu", + "reklamdsp.com", + "relmaxtop.com", + "rememberdiscussion.com", + "remox.com", + "report-1.appmetrica.webvisor.com", + "report-2.appmetrica.webvisor.com", + "report-partners.appmetrica.yandex.net", + "report.ap.yandex-net.ru", + "report.appmetrica.yandex.net", + "republika.onet.pl", + "resalag.com", + "rescuerhino.com", + "resonantbrush.com", + "resonate.com", + "responsiveads.com", + "restrainstorm.com", + "retargeter.com", + "rev.iq", + "revcatch.com", + "revcontent.com", + "reveal.clearbit.com", + "revenuedirect.com", + "revenuehits.com", + "revive.haskovo.net", + "revive.netriota.hu", + "revive.plays.bg", + "revprotect.com", + "revstats.com", + "rexadvert.xyz", + "reyden-x.com", + "rhombusads.com", + "rhythmone.com", + "richaudience.com", + "richmails.com", + "richstring.com", + "rightstats.com", + "ringplant.com", + "ringsrecord.com", + "ritzyrepresentative.com", + "rlcdn.com", + "rle.ru", + "rmads.msn.com", + "rmedia.boston.com", + "roaddynamics.com", + "roar.com", + "robotreplay.com", + "rockabox.co", + "rockagainst.com", + "rockstarwriter.com", + "rok.com.com", + "rollconnection.com", + "rose.ixbt.com", + "rotabanner.com", + "roxr.net", + "rpt-ads.vidaahub.com", + "rqtrk.eu", + "rs6.net", + "rsc-ads.vidaahub.com", + "rsc-mntz.vidaahub.com", + "rta.dailymail.co.uk", + "rtb.gumgum.com", + "rtbadzesto.com", + "rtbflairads.com", + "rtbplatform.net", + "rtbpop.com", + "rtbpopd.com", + "rtmark.net", + "rtxplatform.com", + "ru4.com", + "rubiconproject.com", + "rum-http-intake.logs.datadoghq.com", + "rum-http-intake.logs.datadoghq.eu", + "runads.com", + "rundsp.com", + "ruralrobin.com", + "s.adroll.com", + "s.dmmew.com", + "s20dh7e9dh.com", + "s2d6.com", + "sabio.us", + "sadloaf.com", + "safeoffers.pro", + "sail-horizon.com", + "samplesamba.com", + "samsungacr.com", + "samsungads.com", + "sanalytics.disneyplus.com", + "sanity-dataplane.rudderstack.com", + "savoryorange.com", + "sbird.xyz", + "sbx.pagesjaunes.fr", + "sc-analytics.appspot.com", + "scambiobanner.aruba.it", + "scanscout.com", + "scarcesign.com", + "scaredsnakes.com", + "scaredsong.com", + "scarfsmash.com", + "scatteredheat.com", + "scintillatingscissors.com", + "scintillatingsilver.com", + "scissorsstatement.com", + "scopelight.com", + "scorecardresearch.com", + "scratch2cash.com", + "screechingfurniture.com", + "screechingstocking.com", + "screechingstove.com", + "scrubswim.com", + "seadform.net", + "searchmarketing.com", + "searchramp.com", + "secre.jp", + "secretspiders.com", + "secure.webconnect.net", + "securedopen-bp.com", + "securemetrics.apple.com", + "securemetrics.apple.com.cn", + "sedoparking.com", + "sedotracker.com", + "segment-cdn.producthunt.com", + "selectivesummer.com", + "semasio.net", + "sendmepixel.com", + "seraphichorizon.com", + "serendipityecho.com", + "serv0.com", + "servclick1move.com", + "serve.tercept.com", + "servedby-buysellads.com", + "servedbyadbutler.com", + "servedbyopenx.com", + "servethis.com", + "services.hearstmags.com", + "sessioncam.com", + "sexcounter.com", + "sexlist.com", + "sextracker.com", + "shadowmade.com", + "shakegoldfish.com", + "shareasale.com", + "sharethrough.com", + "shd247.click", + "sher.index.hu", + "shesubscriptions.com", + "shinystat.com", + "shinystat.it", + "shiveringspot.com", + "shiverscissors.com", + "shockinggrass.com", + "shoppingads.com", + "showads.pubmatic.com", + "shredform.com", + "shrillspoon.com", + "shxtrk.com", + "sicksmash.com", + "sidebar.angelfire.com", + "signalayer.com", + "signalszone.com", + "sillyscrew.com", + "silvermob.com", + "simpleanalytics.io", + "simplesafari.com", + "simpli.fi", + "simulateswing.com", + "sincerebuffalo.com", + "sinoa.com", + "sitedataprocessing.com", + "siteimproveanalytics.com", + "siteimproveanalytics.io", + "siteintercept.qualtrics.com", + "sitemeter.com", + "sixscissors.com", + "sixsigmatraffic.com", + "sizmek.com", + "skimresources.com", + "skisofa.com", + "skroutza.skroutz.gr", + "skylink.vn", + "slimesupplies.net", + "slopeaota.com", + "smaato.com", + "smart-data-systems.com", + "smart-traffik.com", + "smart-traffik.io", + "smart4ads.com", + "smartadserver.com", + "smartclip.net", + "smartlook.com", + "smartstream.tv", + "smartyads.com", + "smashquartz.com", + "smashsurprise.com", + "smetrics.10daily.com.au", + "smetrics.bestbuy.com", + "smetrics.ctv.ca", + "smetrics.fedex.com", + "smetrics.foxnews.com", + "smetrics.walgreens.com", + "smetrics.washingtonpost.com", + "smilewanted.com", + "smilingcattle.com", + "smoggysnakes.com", + "smrtb.com", + "snapads.com", + "snazzypoodle.com", + "snoobi.com", + "socialspark.com", + "softclick.com.br", + "soggysponge.com", + "soicos.com", + "sombersea.com", + "sombersquirrel.com", + "sombersurprise.com", + "somniture.stuff.co.nz", + "somoaudience.com", + "sonobi.com", + "sortable.com", + "sourcepoint.vice.com", + "sovrn.com", + "sp-a-q-f.ib-game.jp", + "spacash.com", + "spaceleadster.com", + "sparklingshelf.com", + "sparkstudios.com", + "speakol.com", + "specificmedia.co.uk", + "specificpop.com", + "speedomizer.com", + "speedshiftmedia.com", + "spellingthoughts.com", + "spezialreporte.de", + "spiffymachine.com", + "spinbox.techtracker.com", + "spinbox.versiontracker.com", + "spinnaker-js.com", + "spirebaboon.com", + "sponsorads.de", + "sponsorpro.de", + "spookysleet.com", + "spotlessstamp.com", + "spotscenered.info", + "spotx.tv", + "spotxchange.com", + "springbot.com", + "springserve.com", + "sprysummit.com", + "spulse.net", + "spylog.com", + "spywarelabs.com", + "spywords.com", + "srvmath.com", + "srvtrck.com", + "srwww1.com", + "sshowads.pubmatic.com", + "sskzlabs.com", + "st.dynamicyield.com", + "st.pubmatic.com", + "stack-sonar.com", + "stackadapt.com", + "stakingsmile.com", + "stalesummer.com", + "starffa.com", + "starkscale.com", + "starrynets.com", + "starsmarter.com", + "startapp.com", + "stat-track.com", + "stat.cliche.se", + "stat.dyna.ultraweb.hu", + "stat.pl", + "stat.webmedia.pl", + "stat.xiaomi.com", + "stat.zenon.net", + "stat24.com", + "stat24.meta.ua", + "statcounter.com", + "statdynamic.com", + "static-tracking.klaviyo.com", + "static.fmpub.net", + "static.itrack.it", + "static.kameleoon.com", + "staticads.btopenworld.com", + "statistik-gallup.net", + "statm.the-adult-company.com", + "stats.blogger.com", + "stats.hyperinzerce.cz", + "stats.merriam-webster.com", + "stats.mirrorfootball.co.uk", + "stats.nextgen-email.com", + "stats.olark.com", + "stats.pusher.com", + "stats.rdphv.net", + "stats.self.com", + "stats.stb-ottow.de", + "stats.townnews.com", + "stats.wordpress.com", + "stats.wp.com", + "stats2.self.com", + "stats4all.com", + "statserv.net", + "statsie.com", + "statxpress.com", + "steadfastsound.com", + "steadfastsystem.com", + "steelhouse.com", + "steelhousemedia.com", + "stickyadstv.com", + "stiffgame.com", + "stimulatingsneeze.com", + "stomachscience.com", + "stopstomach.com", + "storetail.io", + "storygize.net", + "strack.pubmatic.com", + "straightnest.com", + "stretchsquirrel.com", + "studycooking.com", + "stupendoussleet.com", + "stupendoussnow.com", + "subscribe.hearstmags.com", + "succeedscene.com", + "successbuffet.com", + "sugoicounter.com", + "sulkycook.com", + "summerobject.com", + "sumo.com", + "sumome.com", + "sunsetcampfires.com", + "superawesome.tv", + "superchichair.com", + "superclix.de", + "superficialsquare.com", + "supersonicads.com", + "superstats.com", + "supertop.ru", + "supertop100.com", + "supply.colossusssp.com", + "supportwaves.com", + "surfmusik-adserver.de", + "surveygizmobeacon.s3.amazonaws.com", + "sw88.espn.com", + "swan-swan-goose.com", + "swankysquare.com", + "swingslip.com", + "swordgoose.com", + "synonymoussticks.com", + "systemssummit.com", + "t.appsflyer.com", + "t.bawafx.com", + "t.carta.com", + "t.co", + "t.eloqua.com", + "t.email.superdrug.com", + "t.en25.com", + "t.firstpromoter.com", + "t.insigit.com", + "t.irtyd.com", + "t.leady.com", + "t.mmtrkr.com", + "t.news.browns-restaurants.co.uk", + "t.notif-colissimo-laposte.info", + "t.pie.org", + "t.podcast.co", + "t.pubmatic.com", + "t.salesmatemail.com", + "t.vacations.disneydestinations.com", + "t.visit.disneydestinations.com", + "t.visitorqueue.com", + "t.x.co", + "t1.rorystravelclub.co.uk", + "t1.rorytravelclub-news.co.uk", + "taboola.com", + "tag-demo.mention-me.com", + "tag.mention-me.com", + "tagcommander.com", + "tagger.opecloud.com", + "tags.tiqcdn.com", + "tagtoo.com", + "tailsweep.com", + "tailsweep.se", + "takethatad.com", + "tamgrt.com", + "tangibleteam.com", + "tangyamount.com", + "tapad.com", + "tapfiliate.com", + "tapinfluence.com", + "tapjoy.com", + "tappx.com", + "targad.de", + "target.microsoft.com", + "targeting.api.drift.com", + "targeting.nzme.arcpublishing.com", + "targeting.voxus.tv", + "targetingnow.com", + "targetnet.com", + "targetpoint.com", + "tatsumi-sys.jp", + "tawdryson.com", + "tcads.net", + "teads.tv", + "tealeaf.com", + "tealium.cbsnews.com", + "tealium.com", + "tealiumiq.com", + "tedioustooth.com", + "teenrevenue.com", + "telaria.com", + "telemetry.dropbox.com", + "telemetry.goodlifefitness.com", + "telemetry.malwarebytes.com", + "telemetry.v.dropbox.com", + "temelio.com", + "tend.io", + "tendertest.com", + "ter-jrnl-oc.vidaahub.com", + "terriblethumb.com", + "text-link-ads.com", + "textad.sexsearch.com", + "textads.biz", + "textlinks.com", + "tfag.de", + "the-ozone-project.com", + "theadex.com", + "theadhost.com", + "theadsparks.com", + "thebugs.ws", + "thecrazychili.com", + "themangotea.com", + "themoneytizer.com", + "therapistla.com", + "thewavebeats.com", + "thinkitten.com", + "thirdparty.bnc.lt", + "thirdrespect.com", + "thomastorch.com", + "throtle.io", + "thruport.com", + "thunderhead.com", + "tia.timeinc.net", + "ticketaunt.com", + "ticklesign.com", + "ticksel.com", + "tics.techdirt.com", + "tidaltv.com", + "tidysprite.com", + "tinybar.com", + "tinybluewhale.com", + "tinytendency.com", + "tiresomethunder.com", + "tkbo.com", + "tls.telemetry.swe.quicinc.com", + "tlvmedia.com", + "tm.br.de", + "tnkexchange.com", + "tns-counter.ru", + "toolforthought.com", + "top-casting-termine.de", + "top-site-list.com", + "top.list.ru", + "top.mail.ru", + "top100-images.rambler.ru", + "top100.mafia.ru", + "top123.ro", + "top20free.com", + "toplist.cz", + "toplist.pornhost.com", + "toplista.mw.hu", + "toplistcity.com", + "topsir.com", + "topsite.lv", + "topsites.com.br", + "topstats.com", + "totemcash.com", + "touchclarity.com", + "tour.brazzers.com", + "tr.api.fanbyte.com", + "track-eu.customer.io", + "track.adform.net", + "track.anchorfree.com", + "track.canva.com", + "track.contently.com", + "track.customer.io", + "track.effiliation.com", + "track.flexlinks.com", + "track.flexlinkspro.com", + "track.lettingaproperty.com", + "track.mailalert.io", + "track.mailerlite.com", + "track.miro.com", + "track.nationalgunrights.org", + "track.privacyatclearbit.com", + "track.pubmatic.com", + "track.segmetrics.io", + "track.smtpmessage.com", + "track.software-codes.com", + "track.spe.schoolmessenger.com", + "track.ultravpn.com", + "track.unear.net", + "track.vcdc.com", + "track.viewdeos.com", + "track1.viewdeos.com", + "trackalyzer.com", + "trackedlink.net", + "trackedweb.net", + "tracker.bannerflow.com", + "tracker.cdnbye.com", + "tracker.icerocket.com", + "tracker.metricswave.com", + "tracker.mmdlv.it", + "tracker.samplicio.us", + "tracking.epicgames.com", + "tracking.hyros.com", + "tracking.ibxlink.com", + "tracking.intentsify.io", + "tracking.intl.miui.com", + "tracking.jiffyworld.com", + "tracking.markethero.io", + "tracking.miui.com", + "tracking.netalerts.io", + "tracking.olx-st.com", + "tracking.orixa-media.com", + "tracking.shopstyle.com", + "tracking.thinkabt.com", + "tracking.wetter.at", + "tracking01.walmart.com", + "tracking101.com", + "tracking22.com", + "trackingsoft.com", + "trackmysales.com", + "tradeadexchange.com", + "tradedoubler.com", + "traffic-exchange.com", + "traffic.hyteck.de", + "trafficfactory.biz", + "trafficforce.com", + "trafficholder.com", + "traffichunt.com", + "trafficjunky.net", + "trafficleader.com", + "trafficrouter.io", + "trafficshop.com", + "trafficspaces.net", + "trafficstrategies.com", + "trafficswarm.com", + "trafficz.com", + "traffiq.com", + "trafic.ro", + "traktrafficflow.com", + "tranquilplume.com", + "travis.bosscasinos.com", + "trck.a8.net", + "trck.mtrgt.id", + "trcklion.com", + "treasuredata.com", + "trekdata.com", + "tremendoustime.com", + "tremorhub.com", + "trendcounter.com", + "trendmd.com", + "trialfire.com", + "tribalfusion.com", + "triplelift.com", + "triptease.io", + "trk.4ff.pro", + "trk.bc.shutterfly.com", + "trk.pinterest.com", + "trk.sayerfinancial.com", + "trk.techtarget.com", + "trk1.avdlink.net", + "trk42.net", + "trkn.us", + "trkoptimizer.com", + "trkpnt.ongage.net", + "trmit.com", + "truckstomatoes.com", + "truculentrate.com", + "truehits.net", + "truehits1.gits.net.th", + "truehits2.gits.net.th", + "trust.titanhq.com", + "trustx.org", + "tsyndicate.com", + "tsyndicate.net", + "tubemogul.com", + "tumbleicicle.com", + "turboadv.com", + "turn.com", + "twelvedawn.com", + "twittad.com", + "twyn.com", + "tynt.com", + "typicalteeth.com", + "tyroo.com", + "uarating.com", + "ucfunnel.com", + "udkcrj.com", + "udncoeln.com", + "uib.ff.avast.com", + "ukoffzeh.com", + "ultimateclixx.com", + "ultramercial.com", + "ultraoranges.com", + "unaccountablepie.com", + "unarmedindustry.com", + "unbecominglamp.com", + "understoodocean.com", + "undertone.com", + "unidentifiedanalytics.web.app", + "unloadyourself.com", + "unruly.co", + "unrulymedia.com", + "untd.com", + "unusualtitle.com", + "unwieldyhealth.com", + "unwieldyimpulse.com", + "upgradeyoga.com", + "upu.samsungelectronics.com", + "urbanlaurel.com", + "url9467.comms-2.zoopla.co.uk", + "urlcash.net", + "us.a1.yimg.com", + "userreplay.com", + "userreplay.net", + "users.maxcluster.net", + "utils.mediageneral.net", + "utl-1.com", + "uu.domainforlite.com", + "v1.cnzz.com", + "v1adserver.com", + "valerie.forbes.com", + "validclick.com", + "valuead.com", + "valueclick.com", + "valueclickmedia.com", + "valuecommerce.com", + "vanfireworks.com", + "vcommission.com", + "veille-referencement.com", + "velismedia.com", + "venetrigni.com", + "vengefulgrass.com", + "ventivmedia.com", + "venturead.com", + "vericlick.com", + "vertamedia.com", + "verticalmass.com", + "vervewireless.com", + "vgnp3trk.com", + "vibrantsundown.com", + "vid.pubmatic.com", + "vidcpm.com", + "video-stats.video.google.com", + "videoadex.com", + "videoadstech.org", + "videoegg.com", + "videostats.kakao.com", + "vidora.com", + "view4cash.de", + "viglink.com", + "vilenexus.com", + "virtualvincent.com", + "visiblemeasures.com", + "visistat.com", + "visitbox.de", + "visual-pagerank.fr", + "visualrevenue.com", + "vivads.net", + "vivtracking.com", + "vmmpxl.com", + "voicefive.com", + "volatilevessel.com", + "voluum.com", + "voluumtrk2.com", + "vpon.com", + "vrs.cz", + "vtracy.de", + "vungle.com", + "w55c.net", + "wa.and.co.uk", + "waardex.com", + "warmafterthought.com", + "washbanana.com", + "wdads.sx.atl.publicus.com", + "wdfl.co", + "web-stat.com", + "web.informer.com", + "web2.deja.com", + "webads.co.nz", + "webads.nl", + "webanalytics.zohodcm.com", + "webcash.nl", + "webcontentassessor.com", + "webcounter.cz", + "webcounter.goweb.de", + "webgains.com", + "weborama.com", + "weborama.fr", + "webpower.com", + "webreseau.com", + "webseoanalytics.com", + "webstat.channel4.com", + "webstat.com", + "webstat.net", + "webtrackerplus.com", + "webtraffic.se", + "webtraxx.de", + "webxcdn.com", + "welved.com", + "werbung.meteoxpress.com", + "wetrack.it", + "whaleads.com", + "wheredoyoucomefrom.ovh", + "whirlwealth.com", + "whiskyqueue.com", + "whisperingcascade.com", + "whisperingcrib.com", + "whisperingsummit.com", + "whoisonline.net", + "wholepagecache.com", + "wickedreports.com", + "widget.educationdynamics.com", + "widget.privy.com", + "wikia-ads.wikia.com", + "wikiquotations.com", + "win.iqm.com", + "window.nixnet.cz", + "wintricksbanner.googlepages.com", + "wirecomic.com", + "wirypaste.com", + "wisepops.com", + "witch-counter.de", + "wittypopcorn.com", + "wizaly.com", + "wl.spotify.com", + "wlmarketing.com", + "wondoads.de", + "woopra.com", + "worldrealize.com", + "worldwide-cash.net", + "worriednumber.com", + "wowfunnow.com", + "writerhubs.com", + "wt-eu02.net", + "wt.bankmillennium.pl", + "www-banner.chat.ru", + "www-google-analytics.l.google.com", + "www.dnps.com", + "www.kaplanindex.com", + "www.photo-ads.co.uk", + "www8.glam.com", + "wwwpromoter.com", + "x-traceur.com", + "x6.yakiuchi.com", + "xad.com", + "xapads.com", + "xchange.ro", + "xertive.com", + "xfreeservice.com", + "xg4ken.com", + "xiti.com", + "xplusone.com", + "xponsor.com", + "xpu.samsungelectronics.com", + "xq1.net", + "xtendmedia.com", + "xtracker.logimeter.com", + "xxxcounter.com", + "xxxmyself.com", + "y.ibsys.com", + "yab-adimages.s3.amazonaws.com", + "yadro.ru", + "yandexmetrica.com", + "yepads.com", + "yesads.com", + "yesadvertising.com", + "yieldads.com", + "yieldlab.net", + "yieldmanager.net", + "yieldmo.com", + "yieldoptimizer.com", + "yieldtraffic.com", + "yldbt.com", + "ymetrica1.com", + "yoads.net", + "yoggrt.com", + "youcandrawanything.com", + "youradexchange.com", + "ypu.samsungelectronics.com", + "zangocash.com", + "zanox-affiliate.de", + "zanox.com", + "zantracker.com", + "zarget.com", + "zdbb.net", + "zedo.com", + "zemanta.com", + "zencudo.co.uk", + "zenkreka.com", + "zenzuu.com", + "zephyrlabyrinth.com", + "zeus.developershed.com", + "zeusclicks.com", + "zeydoo.com", + "zion-telemetry.api.cnn.io", + "zippingcare.com", + "zlp6s.pw", + "zm232.com", + "zmedia.com", + "zonewedgeshaft.com", + "zpu.samsungelectronics.com", + "zqtk.net", + "zzhc.vnet.cn", + } +) diff --git a/scrapling/engines/toolbelt/navigation.py b/scrapling/engines/toolbelt/navigation.py index 9b233d8..01bee07 100644 --- a/scrapling/engines/toolbelt/navigation.py +++ b/scrapling/engines/toolbelt/navigation.py @@ -19,6 +19,27 @@ class ProxyDict(Struct): password: str = "" +def _is_domain_blocked(hostname: str, domains: frozenset) -> bool: + """Check if a hostname matches any blocked domain using O(1) frozenset lookups. + + Walks up the hostname's suffix chain: for "tracker.ads.doubleclick.net", + checks "tracker.ads.doubleclick.net", "ads.doubleclick.net", "doubleclick.net". + + :param hostname: The hostname to check. + :param domains: A frozenset of blocked domain names. + :return: True if the hostname or any of its parent domains is in the blocked set. + """ + if hostname in domains: + return True + idx = hostname.find(".") + while idx != -1: + suffix = hostname[idx + 1 :] + if "." in suffix and suffix in domains: + return True + idx = hostname.find(".", idx + 1) + return False + + def create_intercept_handler(disable_resources: bool, blocked_domains: Optional[Set[str]] = None) -> Callable: """Create a route handler that blocks both resource types and specific domains. @@ -27,7 +48,7 @@ def create_intercept_handler(disable_resources: bool, blocked_domains: Optional[ :return: A sync route handler function. """ disabled_resources = EXTRA_RESOURCES if disable_resources else set() - domains = blocked_domains or set() + domains = frozenset(blocked_domains) if blocked_domains else frozenset() def handler(route: Route): if route.request.resource_type in disabled_resources: @@ -35,7 +56,7 @@ def create_intercept_handler(disable_resources: bool, blocked_domains: Optional[ route.abort() elif domains: hostname = urlparse(route.request.url).hostname or "" - if any(hostname == d or hostname.endswith("." + d) for d in domains): + if _is_domain_blocked(hostname, domains): log.debug(f'Blocking request to blocked domain "{hostname}" ({route.request.url})') route.abort() else: @@ -54,7 +75,7 @@ def create_async_intercept_handler(disable_resources: bool, blocked_domains: Opt :return: An async route handler function. """ disabled_resources = EXTRA_RESOURCES if disable_resources else set() - domains = blocked_domains or set() + domains = frozenset(blocked_domains) if blocked_domains else frozenset() async def handler(route: async_Route): if route.request.resource_type in disabled_resources: @@ -62,7 +83,7 @@ def create_async_intercept_handler(disable_resources: bool, blocked_domains: Opt await route.abort() elif domains: hostname = urlparse(route.request.url).hostname or "" - if any(hostname == d or hostname.endswith("." + d) for d in domains): + if _is_domain_blocked(hostname, domains): log.debug(f'Blocking request to blocked domain "{hostname}" ({route.request.url})') await route.abort() else: diff --git a/scrapling/fetchers/chrome.py b/scrapling/fetchers/chrome.py index 594b178..0c06aa1 100644 --- a/scrapling/fetchers/chrome.py +++ b/scrapling/fetchers/chrome.py @@ -15,6 +15,7 @@ class DynamicFetcher(BaseFetcher): :param headless: Run the browser in headless/hidden (default), or headful/visible mode. :param disable_resources: Drop requests for unnecessary resources for a speed boost. :param blocked_domains: A set of domain names to block requests to. Subdomains are also matched (e.g., ``"example.com"`` blocks ``"sub.example.com"`` too). + :param block_ads: Block requests to ~3,500 known ad/tracking domains. Can be combined with ``blocked_domains``. :param useragent: Pass a useragent string to be used. Otherwise the fetcher will generate a real Useragent of the same browser and use it. :param cookies: Set cookies for the next request. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. @@ -55,6 +56,7 @@ class DynamicFetcher(BaseFetcher): :param headless: Run the browser in headless/hidden (default), or headful/visible mode. :param disable_resources: Drop requests for unnecessary resources for a speed boost. :param blocked_domains: A set of domain names to block requests to. Subdomains are also matched (e.g., ``"example.com"`` blocks ``"sub.example.com"`` too). + :param block_ads: Block requests to ~3,500 known ad/tracking domains. Can be combined with ``blocked_domains``. :param useragent: Pass a useragent string to be used. Otherwise the fetcher will generate a real Useragent of the same browser and use it. :param cookies: Set cookies for the next request. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. diff --git a/scrapling/fetchers/stealth_chrome.py b/scrapling/fetchers/stealth_chrome.py index 1cf89f3..0c7134c 100644 --- a/scrapling/fetchers/stealth_chrome.py +++ b/scrapling/fetchers/stealth_chrome.py @@ -20,6 +20,7 @@ class StealthyFetcher(BaseFetcher): :param disable_resources: Drop requests for unnecessary resources for a speed boost. Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. :param blocked_domains: A set of domain names to block requests to. Subdomains are also matched (e.g., ``"example.com"`` blocks ``"sub.example.com"`` too). + :param block_ads: Block requests to ~3,500 known ad/tracking domains. Can be combined with ``blocked_domains``. :param useragent: Pass a useragent string to be used. Otherwise the fetcher will generate a real Useragent of the same browser and use it. :param cookies: Set cookies for the next request. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. @@ -69,6 +70,7 @@ class StealthyFetcher(BaseFetcher): :param disable_resources: Drop requests for unnecessary resources for a speed boost. Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. :param blocked_domains: A set of domain names to block requests to. Subdomains are also matched (e.g., ``"example.com"`` blocks ``"sub.example.com"`` too). + :param block_ads: Block requests to ~3,500 known ad/tracking domains. Can be combined with ``blocked_domains``. :param useragent: Pass a useragent string to be used. Otherwise the fetcher will generate a real Useragent of the same browser and use it. :param cookies: Set cookies for the next request. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. diff --git a/tests/fetchers/test_utils.py b/tests/fetchers/test_utils.py index bebb5fd..942835f 100644 --- a/tests/fetchers/test_utils.py +++ b/tests/fetchers/test_utils.py @@ -5,11 +5,9 @@ from scrapling.engines.toolbelt.navigation import ( construct_proxy_dict, create_intercept_handler, create_async_intercept_handler, + _is_domain_blocked, ) -from scrapling.engines.toolbelt.fingerprints import ( - get_os_name, - generate_headers -) +from scrapling.engines.toolbelt.fingerprints import get_os_name, generate_headers @pytest.fixture @@ -148,31 +146,19 @@ class TestConstructProxyDict: """Test a basic proxy string""" result = construct_proxy_dict("http://proxy.example.com:8080") - expected = { - "server": "http://proxy.example.com:8080", - "username": "", - "password": "" - } + expected = {"server": "http://proxy.example.com:8080", "username": "", "password": ""} assert result == expected def test_proxy_string_with_auth(self): """Test proxy string with authentication""" result = construct_proxy_dict("http://user:pass@proxy.example.com:8080") - expected = { - "server": "http://proxy.example.com:8080", - "username": "user", - "password": "pass" - } + expected = {"server": "http://proxy.example.com:8080", "username": "user", "password": "pass"} assert result == expected def test_proxy_dict_input(self): """Test proxy dictionary input""" - input_dict = { - "server": "http://proxy.example.com:8080", - "username": "user", - "password": "pass" - } + input_dict = {"server": "http://proxy.example.com:8080", "username": "user", "password": "pass"} result = construct_proxy_dict(input_dict) assert result == input_dict @@ -182,11 +168,7 @@ class TestConstructProxyDict: input_dict = {"server": "http://proxy.example.com:8080"} result = construct_proxy_dict(input_dict) - expected = { - "server": "http://proxy.example.com:8080", - "username": "", - "password": "" - } + expected = {"server": "http://proxy.example.com:8080", "username": "", "password": ""} assert result == expected def test_invalid_proxy_string(self): @@ -240,7 +222,7 @@ class TestResponse: cookies={"session": "abc123"}, headers={"Content-Type": "text/html"}, request_headers={"User-Agent": "Test"}, - encoding="utf-8" + encoding="utf-8", ) assert response.url == "https://example.com" @@ -250,7 +232,7 @@ class TestResponse: def test_response_with_bytes_content(self): """Test Response with 'bytes' content""" - content_bytes = "Test".encode('utf-8') + content_bytes = "Test".encode("utf-8") response = Response( url="https://example.com", @@ -259,7 +241,7 @@ class TestResponse: reason="OK", cookies={}, headers={}, - request_headers={} + request_headers={}, ) # Should handle 'bytes' content properly @@ -268,6 +250,7 @@ class TestResponse: class _MockRequest: """Minimal mock for Playwright's Request object.""" + def __init__(self, url: str, resource_type: str = "document"): self.url = url self.resource_type = resource_type @@ -275,6 +258,7 @@ class _MockRequest: class _MockRoute: """Minimal mock for Playwright's sync Route object.""" + def __init__(self, url: str, resource_type: str = "document"): self.request = _MockRequest(url, resource_type) self.aborted = False @@ -289,6 +273,7 @@ class _MockRoute: class _AsyncMockRoute: """Minimal mock for Playwright's async Route object.""" + def __init__(self, url: str, resource_type: str = "document"): self.request = _MockRequest(url, resource_type) self.aborted = False @@ -411,3 +396,90 @@ class TestCreateAsyncInterceptHandler: route = _AsyncMockRoute("https://notexample.com/page") await handler(route) assert route.continued + + +class TestIsDomainBlocked: + """Test the frozenset-based domain matching helper.""" + + def test_exact_match(self): + domains = frozenset({"doubleclick.net"}) + assert _is_domain_blocked("doubleclick.net", domains) is True + + def test_subdomain_match(self): + domains = frozenset({"doubleclick.net"}) + assert _is_domain_blocked("ads.doubleclick.net", domains) is True + + def test_deep_subdomain_match(self): + domains = frozenset({"doubleclick.net"}) + assert _is_domain_blocked("tracker.ads.doubleclick.net", domains) is True + + def test_no_partial_match(self): + domains = frozenset({"doubleclick.net"}) + assert _is_domain_blocked("notdoubleclick.net", domains) is False + + def test_no_match(self): + domains = frozenset({"doubleclick.net"}) + assert _is_domain_blocked("example.com", domains) is False + + def test_empty_domains(self): + assert _is_domain_blocked("example.com", frozenset()) is False + + def test_multiple_domains(self): + domains = frozenset({"ads.com", "tracker.io", "doubleclick.net"}) + assert _is_domain_blocked("cdn.ads.com", domains) is True + assert _is_domain_blocked("tracker.io", domains) is True + assert _is_domain_blocked("safe.example.com", domains) is False + + +class TestAdDomains: + """Test the built-in ad domain list.""" + + def test_ad_domains_is_frozenset(self): + from scrapling.engines.toolbelt.ad_domains import AD_DOMAINS + + assert isinstance(AD_DOMAINS, frozenset) + + def test_ad_domains_has_entries(self): + from scrapling.engines.toolbelt.ad_domains import AD_DOMAINS + + assert len(AD_DOMAINS) > 1000 + + def test_ad_domains_contains_known_entries(self): + from scrapling.engines.toolbelt.ad_domains import AD_DOMAINS + + assert "doubleclick.net" in AD_DOMAINS + assert "googlesyndication.com" in AD_DOMAINS + + +class TestBlockAdsConfig: + """Test that block_ads merges ad domains into blocked_domains at config level.""" + + def test_block_ads_populates_blocked_domains(self): + from scrapling.engines._browsers._validators import PlaywrightConfig + + config = PlaywrightConfig(block_ads=True) + assert config.blocked_domains is not None + assert len(config.blocked_domains) > 1000 + assert "doubleclick.net" in config.blocked_domains + + def test_block_ads_false_leaves_blocked_domains_none(self): + from scrapling.engines._browsers._validators import PlaywrightConfig + + config = PlaywrightConfig(block_ads=False) + assert config.blocked_domains is None + + def test_block_ads_merges_with_user_domains(self): + from scrapling.engines._browsers._validators import PlaywrightConfig + + user_domains = {"my-custom-block.com"} + config = PlaywrightConfig(block_ads=True, blocked_domains=user_domains) + assert config.blocked_domains is not None + assert "my-custom-block.com" in config.blocked_domains + assert "doubleclick.net" in config.blocked_domains + + def test_block_ads_does_not_modify_original_set(self): + from scrapling.engines._browsers._validators import PlaywrightConfig + + user_domains = {"my-custom-block.com"} + _ = PlaywrightConfig(block_ads=True, blocked_domains=user_domains) + assert len(user_domains) == 1 From 0678ed1406f0e49d292f3974a69f79f924cb30b7 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 12 Apr 2026 18:00:41 +0200 Subject: [PATCH 02/12] fix(shell): add missing parameters to the shell signature --- scrapling/core/_shell_signatures.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scrapling/core/_shell_signatures.py b/scrapling/core/_shell_signatures.py index ce509b6..afa482d 100644 --- a/scrapling/core/_shell_signatures.py +++ b/scrapling/core/_shell_signatures.py @@ -2,6 +2,7 @@ from scrapling.core._types import ( Any, Dict, List, + Set, Tuple, Sequence, Callable, @@ -58,7 +59,12 @@ _FETCH_PARAMS = { "cdp_url": Optional[str], "useragent": Optional[str], "extra_flags": Optional[List[str]], + "blocked_domains": Optional[Set[str]], "block_ads": bool, + "retries": int, + "retry_delay": int | float, + "capture_xhr": str | None, + "executable_path": Optional[str], } _STEALTHY_FETCH_PARAMS = { @@ -85,7 +91,12 @@ _STEALTHY_FETCH_PARAMS = { "cdp_url": Optional[str], "useragent": Optional[str], "extra_flags": Optional[List[str]], + "blocked_domains": Optional[Set[str]], "block_ads": bool, + "retries": int, + "retry_delay": int | float, + "capture_xhr": str | None, + "executable_path": Optional[str], "allow_webgl": bool, "hide_canvas": bool, "block_webrtc": bool, From be28fe16ec1501e1f534b007b5263ac06b301ebe Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 12 Apr 2026 18:03:03 +0200 Subject: [PATCH 03/12] feat(browsers): add new feature to enable DNS-over-HTTP to prevent DNS leaks --- scrapling/cli.py | 11 +++++++++++ scrapling/core/_shell_signatures.py | 2 ++ scrapling/engines/_browsers/_base.py | 7 +++++++ scrapling/engines/_browsers/_types.py | 1 + scrapling/engines/_browsers/_validators.py | 1 + scrapling/fetchers/chrome.py | 2 ++ scrapling/fetchers/stealth_chrome.py | 2 ++ 7 files changed, 26 insertions(+) diff --git a/scrapling/cli.py b/scrapling/cli.py index 9ca0dd4..3327dcb 100644 --- a/scrapling/cli.py +++ b/scrapling/cli.py @@ -309,6 +309,11 @@ def _common_browser_options(f): default=True, help="Run browser in headless mode (default: True)", ), + option( + "--dns-over-https/--no-dns-over-https", + default=False, + help="Route DNS through Cloudflare's DoH to prevent DNS leaks when using proxies (default: False)", + ), option( "--block-ads/--no-block-ads", default=False, @@ -503,6 +508,7 @@ def __build_browser_kwargs( real_chrome, proxy, parsed_headers, + dns_over_https, block_ads, ) -> Dict[str, Any]: """Build shared kwargs dict for browser-based commands.""" @@ -513,6 +519,7 @@ def __build_browser_kwargs( "timeout": timeout, "locale": locale, "real_chrome": real_chrome, + "dns_over_https": dns_over_https, "block_ads": block_ads, } if wait > 0: @@ -545,6 +552,7 @@ def fetch( proxy, extra_headers, ai_targeted, + dns_over_https, block_ads, ): """Opens up a browser and fetch content using DynamicFetcher.""" @@ -560,6 +568,7 @@ def fetch( real_chrome, proxy, parsed_headers, + dns_over_https, block_ads, ) from scrapling.fetchers import DynamicFetcher @@ -606,6 +615,7 @@ def stealthy_fetch( allow_webgl, hide_canvas, ai_targeted, + dns_over_https, block_ads, ): """Opens up a browser with advanced stealth features and fetch content using StealthyFetcher.""" @@ -621,6 +631,7 @@ def stealthy_fetch( real_chrome, proxy, parsed_headers, + dns_over_https, block_ads, ) kwargs.update( diff --git a/scrapling/core/_shell_signatures.py b/scrapling/core/_shell_signatures.py index afa482d..b2340fb 100644 --- a/scrapling/core/_shell_signatures.py +++ b/scrapling/core/_shell_signatures.py @@ -65,6 +65,7 @@ _FETCH_PARAMS = { "retry_delay": int | float, "capture_xhr": str | None, "executable_path": Optional[str], + "dns_over_https": bool, } _STEALTHY_FETCH_PARAMS = { @@ -97,6 +98,7 @@ _STEALTHY_FETCH_PARAMS = { "retry_delay": int | float, "capture_xhr": str | None, "executable_path": Optional[str], + "dns_over_https": bool, "allow_webgl": bool, "hide_canvas": bool, "block_webrtc": bool, diff --git a/scrapling/engines/_browsers/_base.py b/scrapling/engines/_browsers/_base.py index 2d42b08..c9d390c 100644 --- a/scrapling/engines/_browsers/_base.py +++ b/scrapling/engines/_browsers/_base.py @@ -455,6 +455,13 @@ class BaseSessionMixin: if config.extra_flags or extra_flags: flags = list(set(tuple(flags) + tuple(config.extra_flags or extra_flags or ()))) + if config.dns_over_https: + doh_flag = "--dns-over-https-templates=https://cloudflare-dns.com/dns-query" + if isinstance(flags, list): + flags.append(doh_flag) + else: + flags = list(flags) + [doh_flag] + self._browser_options.update( { "args": flags, diff --git a/scrapling/engines/_browsers/_types.py b/scrapling/engines/_browsers/_types.py index 932b2e2..5030480 100644 --- a/scrapling/engines/_browsers/_types.py +++ b/scrapling/engines/_browsers/_types.py @@ -93,6 +93,7 @@ class PlaywrightSession(TypedDict, total=False): retry_delay: int | float capture_xhr: str | None executable_path: Optional[str] + dns_over_https: bool class PlaywrightFetchParams(TypedDict, total=False): diff --git a/scrapling/engines/_browsers/_validators.py b/scrapling/engines/_browsers/_validators.py index 8d47061..f7bb34e 100644 --- a/scrapling/engines/_browsers/_validators.py +++ b/scrapling/engines/_browsers/_validators.py @@ -90,6 +90,7 @@ class PlaywrightConfig(Struct, kw_only=True, frozen=False, weakref=True): retry_delay: Seconds = 1 capture_xhr: str | None = None executable_path: Optional[str] = None + dns_over_https: bool = False def __post_init__(self): # pragma: no cover """Custom validation after msgspec validation""" diff --git a/scrapling/fetchers/chrome.py b/scrapling/fetchers/chrome.py index 0c06aa1..f7795b7 100644 --- a/scrapling/fetchers/chrome.py +++ b/scrapling/fetchers/chrome.py @@ -16,6 +16,7 @@ class DynamicFetcher(BaseFetcher): :param disable_resources: Drop requests for unnecessary resources for a speed boost. :param blocked_domains: A set of domain names to block requests to. Subdomains are also matched (e.g., ``"example.com"`` blocks ``"sub.example.com"`` too). :param block_ads: Block requests to ~3,500 known ad/tracking domains. Can be combined with ``blocked_domains``. + :param dns_over_https: Route DNS queries through Cloudflare's DNS-over-HTTPS to prevent DNS leaks when using proxies. :param useragent: Pass a useragent string to be used. Otherwise the fetcher will generate a real Useragent of the same browser and use it. :param cookies: Set cookies for the next request. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. @@ -57,6 +58,7 @@ class DynamicFetcher(BaseFetcher): :param disable_resources: Drop requests for unnecessary resources for a speed boost. :param blocked_domains: A set of domain names to block requests to. Subdomains are also matched (e.g., ``"example.com"`` blocks ``"sub.example.com"`` too). :param block_ads: Block requests to ~3,500 known ad/tracking domains. Can be combined with ``blocked_domains``. + :param dns_over_https: Route DNS queries through Cloudflare's DNS-over-HTTPS to prevent DNS leaks when using proxies. :param useragent: Pass a useragent string to be used. Otherwise the fetcher will generate a real Useragent of the same browser and use it. :param cookies: Set cookies for the next request. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. diff --git a/scrapling/fetchers/stealth_chrome.py b/scrapling/fetchers/stealth_chrome.py index 0c7134c..70ce937 100644 --- a/scrapling/fetchers/stealth_chrome.py +++ b/scrapling/fetchers/stealth_chrome.py @@ -21,6 +21,7 @@ class StealthyFetcher(BaseFetcher): Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. :param blocked_domains: A set of domain names to block requests to. Subdomains are also matched (e.g., ``"example.com"`` blocks ``"sub.example.com"`` too). :param block_ads: Block requests to ~3,500 known ad/tracking domains. Can be combined with ``blocked_domains``. + :param dns_over_https: Route DNS queries through Cloudflare's DNS-over-HTTPS to prevent DNS leaks when using proxies. :param useragent: Pass a useragent string to be used. Otherwise the fetcher will generate a real Useragent of the same browser and use it. :param cookies: Set cookies for the next request. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. @@ -71,6 +72,7 @@ class StealthyFetcher(BaseFetcher): Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. :param blocked_domains: A set of domain names to block requests to. Subdomains are also matched (e.g., ``"example.com"`` blocks ``"sub.example.com"`` too). :param block_ads: Block requests to ~3,500 known ad/tracking domains. Can be combined with ``blocked_domains``. + :param dns_over_https: Route DNS queries through Cloudflare's DNS-over-HTTPS to prevent DNS leaks when using proxies. :param useragent: Pass a useragent string to be used. Otherwise the fetcher will generate a real Useragent of the same browser and use it. :param cookies: Set cookies for the next request. :param network_idle: Wait for the page until there are no network connections for at least 500 ms. From 131b729f228d30b5882caff051620ea44eaad0ac Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 12 Apr 2026 18:07:38 +0200 Subject: [PATCH 04/12] docs: adding the new features --- docs/fetching/dynamic.md | 2 ++ docs/fetching/stealthy.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/fetching/dynamic.md b/docs/fetching/dynamic.md index 5e118f6..e8ce9db 100644 --- a/docs/fetching/dynamic.md +++ b/docs/fetching/dynamic.md @@ -88,6 +88,8 @@ Scrapling provides many options with this fetcher and its session classes. To ma | additional_args | Additional arguments to be passed to Playwright's context as additional settings, and they take higher priority than Scrapling's settings. | ✔️ | | selector_config | A dictionary of custom parsing arguments to be used when creating the final `Selector`/`Response` class. | ✔️ | | blocked_domains | A set of domain names to block requests to. Subdomains are also matched (e.g., `"example.com"` blocks `"sub.example.com"` too). | ✔️ | +| block_ads | Block requests to ~3,500 known ad/tracking domains. Can be combined with `blocked_domains`. | ✔️ | +| dns_over_https | Route DNS queries through Cloudflare's DNS-over-HTTPS to prevent DNS leaks when using proxies. | ✔️ | | proxy_rotator | A `ProxyRotator` instance for automatic proxy rotation. Cannot be combined with `proxy`. | ✔️ | | retries | Number of retry attempts for failed requests. Defaults to 3. | ✔️ | | retry_delay | Seconds to wait between retry attempts. Defaults to 1. | ✔️ | diff --git a/docs/fetching/stealthy.md b/docs/fetching/stealthy.md index 22b4aec..5b7c042 100644 --- a/docs/fetching/stealthy.md +++ b/docs/fetching/stealthy.md @@ -69,6 +69,8 @@ Scrapling provides many options with this fetcher and its session classes. Befor | additional_args | Additional arguments to be passed to Playwright's context as additional settings, and they take higher priority than Scrapling's settings. | ✔️ | | selector_config | A dictionary of custom parsing arguments to be used when creating the final `Selector`/`Response` class. | ✔️ | | blocked_domains | A set of domain names to block requests to. Subdomains are also matched (e.g., `"example.com"` blocks `"sub.example.com"` too). | ✔️ | +| block_ads | Block requests to ~3,500 known ad/tracking domains. Can be combined with `blocked_domains`. | ✔️ | +| dns_over_https | Route DNS queries through Cloudflare's DNS-over-HTTPS to prevent DNS leaks when using proxies. | ✔️ | | proxy_rotator | A `ProxyRotator` instance for automatic proxy rotation. Cannot be combined with `proxy`. | ✔️ | | retries | Number of retry attempts for failed requests. Defaults to 3. | ✔️ | | retry_delay | Seconds to wait between retry attempts. Defaults to 1. | ✔️ | From ad6fd5284553f4160f9e2aa6f002147b64e6064c Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 12 Apr 2026 18:11:02 +0200 Subject: [PATCH 05/12] perf: force ad blocking on the MCP server and when the AI mode is activated on CLI --- scrapling/cli.py | 2 ++ scrapling/core/ai.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/scrapling/cli.py b/scrapling/cli.py index 3327dcb..47056d9 100644 --- a/scrapling/cli.py +++ b/scrapling/cli.py @@ -53,6 +53,8 @@ def __Request_and_Save( if not output_path.is_absolute(): output_path = Path.cwd() / output_file + if ai_targeted: + kwargs.setdefault("block_ads", True) response = fetcher_func(url, **kwargs) Convertor.write_content_to_file(response, str(output_path), css_selector, main_content_only=ai_targeted) log.info(f"Content successfully saved to '{output_path}'") diff --git a/scrapling/core/ai.py b/scrapling/core/ai.py index 9b33821..060cb20 100644 --- a/scrapling/core/ai.py +++ b/scrapling/core/ai.py @@ -183,6 +183,7 @@ class ScraplingMCPServer: cookies=cookies, cdp_url=cdp_url, headless=headless, + block_ads=True, max_pages=max_pages, useragent=useragent, timezone_id=timezone_id, @@ -569,6 +570,7 @@ class ScraplingMCPServer: cookies=cookies, cdp_url=cdp_url, headless=headless, + block_ads=True, max_pages=len(urls), useragent=useragent, timezone_id=timezone_id, @@ -777,6 +779,7 @@ class ScraplingMCPServer: timeout=timeout, cookies=cookies, headless=headless, + block_ads=True, useragent=useragent, timezone_id=timezone_id, real_chrome=real_chrome, From 9a7fb0b426010f7a95e0c14f3fdb273f1afeae5e Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 12 Apr 2026 18:20:56 +0200 Subject: [PATCH 06/12] docs: adding the new features and updating the translated pages --- README.md | 3 ++- docs/README_AR.md | 3 ++- docs/README_CN.md | 3 ++- docs/README_DE.md | 3 ++- docs/README_ES.md | 3 ++- docs/README_FR.md | 3 ++- docs/README_JP.md | 3 ++- docs/README_KR.md | 3 ++- docs/README_RU.md | 3 ++- docs/ai/mcp-server.md | 1 + docs/cli/extract-commands.md | 6 +++++- docs/index.md | 3 ++- 12 files changed, 26 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 408cfef..7bf2952 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,8 @@ MySpider().start() - **Anti-bot Bypass**: Advanced stealth capabilities with `StealthyFetcher` and fingerprint spoofing. Can easily bypass all types of Cloudflare's Turnstile/Interstitial with automation. - **Session Management**: Persistent session support with `FetcherSession`, `StealthySession`, and `DynamicSession` classes for cookie and state management across requests. - **Proxy Rotation**: Built-in `ProxyRotator` with cyclic or custom rotation strategies across all session types, plus per-request proxy overrides. -- **Domain Blocking**: Block requests to specific domains (and their subdomains) in browser-based fetchers. +- **Domain & Ad Blocking**: Block requests to specific domains (and their subdomains) or enable built-in ad blocking (~3,500 known ad/tracker domains) in browser-based fetchers. +- **DNS Leak Prevention**: Optional DNS-over-HTTPS support to route DNS queries through Cloudflare's DoH, preventing DNS leaks when using proxies. - **Async Support**: Complete async support across all fetchers and dedicated async session classes. ### Adaptive Scraping & AI Integration diff --git a/docs/README_AR.md b/docs/README_AR.md index c0de787..ed23dd9 100644 --- a/docs/README_AR.md +++ b/docs/README_AR.md @@ -219,7 +219,8 @@ MySpider().start() - **تجاوز مكافحة الروبوتات**: قدرات تخفي متقدمة مع `StealthyFetcher` وانتحال fingerprint. يمكنه تجاوز جميع أنواع Turnstile/Interstitial من Cloudflare بسهولة بالأتمتة. - **إدارة الجلسات**: دعم الجلسات المستمرة مع فئات `FetcherSession` و`StealthySession` و`DynamicSession` لإدارة ملفات تعريف الارتباط والحالة عبر الطلبات. - **تدوير Proxy**: `ProxyRotator` مدمج مع استراتيجيات التدوير الدوري أو المخصصة عبر جميع أنواع الجلسات، بالإضافة إلى تجاوزات Proxy لكل طلب. -- **حظر النطاقات**: حظر الطلبات إلى نطاقات محددة (ونطاقاتها الفرعية) في الجوالب المعتمدة على المتصفح. +- **حظر النطاقات والإعلانات**: حظر الطلبات إلى نطاقات محددة (ونطاقاتها الفرعية) أو تفعيل حظر الإعلانات المدمج (~3,500 نطاق إعلانات/تتبع معروف) في الجوالب المعتمدة على المتصفح. +- **منع تسرب DNS**: دعم اختياري لـ DNS-over-HTTPS لتوجيه استعلامات DNS عبر Cloudflare DoH، مما يمنع تسرب DNS عند استخدام Proxy. - **دعم Async**: دعم async كامل عبر جميع الجوالب وفئات الجلسات async المخصصة. ### الاستخراج التكيفي والتكامل مع الذكاء الاصطناعي diff --git a/docs/README_CN.md b/docs/README_CN.md index 6cb9d4e..7222081 100644 --- a/docs/README_CN.md +++ b/docs/README_CN.md @@ -219,7 +219,8 @@ MySpider().start() - **反机器人绕过**:使用 `StealthyFetcher` 的高级隐秘功能和 fingerprint 伪装。可以轻松自动绕过所有类型的 Cloudflare Turnstile/Interstitial。 - **Session 管理**:使用 `FetcherSession`、`StealthySession` 和 `DynamicSession` 类实现持久化 Session 支持,用于跨请求的 cookie 和状态管理。 - **Proxy 轮换**:内置 `ProxyRotator`,支持轮询或自定义策略,适用于所有 Session 类型,并支持按请求覆盖 Proxy。 -- **域名屏蔽**:在基于浏览器的 Fetcher 中屏蔽对特定域名(及其子域名)的请求。 +- **域名和广告屏蔽**:在基于浏览器的 Fetcher 中屏蔽对特定域名(及其子域名)的请求,或启用内置广告屏蔽(约 3,500 个已知广告/追踪域名)。 +- **DNS 泄漏防护**:可选的 DNS-over-HTTPS 支持,通过 Cloudflare 的 DoH 路由 DNS 查询,防止使用代理时的 DNS 泄漏。 - **Async 支持**:所有 Fetcher 和专用 async Session 类的完整 async 支持。 ### 自适应抓取和 AI 集成 diff --git a/docs/README_DE.md b/docs/README_DE.md index 82d336f..f6f0fc9 100644 --- a/docs/README_DE.md +++ b/docs/README_DE.md @@ -219,7 +219,8 @@ MySpider().start() - **Anti-Bot-Umgehung**: Erweiterte Stealth-Fähigkeiten mit `StealthyFetcher` und Fingerprint-Spoofing. Kann alle Arten von Cloudflares Turnstile/Interstitial einfach mit Automatisierung umgehen. - **Session-Verwaltung**: Persistente Session-Unterstützung mit den Klassen `FetcherSession`, `StealthySession` und `DynamicSession` für Cookie- und Zustandsverwaltung über Anfragen hinweg. - **Proxy-Rotation**: Integrierter `ProxyRotator` mit zyklischen oder benutzerdefinierten Rotationsstrategien über alle Session-Typen hinweg, plus Proxy-Überschreibungen pro Anfrage. -- **Domain-Blockierung**: Anfragen an bestimmte Domains (und deren Subdomains) in browserbasierten Fetchern blockieren. +- **Domain- & Werbeblockierung**: Anfragen an bestimmte Domains (und deren Subdomains) blockieren oder die integrierte Werbeblockierung (~3.500 bekannte Werbe-/Tracker-Domains) in browserbasierten Fetchern aktivieren. +- **DNS-Leak-Prävention**: Optionale DNS-over-HTTPS-Unterstützung zur Weiterleitung von DNS-Anfragen über Cloudflares DoH, um DNS-Leaks bei der Verwendung von Proxys zu verhindern. - **Async-Unterstützung**: Vollständige async-Unterstützung über alle Fetcher und dedizierte async Session-Klassen hinweg. ### Adaptives Scraping & KI-Integration diff --git a/docs/README_ES.md b/docs/README_ES.md index 58dccb8..31e4683 100644 --- a/docs/README_ES.md +++ b/docs/README_ES.md @@ -219,7 +219,8 @@ MySpider().start() - **Evasión Anti-bot**: Capacidades de sigilo avanzadas con `StealthyFetcher` y falsificación de fingerprint. Puede evadir fácilmente todos los tipos de Turnstile/Interstitial de Cloudflare con automatización. - **Gestión de Session**: Soporte de sesión persistente con las clases `FetcherSession`, `StealthySession` y `DynamicSession` para la gestión de cookies y estado entre solicitudes. - **Rotación de Proxy**: `ProxyRotator` integrado con estrategias de rotación cíclica o personalizadas en todos los tipos de sesión, además de sobrescrituras de Proxy por solicitud. -- **Bloqueo de Dominios**: Bloquea solicitudes a dominios específicos (y sus subdominios) en fetchers basados en navegador. +- **Bloqueo de Dominios y Anuncios**: Bloquea solicitudes a dominios específicos (y sus subdominios) o activa el bloqueo de anuncios integrado (~3,500 dominios de anuncios/rastreadores conocidos) en fetchers basados en navegador. +- **Prevención de Fugas DNS**: Soporte opcional de DNS-over-HTTPS para enrutar consultas DNS a través del DoH de Cloudflare, previniendo fugas DNS al usar proxies. - **Soporte Async**: Soporte async completo en todos los fetchers y clases de sesión async dedicadas. ### Scraping Adaptativo e Integración con IA diff --git a/docs/README_FR.md b/docs/README_FR.md index a452210..a687136 100644 --- a/docs/README_FR.md +++ b/docs/README_FR.md @@ -219,7 +219,8 @@ MySpider().start() - **Contournement anti-bot** : Capacités de furtivité avancées avec `StealthyFetcher` et usurpation d'empreinte. Peut facilement contourner tous les types de Turnstile/Interstitial de Cloudflare avec l'automatisation. - **Gestion de sessions** : Support de sessions persistantes avec les classes `FetcherSession`, `StealthySession` et `DynamicSession` pour la gestion des cookies et de l'état entre les requêtes. - **Rotation de proxy** : `ProxyRotator` intégré avec des stratégies de rotation cycliques ou personnalisées sur tous les types de sessions, plus des surcharges de proxy par requête. -- **Blocage de domaines** : Bloquez les requêtes vers des domaines spécifiques (et leurs sous-domaines) dans les fetchers basés sur navigateur. +- **Blocage de domaines et publicités** : Bloquez les requêtes vers des domaines spécifiques (et leurs sous-domaines) ou activez le blocage de publicités intégré (~3 500 domaines publicitaires/traceurs connus) dans les fetchers basés sur navigateur. +- **Prévention des fuites DNS** : Support optionnel de DNS-over-HTTPS pour router les requêtes DNS via le DoH de Cloudflare, empêchant les fuites DNS lors de l'utilisation de proxies. - **Support async** : Support async complet sur tous les fetchers et classes de sessions async dédiées. ### Scraping adaptatif & Intégration IA diff --git a/docs/README_JP.md b/docs/README_JP.md index 9a2a29d..a2f0a3f 100644 --- a/docs/README_JP.md +++ b/docs/README_JP.md @@ -219,7 +219,8 @@ MySpider().start() - **アンチボット回避**:`StealthyFetcher` と fingerprint 偽装による高度なステルス機能。自動化で Cloudflare の Turnstile/Interstitial のすべてのタイプを簡単に回避。 - **Session 管理**:リクエスト間で Cookie と状態を管理するための `FetcherSession`、`StealthySession`、`DynamicSession` クラスによる永続的な Session サポート。 - **Proxy 回転**:すべての Session タイプに対応したラウンドロビンまたはカスタム戦略の組み込み `ProxyRotator`、さらにリクエストごとの Proxy オーバーライド。 -- **ドメインブロック**:ブラウザベースの Fetcher で特定のドメイン(およびそのサブドメイン)へのリクエストをブロック。 +- **ドメイン&広告ブロック**:ブラウザベースの Fetcher で特定のドメイン(およびそのサブドメイン)へのリクエストをブロック、または内蔵広告ブロック(約3,500の既知の広告/トラッカードメイン)を有効化。 +- **DNS リーク防止**:Proxy 使用時の DNS リークを防ぐため、Cloudflare の DoH 経由で DNS クエリをルーティングするオプションの DNS-over-HTTPS サポート。 - **async サポート**:すべての Fetcher および専用 async Session クラス全体での完全な async サポート。 ### 適応型スクレイピングと AI 統合 diff --git a/docs/README_KR.md b/docs/README_KR.md index 6d79389..886aea4 100644 --- a/docs/README_KR.md +++ b/docs/README_KR.md @@ -219,7 +219,8 @@ MySpider().start() - **안티봇 우회**: `StealthyFetcher`와 fingerprint 위장을 통한 고급 스텔스 기능. 자동화로 모든 유형의 Cloudflare Turnstile/Interstitial을 손쉽게 우회합니다. - **세션 관리**: `FetcherSession`, `StealthySession`, `DynamicSession` 클래스로 요청 간 쿠키와 상태를 관리하는 영속적 세션을 지원합니다. - **프록시 로테이션**: 모든 세션 타입에 대응하는 순환 또는 커스텀 전략의 내장 `ProxyRotator`와 요청별 프록시 오버라이드를 제공합니다. -- **도메인 차단**: 브라우저 기반 Fetcher에서 특정 도메인(및 하위 도메인)으로의 요청을 차단합니다. +- **도메인 및 광고 차단**: 브라우저 기반 Fetcher에서 특정 도메인(및 하위 도메인)으로의 요청을 차단하거나 내장 광고 차단(약 3,500개의 알려진 광고/트래커 도메인)을 활성화합니다. +- **DNS 유출 방지**: 프록시 사용 시 DNS 유출을 방지하기 위해 Cloudflare DoH를 통해 DNS 쿼리를 라우팅하는 선택적 DNS-over-HTTPS 지원. - **비동기 지원**: 모든 Fetcher와 전용 비동기 세션 클래스에서 완전한 비동기를 지원합니다. ### 적응형 스크레이핑 & AI 통합 diff --git a/docs/README_RU.md b/docs/README_RU.md index 0fa251f..031c229 100644 --- a/docs/README_RU.md +++ b/docs/README_RU.md @@ -222,7 +222,8 @@ MySpider().start() - **Обход анти-ботов**: Расширенные возможности скрытности с `StealthyFetcher` и подмену fingerprint'ов. Может легко обойти все типы Cloudflare Turnstile/Interstitial с помощью автоматизации. - **Управление сессиями**: Поддержка постоянных сессий с классами `FetcherSession`, `StealthySession` и `DynamicSession` для управления cookie и состоянием между запросами. - **Ротация Proxy**: Встроенный `ProxyRotator` с циклической или пользовательскими стратегиями для всех типов сессий, а также переопределение Proxy для каждого запроса. -- **Блокировка доменов**: Блокируйте запросы к определённым доменам (и их поддоменам) в браузерных Fetcher'ах. +- **Блокировка доменов и рекламы**: Блокируйте запросы к определённым доменам (и их поддоменам) или включите встроенную блокировку рекламы (~3 500 известных рекламных/трекерных доменов) в браузерных Fetcher'ах. +- **Защита от утечки DNS**: Опциональная поддержка DNS-over-HTTPS для маршрутизации DNS-запросов через Cloudflare DoH, предотвращая утечку DNS при использовании прокси. - **Поддержка async**: Полная async-поддержка во всех Fetcher'ах и выделенных async-классах сессий. ### Адаптивный скрапинг и интеграция с ИИ diff --git a/docs/ai/mcp-server.md b/docs/ai/mcp-server.md index 27156af..08c7b61 100644 --- a/docs/ai/mcp-server.md +++ b/docs/ai/mcp-server.md @@ -33,6 +33,7 @@ The Scrapling MCP Server provides nine powerful tools for web scraping: - **Browser Impersonation**: Mimic real browsers with TLS fingerprinting, real browser headers matching that version, and more - **Parallel Processing**: Scrape multiple URLs concurrently for efficiency - **Session Persistence**: Reuse browser sessions across multiple requests for better performance +- **Ad Blocking**: All browser-based tools automatically block requests to ~3,500 known ad and tracker domains, saving tokens and speeding up page loads - **Prompt Injection Protection**: Automatic sanitization of hidden content (CSS-hidden elements, aria-hidden, zero-width characters, HTML comments, template tags) that could be used for prompt injection attacks #### But why use Scrapling MCP Server instead of other available tools? diff --git a/docs/cli/extract-commands.md b/docs/cli/extract-commands.md index fb0545c..671cdcc 100644 --- a/docs/cli/extract-commands.md +++ b/docs/cli/extract-commands.md @@ -24,7 +24,7 @@ The extract command is a set of simple terminal tools that: !!! tip "AI-Targeted Mode" - All extract commands support an `--ai-targeted` flag. When enabled, it extracts only the main body content, strips noise tags (script, style, noscript, svg), removes hidden elements that could be used for prompt injection (CSS-hidden, aria-hidden, template tags), strips zero-width unicode characters, and removes HTML comments. This is ideal when the output is destined for an AI model. + All extract commands support an `--ai-targeted` flag. When enabled, it extracts only the main body content, strips noise tags (script, style, noscript, svg), removes hidden elements that could be used for prompt injection (CSS-hidden, aria-hidden, template tags), strips zero-width unicode characters, and removes HTML comments. For browser commands (`fetch`/`stealthy-fetch`), it also automatically enables ad blocking. This is ideal when the output is destined for an AI model. ## Quick Start @@ -291,6 +291,8 @@ We will go through each command in detail below. --real-chrome/--no-real-chrome If you have a Chrome browser installed on your device, enable this, and the Fetcher will launch an instance of your browser and use it. (default: False) --proxy TEXT Proxy URL in format "http://username:password@host:port" -H, --extra-headers TEXT Extra headers in format "Key: Value" (can be used multiple times) + --dns-over-https / --no-dns-over-https Route DNS through Cloudflare's DoH to prevent DNS leaks when using proxies (default: False) + --block-ads / --no-block-ads Block requests to known ad and tracker domains (default: False) --ai-targeted Extract only main content and sanitize hidden elements for AI consumption (default: False) --help Show this message and exit. ``` @@ -337,6 +339,8 @@ We will go through each command in detail below. --hide-canvas / --show-canvas Add noise to canvas operations (default: False) --proxy TEXT Proxy URL in format "http://username:password@host:port" -H, --extra-headers TEXT Extra headers in format "Key: Value" (can be used multiple times) + --dns-over-https / --no-dns-over-https Route DNS through Cloudflare's DoH to prevent DNS leaks when using proxies (default: False) + --block-ads / --no-block-ads Block requests to known ad and tracker domains (default: False) --ai-targeted Extract only main content and sanitize hidden elements for AI consumption (default: False) --help Show this message and exit. ``` diff --git a/docs/index.md b/docs/index.md index ae41d6c..b1cf181 100644 --- a/docs/index.md +++ b/docs/index.md @@ -109,7 +109,8 @@ MySpider().start() - **Anti-bot Bypass**: Advanced stealth capabilities with `StealthyFetcher` and fingerprint spoofing. Can easily bypass all types of Cloudflare's Turnstile/Interstitial with automation. - **Session Management**: Persistent session support with `FetcherSession`, `StealthySession`, and `DynamicSession` classes for cookie and state management across requests. - **Proxy Rotation**: Built-in `ProxyRotator` with cyclic or custom rotation strategies across all session types, plus per-request proxy overrides. -- **Domain Blocking**: Block requests to specific domains (and their subdomains) in browser-based fetchers. +- **Domain & Ad Blocking**: Block requests to specific domains (and their subdomains) or enable built-in ad blocking (~3,500 known ad/tracker domains) in browser-based fetchers. +- **DNS Leak Prevention**: Optional DNS-over-HTTPS support to route DNS queries through Cloudflare's DoH, preventing DNS leaks when using proxies. - **Async Support**: Complete async support across all fetchers and dedicated async session classes. ### Adaptive Scraping & AI Integration From 887eeee4c2d68b31c5462ec7549f50a4a57f69be Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 12 Apr 2026 18:47:13 +0200 Subject: [PATCH 07/12] build: pump up version and deps --- pyproject.toml | 8 ++++---- scrapling/__init__.py | 2 +- server.json | 4 ++-- setup.cfg | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6ac5d9c..e8a5500 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "scrapling" # Static version instead of a dynamic version so we can get better layer caching while building docker, check the docker file to understand -version = "0.4.5" +version = "0.4.6" description = "Scrapling is an undetectable, powerful, flexible, high-performance Python library that makes Web Scraping easy and effortless as it should be!" readme = {file = "README.md", content-type = "text/markdown"} license = {file = "LICENSE"} @@ -61,7 +61,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "lxml>=6.0.2", + "lxml>=6.0.3", "cssselect>=1.4.0", "orjson>=3.11.8", "tld>=0.13.2", @@ -77,12 +77,12 @@ fetchers = [ "patchright==1.58.2", "browserforge>=1.2.4", "apify-fingerprint-datapoints>=0.12.0", - "msgspec>=0.20.0", + "msgspec>=0.21.0", "anyio>=4.12.1", "protego>=0.6.0", ] ai = [ - "mcp>=1.26.0", + "mcp>=1.27.0", "markdownify>=1.2.0", "scrapling[fetchers]", ] diff --git a/scrapling/__init__.py b/scrapling/__init__.py index a121db4..c0c66ff 100644 --- a/scrapling/__init__.py +++ b/scrapling/__init__.py @@ -1,5 +1,5 @@ __author__ = "Karim Shoair (karim.shoair@pm.me)" -__version__ = "0.4.5" +__version__ = "0.4.6" __copyright__ = "Copyright (c) 2024 Karim Shoair" from typing import Any, TYPE_CHECKING diff --git a/server.json b/server.json index d1a95ad..36f60e7 100644 --- a/server.json +++ b/server.json @@ -14,12 +14,12 @@ "mimeType": "image/png" } ], - "version": "0.4.5", + "version": "0.4.6", "packages": [ { "registryType": "pypi", "identifier": "scrapling", - "version": "0.4.5", + "version": "0.4.6", "runtimeHint": "uvx", "packageArguments": [ { diff --git a/setup.cfg b/setup.cfg index 48cddb3..0794d59 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = scrapling -version = 0.4.5 +version = 0.4.6 author = Karim Shoair author_email = karim.shoair@pm.me description = Scrapling is an undetectable, powerful, flexible, high-performance Python library that makes Web Scraping easy and effortless as it should be! From 4279ca8cbc4441a974c301042dc23493b96491db Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 12 Apr 2026 18:47:26 +0200 Subject: [PATCH 08/12] docs(agent): update skill with the latest changes --- agent-skill/Scrapling-Skill.zip | Bin 81526 -> 82913 bytes agent-skill/Scrapling-Skill/SKILL.md | 10 ++++++---- .../Scrapling-Skill/examples/README.md | 2 +- .../references/fetching/dynamic.md | 2 ++ .../references/fetching/stealthy.md | 2 ++ .../Scrapling-Skill/references/mcp-server.md | 4 ++++ 6 files changed, 15 insertions(+), 5 deletions(-) diff --git a/agent-skill/Scrapling-Skill.zip b/agent-skill/Scrapling-Skill.zip index c43d0df74a3310ba0889e6bc2f6d30f1c55b9d78..b8766e29220768219f31dd7a871e37031c2651c9 100644 GIT binary patch delta 25331 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 2. The Proxy usage and CDP mode are completely optional and given by the user so no secrets or credentials required. Depending on the user usage. > 3. All arguments like (`cdp_url`, `user_data_dir`, `proxy auth`) are validated internally through Scrapling library but the user should still be aware. -**IMPORTANT**: While using the commandline scraping commands, you MUST use the commandline argument `--ai-targeted` to protect from Prompt Injection! +**IMPORTANT**: While using the commandline scraping commands, you MUST use the commandline argument `--ai-targeted` to protect from Prompt Injection! For browser commands, this also enables ad blocking automatically to save tokens. ## Setup (once) Create a virtual Python environment through any way available, like `venv`, then inside the environment do: -`pip install "scrapling[all]>=0.4.5"` +`pip install "scrapling[all]>=0.4.6"` Then do this to download all the browsers' dependencies: @@ -156,7 +156,9 @@ Both (`fetch` / `stealthy-fetch`) share options: | --wait-selector | TEXT | CSS selector to wait for before proceeding | | --proxy | TEXT | Proxy URL in format "http://username:password@host:port" | | -H, --extra-headers | TEXT | Extra headers in format "Key: Value" (can be used multiple times) | -| --ai-targeted | None | Extract only main content and sanitize hidden elements for AI consumption (default: False) | +| --dns-over-https / --no-dns-over-https | None | Route DNS through Cloudflare's DoH to prevent DNS leaks when using proxies (default: False) | +| --block-ads / --no-block-ads | None | Block requests to ~3,500 known ad and tracker domains (default: False) | +| --ai-targeted | None | Extract only main content and sanitize hidden elements for AI consumption (default: False). Also enables ad blocking automatically. | This option is specific to `fetch` only: diff --git a/agent-skill/Scrapling-Skill/examples/README.md b/agent-skill/Scrapling-Skill/examples/README.md index 344dc0f..4f645cd 100644 --- a/agent-skill/Scrapling-Skill/examples/README.md +++ b/agent-skill/Scrapling-Skill/examples/README.md @@ -9,7 +9,7 @@ All examples collect **all 100 quotes across 10 pages**. Make sure Scrapling is installed: ```bash -pip install "scrapling[all]>=0.4.5" +pip install "scrapling[all]>=0.4.6" scrapling install --force ``` diff --git a/agent-skill/Scrapling-Skill/references/fetching/dynamic.md b/agent-skill/Scrapling-Skill/references/fetching/dynamic.md index 109fde6..f623521 100644 --- a/agent-skill/Scrapling-Skill/references/fetching/dynamic.md +++ b/agent-skill/Scrapling-Skill/references/fetching/dynamic.md @@ -76,6 +76,8 @@ All arguments for `DynamicFetcher` and its session classes: | additional_args | Additional arguments to be passed to Playwright's context as additional settings, and they take higher priority than Scrapling's settings. | ✔️ | | selector_config | A dictionary of custom parsing arguments to be used when creating the final `Selector`/`Response` class. | ✔️ | | blocked_domains | A set of domain names to block requests to. Subdomains are also matched (e.g., `"example.com"` blocks `"sub.example.com"` too). | ✔️ | +| block_ads | Block requests to ~3,500 known ad/tracking domains. Can be combined with `blocked_domains`. | ✔️ | +| dns_over_https | Route DNS queries through Cloudflare's DNS-over-HTTPS to prevent DNS leaks when using proxies. | ✔️ | | proxy_rotator | A `ProxyRotator` instance for automatic proxy rotation. Cannot be combined with `proxy`. | ✔️ | | retries | Number of retry attempts for failed requests. Defaults to 3. | ✔️ | | retry_delay | Seconds to wait between retry attempts. Defaults to 1. | ✔️ | diff --git a/agent-skill/Scrapling-Skill/references/fetching/stealthy.md b/agent-skill/Scrapling-Skill/references/fetching/stealthy.md index aea8475..7f3c896 100644 --- a/agent-skill/Scrapling-Skill/references/fetching/stealthy.md +++ b/agent-skill/Scrapling-Skill/references/fetching/stealthy.md @@ -58,6 +58,8 @@ Scrapling provides many options with this fetcher and its session classes. Befor | additional_args | Additional arguments to be passed to Playwright's context as additional settings, and they take higher priority than Scrapling's settings. | ✔️ | | selector_config | A dictionary of custom parsing arguments to be used when creating the final `Selector`/`Response` class. | ✔️ | | blocked_domains | A set of domain names to block requests to. Subdomains are also matched (e.g., `"example.com"` blocks `"sub.example.com"` too). | ✔️ | +| block_ads | Block requests to ~3,500 known ad/tracking domains. Can be combined with `blocked_domains`. | ✔️ | +| dns_over_https | Route DNS queries through Cloudflare's DNS-over-HTTPS to prevent DNS leaks when using proxies. | ✔️ | | proxy_rotator | A `ProxyRotator` instance for automatic proxy rotation. Cannot be combined with `proxy`. | ✔️ | | retries | Number of retry attempts for failed requests. Defaults to 3. | ✔️ | | retry_delay | Seconds to wait between retry attempts. Defaults to 1. | ✔️ | diff --git a/agent-skill/Scrapling-Skill/references/mcp-server.md b/agent-skill/Scrapling-Skill/references/mcp-server.md index 7bbc86b..a860f0a 100644 --- a/agent-skill/Scrapling-Skill/references/mcp-server.md +++ b/agent-skill/Scrapling-Skill/references/mcp-server.md @@ -164,6 +164,10 @@ When `main_content_only=true` (the default), the server automatically sanitizes Keep `main_content_only=true` for maximum protection. +## Ad blocking + +All browser-based tools (`fetch`, `bulk_fetch`, `stealthy_fetch`, `bulk_stealthy_fetch`) and persistent sessions (`open_session`) automatically block requests to ~3,500 known ad and tracker domains. This is always enabled in the MCP server to save tokens and speed up page loads. No configuration needed. + ## Setup Start the server (stdio transport, used by most MCP clients): From d19e861e45b078dfa78eb00deba69f61c039a28a Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Mon, 13 Apr 2026 01:08:53 +0200 Subject: [PATCH 09/12] fix: correct Seconds type alias to accept float values The Seconds type was defined as `Annotated[int, float, Meta(ge=0)]` which per PEP 593 treated float as metadata, not a type. This caused passing float values like wait=1.5 to be rejected. Fixed by using Annotated[float, Meta(ge=0)] since int is a subtype of float. Correct fix for #240 Co-Authored-By: Cocoon-Break <54054995+kuishou68@users.noreply.github.com> --- scrapling/engines/_browsers/_validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapling/engines/_browsers/_validators.py b/scrapling/engines/_browsers/_validators.py index f7bb34e..504ba78 100644 --- a/scrapling/engines/_browsers/_validators.py +++ b/scrapling/engines/_browsers/_validators.py @@ -53,7 +53,7 @@ def _is_invalid_cdp_url(cdp_url: str) -> bool | str: # Type aliases for cleaner annotations PagesCount = Annotated[int, Meta(ge=1, le=50)] RetriesCount = Annotated[int, Meta(ge=1, le=10)] -Seconds = Annotated[int, float, Meta(ge=0)] +Seconds = Annotated[float, Meta(ge=0)] class PlaywrightConfig(Struct, kw_only=True, frozen=False, weakref=True): From 5e13d3ece6d93d519f4a8cee83e95d7816400879 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Mon, 13 Apr 2026 03:57:27 +0200 Subject: [PATCH 10/12] feat(browsers): add a pre-navigation hook to allow page setup Solves #238 --- scrapling/core/_shell_signatures.py | 2 ++ scrapling/engines/_browsers/_controllers.py | 24 +++++++++++++++++---- scrapling/engines/_browsers/_stealth.py | 24 +++++++++++++++++---- scrapling/engines/_browsers/_types.py | 2 ++ scrapling/engines/_browsers/_validators.py | 4 ++++ scrapling/fetchers/chrome.py | 6 ++++-- scrapling/fetchers/stealth_chrome.py | 6 ++++-- 7 files changed, 56 insertions(+), 12 deletions(-) diff --git a/scrapling/core/_shell_signatures.py b/scrapling/core/_shell_signatures.py index b2340fb..00f1fe9 100644 --- a/scrapling/core/_shell_signatures.py +++ b/scrapling/core/_shell_signatures.py @@ -47,6 +47,7 @@ _FETCH_PARAMS = { "wait": int | float, "timezone_id": str | None, "page_action": Optional[Callable], + "page_setup": Optional[Callable], "proxy": Optional[str | Dict[str, str] | Tuple], "extra_headers": Optional[Dict[str, str]], "timeout": int | float, @@ -80,6 +81,7 @@ _STEALTHY_FETCH_PARAMS = { "wait": int | float, "timezone_id": str | None, "page_action": Optional[Callable], + "page_setup": Optional[Callable], "proxy": Optional[str | Dict[str, str] | Tuple], "extra_headers": Optional[Dict[str, str]], "timeout": int | float, diff --git a/scrapling/engines/_browsers/_controllers.py b/scrapling/engines/_browsers/_controllers.py index ce4b643..edabb17 100644 --- a/scrapling/engines/_browsers/_controllers.py +++ b/scrapling/engines/_browsers/_controllers.py @@ -47,7 +47,8 @@ class DynamicSession(SyncSession, DynamicSessionMixin): :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000 :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the ` Response ` object. - :param page_action: Added for automation. A function that takes the `page` object and does the automation you need. + :param page_action: Added for automation. A function that takes the `page` object, runs after navigation, and does the automation you need. + :param page_setup: A function that takes the `page` object, runs before navigation. Use it to register event listeners or routes that must be set up before the page loads. :param wait_selector: Wait for a specific CSS selector to be in a specific state. :param init_script: An absolute path to a JavaScript file to be executed on page creation for all pages in this session. :param locale: Specify user locale, for example, `en-GB`, `de-DE`, etc. Locale will affect navigator.language value, Accept-Language request header value as well as number and date formatting @@ -105,7 +106,8 @@ class DynamicSession(SyncSession, DynamicSessionMixin): :param google_search: Enabled by default, Scrapling will set a Google referer header. :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000 :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the ` Response ` object. - :param page_action: Added for automation. A function that takes the `page` object and does the automation you need. + :param page_action: Added for automation. A function that takes the `page` object, runs after navigation, and does the automation you need. + :param page_setup: A function that takes the `page` object, runs before navigation. Use it to register event listeners or routes that must be set up before the page loads. :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` takes priority over the referer set here if used together._ :param disable_resources: Drop requests for unnecessary resources for a speed boost. Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. @@ -152,6 +154,12 @@ class DynamicSession(SyncSession, DynamicSessionMixin): ), ) + if params.page_setup: + try: + params.page_setup(page) + except Exception as e: # pragma: no cover + log.error(f"Error executing page_setup: {e}") + try: first_response = page.goto(url, referer=referer) self._wait_for_page_stability(page, params.load_dom, params.network_idle) @@ -228,7 +236,8 @@ class AsyncDynamicSession(AsyncSession, DynamicSessionMixin): :param load_dom: Enabled by default, wait for all JavaScript on page(s) to fully load and execute. :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000 :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the ` Response ` object. - :param page_action: Added for automation. A function that takes the `page` object and does the automation you need. + :param page_action: Added for automation. A function that takes the `page` object, runs after navigation, and does the automation you need. + :param page_setup: A function that takes the `page` object, runs before navigation. Use it to register event listeners or routes that must be set up before the page loads. :param wait_selector: Wait for a specific CSS selector to be in a specific state. :param init_script: An absolute path to a JavaScript file to be executed on page creation for all pages in this session. :param locale: Specify user locale, for example, `en-GB`, `de-DE`, etc. Locale will affect navigator.language value, Accept-Language request header value as well as number and date formatting @@ -285,7 +294,8 @@ class AsyncDynamicSession(AsyncSession, DynamicSessionMixin): :param google_search: Enabled by default, Scrapling will set a Google referer header. :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000 :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the ` Response ` object. - :param page_action: Added for automation. A function that takes the `page` object and does the automation you need. + :param page_action: Added for automation. A function that takes the `page` object, runs after navigation, and does the automation you need. + :param page_setup: A function that takes the `page` object, runs before navigation. Use it to register event listeners or routes that must be set up before the page loads. :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` takes priority over the referer set here if used together._ :param disable_resources: Drop requests for unnecessary resources for a speed boost. Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. @@ -333,6 +343,12 @@ class AsyncDynamicSession(AsyncSession, DynamicSessionMixin): ), ) + if params.page_setup: + try: + await params.page_setup(page) + except Exception as e: # pragma: no cover + log.error(f"Error executing page_setup: {e}") + try: first_response = await page.goto(url, referer=referer) await self._wait_for_page_stability(page, params.load_dom, params.network_idle) diff --git a/scrapling/engines/_browsers/_stealth.py b/scrapling/engines/_browsers/_stealth.py index f06c62e..efa195e 100644 --- a/scrapling/engines/_browsers/_stealth.py +++ b/scrapling/engines/_browsers/_stealth.py @@ -47,7 +47,8 @@ class StealthySession(SyncSession, StealthySessionMixin): :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000 :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the ` Response ` object. - :param page_action: Added for automation. A function that takes the `page` object and does the automation you need. + :param page_action: Added for automation. A function that takes the `page` object, runs after navigation, and does the automation you need. + :param page_setup: A function that takes the `page` object, runs before navigation. Use it to register event listeners or routes that must be set up before the page loads. :param wait_selector: Wait for a specific CSS selector to be in a specific state. :param init_script: An absolute path to a JavaScript file to be executed on page creation for all pages in this session. :param locale: Specify user locale, for example, `en-GB`, `de-DE`, etc. Locale will affect navigator.language value, Accept-Language request header value as well as number and date formatting @@ -187,7 +188,8 @@ class StealthySession(SyncSession, StealthySessionMixin): :param google_search: Enabled by default, Scrapling will set a Google referer header. :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000 :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the ` Response ` object. - :param page_action: Added for automation. A function that takes the `page` object and does the automation you need. + :param page_action: Added for automation. A function that takes the `page` object, runs after navigation, and does the automation you need. + :param page_setup: A function that takes the `page` object, runs before navigation. Use it to register event listeners or routes that must be set up before the page loads. :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` takes priority over the referer set here if used together._ :param disable_resources: Drop requests for unnecessary resources for a speed boost. Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. @@ -235,6 +237,12 @@ class StealthySession(SyncSession, StealthySessionMixin): ), ) + if params.page_setup: + try: + params.page_setup(page) + except Exception as e: # pragma: no cover + log.error(f"Error executing page_setup: {e}") + try: first_response = page.goto(url, referer=referer) self._wait_for_page_stability(page, params.load_dom, params.network_idle) @@ -315,7 +323,8 @@ class AsyncStealthySession(AsyncSession, StealthySessionMixin): :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000 :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the ` Response ` object. - :param page_action: Added for automation. A function that takes the `page` object and does the automation you need. + :param page_action: Added for automation. A function that takes the `page` object, runs after navigation, and does the automation you need. + :param page_setup: A function that takes the `page` object, runs before navigation. Use it to register event listeners or routes that must be set up before the page loads. :param wait_selector: Wait for a specific CSS selector to be in a specific state. :param init_script: An absolute path to a JavaScript file to be executed on page creation for all pages in this session. :param locale: Specify user locale, for example, `en-GB`, `de-DE`, etc. Locale will affect navigator.language value, Accept-Language request header value as well as number and date formatting @@ -454,7 +463,8 @@ class AsyncStealthySession(AsyncSession, StealthySessionMixin): :param google_search: Enabled by default, Scrapling will set a Google referer header. :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000 :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the ` Response ` object. - :param page_action: Added for automation. A function that takes the `page` object and does the automation you need. + :param page_action: Added for automation. A function that takes the `page` object, runs after navigation, and does the automation you need. + :param page_setup: A function that takes the `page` object, runs before navigation. Use it to register event listeners or routes that must be set up before the page loads. :param extra_headers: A dictionary of extra headers to add to the request. _The referer set by `google_search` takes priority over the referer set here if used together._ :param disable_resources: Drop requests for unnecessary resources for a speed boost. Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. @@ -503,6 +513,12 @@ class AsyncStealthySession(AsyncSession, StealthySessionMixin): ), ) + if params.page_setup: + try: + await params.page_setup(page) + except Exception as e: # pragma: no cover + log.error(f"Error executing page_setup: {e}") + try: first_response = await page.goto(url, referer=referer) await self._wait_for_page_stability(page, params.load_dom, params.network_idle) diff --git a/scrapling/engines/_browsers/_types.py b/scrapling/engines/_browsers/_types.py index 5030480..b436c59 100644 --- a/scrapling/engines/_browsers/_types.py +++ b/scrapling/engines/_browsers/_types.py @@ -74,6 +74,7 @@ class PlaywrightSession(TypedDict, total=False): wait: int | float timezone_id: str | None page_action: Optional[Callable] + page_setup: Optional[Callable] proxy: Optional[str | Dict[str, str] | Tuple] proxy_rotator: Optional[ProxyRotator] extra_headers: Optional[Dict[str, str]] @@ -105,6 +106,7 @@ class PlaywrightFetchParams(TypedDict, total=False): disable_resources: bool wait_selector: Optional[str] page_action: Optional[Callable] + page_setup: Optional[Callable] selector_config: Optional[Dict] extra_headers: Optional[Dict[str, str]] wait_selector_state: SelectorWaitStates diff --git a/scrapling/engines/_browsers/_validators.py b/scrapling/engines/_browsers/_validators.py index 504ba78..77dc81d 100644 --- a/scrapling/engines/_browsers/_validators.py +++ b/scrapling/engines/_browsers/_validators.py @@ -71,6 +71,7 @@ class PlaywrightConfig(Struct, kw_only=True, frozen=False, weakref=True): wait: Seconds = 0 timezone_id: str | None = "" page_action: Optional[Callable] = None + page_setup: Optional[Callable] = None proxy: Optional[str | Dict[str, str] | Tuple] = None # The default value for proxy in Playwright's source is `None` proxy_rotator: Optional[ProxyRotator] = None extra_headers: Optional[Dict[str, str]] = None @@ -96,6 +97,8 @@ class PlaywrightConfig(Struct, kw_only=True, frozen=False, weakref=True): """Custom validation after msgspec validation""" if self.page_action and not callable(self.page_action): raise TypeError(f"page_action must be callable, got {type(self.page_action).__name__}") + if self.page_setup and not callable(self.page_setup): + raise TypeError(f"page_setup must be callable, got {type(self.page_setup).__name__}") if self.proxy and self.proxy_rotator: raise ValueError( "Cannot use 'proxy_rotator' together with 'proxy'. " @@ -160,6 +163,7 @@ class _fetch_params: timeout: Seconds wait: Seconds page_action: Optional[Callable] + page_setup: Optional[Callable] extra_headers: Optional[Dict[str, str]] disable_resources: bool wait_selector: Optional[str] diff --git a/scrapling/fetchers/chrome.py b/scrapling/fetchers/chrome.py index f7795b7..3c2efcf 100644 --- a/scrapling/fetchers/chrome.py +++ b/scrapling/fetchers/chrome.py @@ -23,7 +23,8 @@ class DynamicFetcher(BaseFetcher): :param load_dom: Enabled by default, wait for all JavaScript on page(s) to fully load and execute. :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000 :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the Response object. - :param page_action: Added for automation. A function that takes the `page` object and does the automation you need. + :param page_action: Added for automation. A function that takes the `page` object, runs after navigation, and does the automation you need. + :param page_setup: A function that takes the `page` object, runs before navigation. Use it to register event listeners or routes that must be set up before the page loads. :param wait_selector: Wait for a specific CSS selector to be in a specific state. :param init_script: An absolute path to a JavaScript file to be executed on page creation with this request. :param locale: Set the locale for the browser if wanted. Defaults to the system default locale. @@ -65,7 +66,8 @@ class DynamicFetcher(BaseFetcher): :param load_dom: Enabled by default, wait for all JavaScript on page(s) to fully load and execute. :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000 :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the Response object. - :param page_action: Added for automation. A function that takes the `page` object and does the automation you need. + :param page_action: Added for automation. A function that takes the `page` object, runs after navigation, and does the automation you need. + :param page_setup: A function that takes the `page` object, runs before navigation. Use it to register event listeners or routes that must be set up before the page loads. :param wait_selector: Wait for a specific CSS selector to be in a specific state. :param init_script: An absolute path to a JavaScript file to be executed on page creation with this request. :param locale: Set the locale for the browser if wanted. Defaults to the system default locale. diff --git a/scrapling/fetchers/stealth_chrome.py b/scrapling/fetchers/stealth_chrome.py index 70ce937..ad6e01f 100644 --- a/scrapling/fetchers/stealth_chrome.py +++ b/scrapling/fetchers/stealth_chrome.py @@ -27,7 +27,8 @@ class StealthyFetcher(BaseFetcher): :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000 :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the ` Response ` object. - :param page_action: Added for automation. A function that takes the `page` object and does the automation you need. + :param page_action: Added for automation. A function that takes the `page` object, runs after navigation, and does the automation you need. + :param page_setup: A function that takes the `page` object, runs before navigation. Use it to register event listeners or routes that must be set up before the page loads. :param wait_selector: Wait for a specific CSS selector to be in a specific state. :param init_script: An absolute path to a JavaScript file to be executed on page creation for all pages in this session. :param locale: Specify user locale, for example, `en-GB`, `de-DE`, etc. Locale will affect navigator.language value, Accept-Language request header value as well as number and date formatting @@ -78,7 +79,8 @@ class StealthyFetcher(BaseFetcher): :param network_idle: Wait for the page until there are no network connections for at least 500 ms. :param timeout: The timeout in milliseconds that is used in all operations and waits through the page. The default is 30,000 :param wait: The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the ` Response ` object. - :param page_action: Added for automation. A function that takes the `page` object and does the automation you need. + :param page_action: Added for automation. A function that takes the `page` object, runs after navigation, and does the automation you need. + :param page_setup: A function that takes the `page` object, runs before navigation. Use it to register event listeners or routes that must be set up before the page loads. :param wait_selector: Wait for a specific CSS selector to be in a specific state. :param init_script: An absolute path to a JavaScript file to be executed on page creation for all pages in this session. :param locale: Specify user locale, for example, `en-GB`, `de-DE`, etc. Locale will affect navigator.language value, Accept-Language request header value as well as number and date formatting From 158ff04c968a81d6a04f1c06cbdd62572c2408f3 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Mon, 13 Apr 2026 04:38:23 +0200 Subject: [PATCH 11/12] docs: add the new page_setup feature --- docs/fetching/dynamic.md | 28 ++++++++++++++++++++++++++-- docs/fetching/stealthy.md | 5 +++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/fetching/dynamic.md b/docs/fetching/dynamic.md index e8ce9db..2e1f537 100644 --- a/docs/fetching/dynamic.md +++ b/docs/fetching/dynamic.md @@ -72,7 +72,8 @@ Scrapling provides many options with this fetcher and its session classes. To ma | load_dom | Enabled by default, wait for all JavaScript on page(s) to fully load and execute (wait for the `domcontentloaded` state). | ✔️ | | timeout | The timeout (milliseconds) used in all operations and waits through the page. The default is 30,000 ms (30 seconds). | ✔️ | | wait | The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the `Response` object. | ✔️ | -| page_action | Added for automation. Pass a function that takes the `page` object and does the necessary automation. | ✔️ | +| page_action | Added for automation. Pass a function that takes the `page` object, runs after navigation, and does the necessary automation. | ✔️ | +| page_setup | A function that takes the `page` object, runs before navigation. Use it to register event listeners or routes that must be set up before the page loads. | ✔️ | | wait_selector | Wait for a specific css selector to be in a specific state. | ✔️ | | init_script | An absolute path to a JavaScript file to be executed on page creation for all pages in this session. | ✔️ | | wait_selector_state | Scrapling will wait for the given state to be fulfilled for the selector given with `wait_selector`. _Default state is `attached`._ | ✔️ | @@ -96,7 +97,7 @@ Scrapling provides many options with this fetcher and its session classes. To ma | capture_xhr | Pass a regex URL pattern string to capture XHR/fetch requests matching it during page load. Captured responses are available via `response.captured_xhr`. Defaults to `None` (disabled). | ✔️ | | executable_path | Absolute path to a custom browser executable to use instead of the bundled Chromium. Useful for non-standard installations or custom browser builds. | ✔️ | -In session classes, all these arguments can be set globally for the session. Still, you can configure each request individually by passing some of the arguments here that can be configured on the browser tab level like: `google_search`, `timeout`, `wait`, `page_action`, `extra_headers`, `disable_resources`, `wait_selector`, `wait_selector_state`, `network_idle`, `load_dom`, `blocked_domains`, `proxy`, and `selector_config`. +In session classes, all these arguments can be set globally for the session. Still, you can configure each request individually by passing some of the arguments here that can be configured on the browser tab level like: `google_search`, `timeout`, `wait`, `page_action`, `page_setup`, `extra_headers`, `disable_resources`, `wait_selector`, `wait_selector_state`, `network_idle`, `load_dom`, `blocked_domains`, `proxy`, and `selector_config`. !!! note "Notes:" @@ -172,6 +173,29 @@ with open(file='main_cover.png', mode='wb') as f: The `body` attribute of the `Response` object always returns `bytes`. +### Pre-Navigation Setup +If you need to set up event listeners, routes, or scripts that must be registered before the page navigates, use `page_setup`. This function receives the `page` object and runs before `page.goto()` is called. + +```python +from playwright.sync_api import Page + +def capture_websockets(page: Page): + page.on("websocket", lambda ws: print(f"WebSocket opened: {ws.url}")) + +page = DynamicFetcher.fetch('https://example.com', page_setup=capture_websockets) +``` +Async version: +```python +from playwright.async_api import Page + +async def capture_websockets(page: Page): + page.on("websocket", lambda ws: print(f"WebSocket opened: {ws.url}")) + +page = await DynamicFetcher.async_fetch('https://example.com', page_setup=capture_websockets) +``` + +You can combine it with `page_action` -- `page_setup` runs before navigation, `page_action` runs after. + ### Browser Automation This is where your knowledge about [Playwright's Page API](https://playwright.dev/python/docs/api/class-page) comes into play. The function you pass here takes the page object from Playwright's API, performs the desired action, and then the fetcher continues. diff --git a/docs/fetching/stealthy.md b/docs/fetching/stealthy.md index 5b7c042..8daf0b0 100644 --- a/docs/fetching/stealthy.md +++ b/docs/fetching/stealthy.md @@ -49,7 +49,8 @@ Scrapling provides many options with this fetcher and its session classes. Befor | load_dom | Enabled by default, wait for all JavaScript on page(s) to fully load and execute (wait for the `domcontentloaded` state). | ✔️ | | timeout | The timeout (milliseconds) used in all operations and waits through the page. The default is 30,000 ms (30 seconds). | ✔️ | | wait | The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the `Response` object. | ✔️ | -| page_action | Added for automation. Pass a function that takes the `page` object and does the necessary automation. | ✔️ | +| page_action | Added for automation. Pass a function that takes the `page` object, runs after navigation, and does the necessary automation. | ✔️ | +| page_setup | A function that takes the `page` object, runs before navigation. Use it to register event listeners or routes that must be set up before the page loads. | ✔️ | | wait_selector | Wait for a specific css selector to be in a specific state. | ✔️ | | init_script | An absolute path to a JavaScript file to be executed on page creation for all pages in this session. | ✔️ | | wait_selector_state | Scrapling will wait for the given state to be fulfilled for the selector given with `wait_selector`. _Default state is `attached`._ | ✔️ | @@ -77,7 +78,7 @@ Scrapling provides many options with this fetcher and its session classes. Befor | capture_xhr | Pass a regex URL pattern string to capture XHR/fetch requests matching it during page load. Captured responses are available via `response.captured_xhr`. Defaults to `None` (disabled). | ✔️ | | executable_path | Absolute path to a custom browser executable to use instead of the bundled Chromium. Useful for non-standard installations or custom browser builds. | ✔️ | -In session classes, all these arguments can be set globally for the session. Still, you can configure each request individually by passing some of the arguments here that can be configured on the browser tab level like: `google_search`, `timeout`, `wait`, `page_action`, `extra_headers`, `disable_resources`, `wait_selector`, `wait_selector_state`, `network_idle`, `load_dom`, `solve_cloudflare`, `blocked_domains`, `proxy`, and `selector_config`. +In session classes, all these arguments can be set globally for the session. Still, you can configure each request individually by passing some of the arguments here that can be configured on the browser tab level like: `google_search`, `timeout`, `wait`, `page_action`, `page_setup`, `extra_headers`, `disable_resources`, `wait_selector`, `wait_selector_state`, `network_idle`, `load_dom`, `solve_cloudflare`, `blocked_domains`, `proxy`, and `selector_config`. !!! note "Notes:" From c2e66ab75351d35b52f32453da618d16daea43fc Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Mon, 13 Apr 2026 04:39:01 +0200 Subject: [PATCH 12/12] docs(agent): update skill with the latest changes --- agent-skill/Scrapling-Skill.zip | Bin 82913 -> 83257 bytes .../references/fetching/dynamic.md | 28 ++++++++++++++++-- .../references/fetching/stealthy.md | 5 ++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/agent-skill/Scrapling-Skill.zip b/agent-skill/Scrapling-Skill.zip index b8766e29220768219f31dd7a871e37031c2651c9..220183b517e707516db32418604807b6ba9e6382 100644 GIT binary patch delta 20721 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
6&vJ7 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 ZKe-p`E(r9`tRyaOdwLMa;~H@B^lx&^pvwRN diff --git a/agent-skill/Scrapling-Skill/references/fetching/dynamic.md b/agent-skill/Scrapling-Skill/references/fetching/dynamic.md index f623521..a831d7c 100644 --- a/agent-skill/Scrapling-Skill/references/fetching/dynamic.md +++ b/agent-skill/Scrapling-Skill/references/fetching/dynamic.md @@ -60,7 +60,8 @@ All arguments for `DynamicFetcher` and its session classes: | load_dom | Enabled by default, wait for all JavaScript on page(s) to fully load and execute (wait for the `domcontentloaded` state). | ✔️ | | timeout | The timeout (milliseconds) used in all operations and waits through the page. The default is 30,000 ms (30 seconds). | ✔️ | | wait | The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the `Response` object. | ✔️ | -| page_action | Added for automation. Pass a function that takes the `page` object and does the necessary automation. | ✔️ | +| page_action | Added for automation. Pass a function that takes the `page` object, runs after navigation, and does the necessary automation. | ✔️ | +| page_setup | A function that takes the `page` object, runs before navigation. Use it to register event listeners or routes that must be set up before the page loads. | ✔️ | | wait_selector | Wait for a specific css selector to be in a specific state. | ✔️ | | init_script | An absolute path to a JavaScript file to be executed on page creation for all pages in this session. | ✔️ | | wait_selector_state | Scrapling will wait for the given state to be fulfilled for the selector given with `wait_selector`. _Default state is `attached`._ | ✔️ | @@ -84,7 +85,7 @@ All arguments for `DynamicFetcher` and its session classes: | capture_xhr | Pass a regex URL pattern string to capture XHR/fetch requests matching it during page load. Captured responses are available via `response.captured_xhr`. Defaults to `None` (disabled). | ✔️ | | executable_path | Absolute path to a custom browser executable to use instead of the bundled Chromium. Useful for non-standard installations or custom browser builds. | ✔️ | -In session classes, all these arguments can be set globally for the session. Still, you can configure each request individually by passing some of the arguments here that can be configured on the browser tab level like: `google_search`, `timeout`, `wait`, `page_action`, `extra_headers`, `disable_resources`, `wait_selector`, `wait_selector_state`, `network_idle`, `load_dom`, `blocked_domains`, `proxy`, and `selector_config`. +In session classes, all these arguments can be set globally for the session. Still, you can configure each request individually by passing some of the arguments here that can be configured on the browser tab level like: `google_search`, `timeout`, `wait`, `page_action`, `page_setup`, `extra_headers`, `disable_resources`, `wait_selector`, `wait_selector_state`, `network_idle`, `load_dom`, `blocked_domains`, `proxy`, and `selector_config`. **Notes:** 1. The `disable_resources` option made requests ~25% faster in tests for some websites and can help save proxy usage, but be careful with it, as it can cause some websites to never finish loading. @@ -156,6 +157,29 @@ with open(file='main_cover.png', mode='wb') as f: The `body` attribute of the `Response` object always returns `bytes`. +### Pre-Navigation Setup +If you need to set up event listeners, routes, or scripts that must be registered before the page navigates, use `page_setup`. This function receives the `page` object and runs before `page.goto()` is called. + +```python +from playwright.sync_api import Page + +def capture_websockets(page: Page): + page.on("websocket", lambda ws: print(f"WebSocket opened: {ws.url}")) + +page = DynamicFetcher.fetch('https://example.com', page_setup=capture_websockets) +``` +Async version: +```python +from playwright.async_api import Page + +async def capture_websockets(page: Page): + page.on("websocket", lambda ws: print(f"WebSocket opened: {ws.url}")) + +page = await DynamicFetcher.async_fetch('https://example.com', page_setup=capture_websockets) +``` + +You can combine it with `page_action` -- `page_setup` runs before navigation, `page_action` runs after. + ### Browser Automation This is where your knowledge about [Playwright's Page API](https://playwright.dev/python/docs/api/class-page) comes into play. The function you pass here takes the page object from Playwright's API, performs the desired action, and then the fetcher continues. diff --git a/agent-skill/Scrapling-Skill/references/fetching/stealthy.md b/agent-skill/Scrapling-Skill/references/fetching/stealthy.md index 7f3c896..8939c8d 100644 --- a/agent-skill/Scrapling-Skill/references/fetching/stealthy.md +++ b/agent-skill/Scrapling-Skill/references/fetching/stealthy.md @@ -38,7 +38,8 @@ Scrapling provides many options with this fetcher and its session classes. Befor | load_dom | Enabled by default, wait for all JavaScript on page(s) to fully load and execute (wait for the `domcontentloaded` state). | ✔️ | | timeout | The timeout (milliseconds) used in all operations and waits through the page. The default is 30,000 ms (30 seconds). | ✔️ | | wait | The time (milliseconds) the fetcher will wait after everything finishes before closing the page and returning the `Response` object. | ✔️ | -| page_action | Added for automation. Pass a function that takes the `page` object and does the necessary automation. | ✔️ | +| page_action | Added for automation. Pass a function that takes the `page` object, runs after navigation, and does the necessary automation. | ✔️ | +| page_setup | A function that takes the `page` object, runs before navigation. Use it to register event listeners or routes that must be set up before the page loads. | ✔️ | | wait_selector | Wait for a specific css selector to be in a specific state. | ✔️ | | init_script | An absolute path to a JavaScript file to be executed on page creation for all pages in this session. | ✔️ | | wait_selector_state | Scrapling will wait for the given state to be fulfilled for the selector given with `wait_selector`. _Default state is `attached`._ | ✔️ | @@ -66,7 +67,7 @@ Scrapling provides many options with this fetcher and its session classes. Befor | capture_xhr | Pass a regex URL pattern string to capture XHR/fetch requests matching it during page load. Captured responses are available via `response.captured_xhr`. Defaults to `None` (disabled). | ✔️ | | executable_path | Absolute path to a custom browser executable to use instead of the bundled Chromium. Useful for non-standard installations or custom browser builds. | ✔️ | -In session classes, all these arguments can be set globally for the session. Still, you can configure each request individually by passing some of the arguments here that can be configured on the browser tab level like: `google_search`, `timeout`, `wait`, `page_action`, `extra_headers`, `disable_resources`, `wait_selector`, `wait_selector_state`, `network_idle`, `load_dom`, `solve_cloudflare`, `blocked_domains`, `proxy`, and `selector_config`. +In session classes, all these arguments can be set globally for the session. Still, you can configure each request individually by passing some of the arguments here that can be configured on the browser tab level like: `google_search`, `timeout`, `wait`, `page_action`, `page_setup`, `extra_headers`, `disable_resources`, `wait_selector`, `wait_selector_state`, `network_idle`, `load_dom`, `solve_cloudflare`, `blocked_domains`, `proxy`, and `selector_config`. **Notes:**