我正在尝试使用bdd
,ddd
和oop
设计抓取应用程序。此应用程序的目的是检查页面是否已启动,stil是否包含某些元素,如链接,图像等。
使用BDD编写我的方案,我提出了Page
,Link
,Image
等类,其中包含url
,src
等属性, alt
。
我的问题是我看到两种可能性来检查实时网站:
1.使用另一个类crawler
类,它将使用前面类中包含的数据并点击Web检查页面是否已启动,是否包含预期元素等:
$crawler = new Crawler();
$page = new Page($url);
$pageReturned = $crawler->get($page);
if ($pageReturned->isUp()) {
// continue with the checking of element...
$image = new Image($src, $alt);
if ($pageReturned->contains($image)) {
// check other things
} else {
// image not found on the page
}
}
将这种“爬行”行为包含在类本身中(看起来更像oop
),这意味着如果它包含给定元素,我会询问页面是否已启动等:
$page = new Page($url);
if ($page->isUp()) {
$image = new Image($src, $alt);
if ($page->contains($image)) {
// check other things
} else {
// image not found on the page
}
}
我很想使用#2,但我想知道如何在不将类绑定到某个爬行库的情况下这样做。我希望稍后可以在不同的库之间切换,例如goutte
或guzzle
,甚至可以直接使用curl
。
也许我在这里完全忽略了oop
的观点...也许有更好/更聪明的方法来做这件事,因此我的问题。 :)
答案 0 :(得分:3)
要实现的一个有用的事情是你的模型代码往往是自包含的 - 它知道模型中的数据元素(即数据图)和数据一致性规则,但不知道其他任何东西。
所以你的页面模型可能看起来像
class Page {
URL uri;
ImageCollection images;
}
换句话说,模型知道页面和图像之间的关系,但它并不一定知道这些东西在实践中意味着什么。
要将您的域模型与现实世界进行实际比较,您可以向模型传递一些知道如何完成工作但不知道状态的服务。
class Crawler {
void verify(URL page, ImageCollection images)
}
现在你将它们匹配在一起;您构造Crawler,并将其传递给Page。页面查找其状态,并将该状态传递给爬网程序
class Page {
void verifyWith(Crawler crawler) {
crawler.verify(this.uri, this.items);
}
}
当然,您可能不希望将页面与Crawler过于紧密地联系在一起;毕竟,您可能想要换出爬虫库,您可能希望对页面状态执行其他操作。
所以你让这个方法的签名更加通用;它接受一个接口,而不是一个具有特定含义的对象。在经典书籍设计模式中,这将是Visitor Pattern
的示例class Page {
interface Visitor {
void visitPage(URL uri, ImageCollection images);
}
void verifyWith(Visitor visitor) {
visitor.visitPage(this.uri, this.images);
}
}
class Crawler implements Page.Visitor {
void visitPage(URL page, ImageCollection images) {
....
}
}
注意 - 模型(页面)负责维护其数据的完整性。这意味着它传递给访问者的任何数据都应该是不可变的,或者是模型状态的可变副本失败。
从长远来看,您可能不希望像这样嵌入在页面中的访问者的定义。网页是模型API的一部分,但访问者是模型SPI的一部分。
interface PageVisitor {
void visitPage(URL uri, ImageCollection images);
}
class Page {
void verifyWith(PageVisitor visitor) {
visitor.visitPage(this.uri, this.images);
}
}
class Crawler implements PageVisitor {
void visitPage(URL page, ImageCollection images) {
....
}
}
有一件事在这里被掩盖了,你似乎有两个不同的实现" page"
// Here's one?
$page = new Page($url);
// And here is something else?
$pageReturned = $crawler->get($page);
ddd的其中一个教训是事物的命名;尤其要确保你不要将两个真正具有不同含义的想法结合起来。在这种情况下,您应该清楚爬网程序返回的类型。
例如,如果您所在的域中无处不在的语言是从REST借来的,那么您可能会有类似
的语句$representation = $crawler->get($resource);
在您的示例中,语言看起来更具特定于HTML,因此这可能是合理的
$htmlDocument = $crawler->get($page)
揭露这一点的原因:文档/表示非常适合作为价值对象的概念 - 它是一个不可变的东西;你无法改变"页面"通过以任何方式操纵html文档。
值对象纯粹是查询表面 - 任何看起来像变异的方法实际上都是一个返回该类型的新实例的查询。
值对象非常适合plalx在其答案中描述的规范模式:
HtmlSpecification {
boolean isSatisfiedBy(HtmlDocument);
}
答案 1 :(得分:1)
这样的事情怎么样?您可以利用任何现有的HTML解析框架来构建可通过CSS选择器查询的文档对象模型,并抽象出域接口背后的实现。
我还使用规范模式为页面创建匹配标准,这样可以很容易地创建新规则。
用法:
var elementsQuery = new ElementsQuery('image[src="someImage.png"], a[href="http://www.google.com"]');
var spec = new PageAvailable().and(new ContainsElements(elementQuery, 2));
var page = pageLoader.load(url);
if (spec.isSatisfiedBy(page)) {
//Page is available & the page contains exactly one image with the attribute src=someImage.png and one link to google
}
您可以采取的一些改进设计的方法是创建一个流畅的构建器,使您可以更轻松地生成CSS选择器(ElementsQuery
)。
E.g。
var elementsQuery = new ElementsQueryBuilder()
.match('image').withAttr('src', 'someImage.png')
.match('a').withAttr('href', 'http://www.google.com');
如果您希望最终能够创建超出ElementsQuery
验证元素存在的规范,那么重要的是公开更强大的API来检查文档对象模型(DOM)。 / p>
您可以使用上述设计替换DOM
,并相应调整PageSpecification
API,以便为规范提供更多功能。
public interface Element {
public String tag();
public String attrValue(String attr);
public boolean containsElements(ElementsQuery query, ExpectedCount count);
public Elements queryElements(ElementsQuery query);
public Elements children();
}
访问整个DOM结构的优势是可以从域访问,而不仅仅是询问基础结构服务是否满足标准是规范声明和实现都可以存在于域中。
在@ VoiceOfUnreason的回答中,Crawler
实现必须存在于基础架构层中,而规则声明存在于域(ImageCollection
)中,检查这些规则的逻辑存在于基础设施。
最后,我认为页面监控条目可能是持久的,可以通过UI或配置文件进行配置。
我可能会做的是拥有两个不同的有界上下文。一个用于维护要监视的页面及其相关规范(Page
是此上下文中的实体),另一个负责执行监视的上下文(Page
是此上下文中的值 - 使用类似于什么的实现我描述过。)