public static function parse($html, $url) { $recipe = RecipeParser_Parser_MicrodataDataVocabulary::parse($html, $url); libxml_use_internal_errors(true); $html = mb_convert_encoding($html, 'HTML-ENTITIES', "UTF-8"); $doc = new DOMDocument(); $doc->loadHTML('<?xml encoding="UTF-8">' . $html); $xpath = new DOMXPath($doc); // // Some of the ingredient lines in on The Daily Meal do not adhere to // the usual microdata formatting. Here we fall back to looking for a // regular list within a higher-level ingredients div. // if (!empty($recipe->ingredients)) { $nodes = $xpath->query("//div[@class='content']/div[@class='ingredient']/ul/li"); foreach ($nodes as $node) { $value = RecipeParser_Text::formatAsOneLine($node->nodeValue); if (empty($value)) { continue; } if (RecipeParser_Text::matchSectionName($value)) { $value = RecipeParser_Text::formatSectionName($value); $recipe->addIngredientsSection($value); } else { $recipe->appendIngredient($value); } } } // // The Daily Meal provides servings details via Edamam's plugin. // if (!$recipe->yield) { $nodes = $xpath->query("//table[@class='edamam-data']/tr[2]/td[2]"); if ($nodes->length) { $recipe->yield = RecipeParser_Text::formatYield($nodes->item(0)->nodeValue); } } return $recipe; }
public static function parse($html, $url) { $recipe = RecipeParser_Parser_MicrodataSchema::parse($html, $url); libxml_use_internal_errors(true); $html = mb_convert_encoding($html, 'HTML-ENTITIES', "UTF-8"); $doc = new DOMDocument(); $doc->loadHTML('<?xml encoding="UTF-8">' . $html); $xpath = new DOMXPath($doc); // Yield $nodes = $xpath->query('//*[@class="prep_box"]'); foreach ($nodes as $node) { $line = $node->nodeValue; if (preg_match("/Number of Servings: (\\d+)/", $line, $m)) { $recipe->yield = RecipeParser_Text::formatYield($m[1]); } } // Instructions $recipe->resetInstructions(); $str = ""; $nodes = $xpath->query('//*[@itemprop="recipeInstructions"]'); if ($nodes->length) { $children = $nodes->item(0)->childNodes; // This is a piece of HTML that has <br> tags for breaks in each instruction. // Rather than just getting nodeValue, I want to preserve the <br> tags. So I'm // looking for them as nodes and appending them to the string. Any other nodes // (either #text or other, e.g. <a href="">) get passed along into the string as // nodeValue. foreach ($children as $child) { if ($child->nodeName == "br") { $str .= "<br>"; } else { $line = trim($child->nodeValue); if (!empty($line)) { $str .= $line; } } } $lines = explode("<br>", $str); foreach ($lines as $line) { if (empty($line)) { continue; } else { if (RecipeParser_Text::matchSectionName($line)) { $line = RecipeParser_Text::formatSectionName($line); $recipe->addInstructionsSection($line); } else { if (!empty($line)) { $line = RecipeParser_Text::formatAsOneLine($line); $line = RecipeParser_Text::stripLeadingNumbers($line); if (stripos($line, "Recipe submitted by SparkPeople") === 0) { continue; } if (stripos($line, "Number of Servings:") === 0) { continue; } $recipe->appendInstruction($line); } } } } } return $recipe; }
public static function parse($html, $url) { $recipe = new RecipeParser_Recipe(); libxml_use_internal_errors(true); $doc = new DOMDocument(); $html = mb_convert_encoding($html, 'HTML-ENTITIES', "UTF-8"); $doc->loadHTML('<?xml encoding="UTF-8">' . $html); $xpath = new DOMXPath($doc); // Title $nodes = $xpath->query('//*[@property="v:name"]'); if ($nodes->length) { $recipe->title = trim($nodes->item(0)->nodeValue); } // Summary $nodes = $xpath->query('//*[@property="v:summary"]'); if ($nodes->length) { $value = trim($nodes->item(0)->nodeValue); $recipe->description = $value; } // Times $searches = array('v:prepTime' => 'prep', 'v:cookTime' => 'cook', 'v:totalTime' => 'total'); foreach ($searches as $itemprop => $time_key) { $nodes = $xpath->query('//*[@property="' . $itemprop . '"]'); if ($nodes->length) { if ($value = $nodes->item(0)->getAttribute('content')) { $value = RecipeParser_Text::iso8601ToMinutes($value); } else { $value = trim($nodes->item(0)->nodeValue); $value = RecipeParser_Times::toMinutes($value); } if ($value) { $recipe->time[$time_key] = $value; } } } // Yield $nodes = $xpath->query('//*[@property="v:yield"]'); if ($nodes->length) { $line = trim($nodes->item(0)->nodeValue); $line = preg_replace('/\\s+/', ' ', $line); $recipe->yield = RecipeParser_Text::formatYield($line); } // Ingredients $nodes = null; // (data-vocabulary) $nodes = $xpath->query('//*[@rel="v:ingredient"]'); foreach ($nodes as $node) { $value = $node->nodeValue; $value = RecipeParser_Text::formatAsOneLine($value); if (empty($value)) { continue; } if (RecipeParser_Text::matchSectionName($value)) { $value = RecipeParser_Text::formatSectionName($value); $recipe->addIngredientsSection($value); } else { $recipe->appendIngredient($value); } } // Instructions $found = false; // Some sites will use an "instruction" class for each line. if (!$found) { $nodes = $xpath->query('//*[@property="v:instructions"]//*[@property="v:instruction"]'); if ($nodes->length) { RecipeParser_Text::parseInstructionsFromNodes($nodes, $recipe); $found = true; } } // Look for markup that uses <li>, <p> or other tags for each instruction. $search_sub_nodes = array("p", "li"); while (!$found && ($tag = array_pop($search_sub_nodes))) { $nodes = $xpath->query('//*[@property="v:instructions"]//' . $tag); if ($nodes->length) { RecipeParser_Text::parseInstructionsFromNodes($nodes, $recipe); $found = true; } } // Either multiple instrutions nodes, or one node with a blob of text. if (!$found) { $nodes = $xpath->query('//*[@property="v:instructions"]'); if ($nodes->length > 1) { // Multiple nodes RecipeParser_Text::parseInstructionsFromNodes($nodes, $recipe); $found = true; } else { if ($nodes->length == 1) { // Blob $str = $nodes->item(0)->nodeValue; RecipeParser_Text::parseInstructionsFromBlob($str, $recipe); $found = true; } } } // Photo $photo_url = ""; $nodes = $xpath->query('//*[@rel="v:photo"]'); if ($nodes->length) { $photo_url = $nodes->item(0)->getAttribute('src'); } if (!$photo_url) { // for <img> as sub-node of rel="v:photo" $nodes = $xpath->query('//*[@rel="v:photo"]//img'); if ($nodes->length) { $photo_url = $nodes->item(0)->getAttribute('src'); } } if ($photo_url) { $recipe->photo_url = RecipeParser_Text::formatPhotoUrl($photo_url, $url); } // Credits $nodes = $xpath->query('//*[@property="v:author"]'); if ($nodes->length) { $line = $nodes->item(0)->nodeValue; $recipe->credits = RecipeParser_Text::formatCredits($line); } return $recipe; }
public static function parse($html, $url) { $recipe = RecipeParser_Parser_Microformat::parse($html, $url); // Turn off libxml errors to prevent mismatched tag warnings. libxml_use_internal_errors(true); $html = mb_convert_encoding($html, 'HTML-ENTITIES', "UTF-8"); $doc = new DOMDocument(); $doc->loadHTML('<?xml encoding="UTF-8">' . $html); $xpath = new DOMXPath($doc); // Description $description = ""; $nodes = $xpath->query('//div[@id="recipe"]/p/i'); foreach ($nodes as $node) { $line = trim($node->nodeValue); if (strpos($line, "Adapted from") === false) { $description .= $line . "\n\n"; } } $description = trim($description); $recipe->description = $description; // Ingredients $recipe->resetIngredients(); $lines = array(); // Add ingredients to blob $nodes = $xpath->query('//div[@id="recipe"]/blockquote/p'); foreach ($nodes as $node) { foreach ($node->childNodes as $child) { $line = trim($child->nodeValue); switch ($child->nodeName) { case "strong": case "b": if (strpos($line, ":") === false) { $line .= ":"; } $lines[] = $line; break; case "#text": case "div": case "p": $lines[] = $line; break; } } } foreach ($lines as $line) { if (RecipeParser_Text::matchSectionName($line)) { $recipe->addIngredientsSection(RecipeParser_Text::formatSectionName($line)); } else { $line = RecipeParser_Text::formatAsOneLine($line); $recipe->appendIngredient($line); } } // Instructions $recipe->resetInstructions(); $lines = array(); $nodes = $xpath->query('//div[@id="recipe"]/*'); $passed_ingredients = false; foreach ($nodes as $node) { if ($node->nodeName == "blockquote") { $passed_ingredients = true; continue; } if ($node->nodeName == "p") { if ($passed_ingredients) { $line = trim($node->nodeValue); // Finished with ingredients once we hit "Adapted" notes or any <p> // with a class attribute. if (stripos($line, "Adapted from") !== false) { break; } else { if ($node->getAttribute("class")) { break; } } // Servings? if (stripos($line, "Serves ") === 0) { $recipe->yield = RecipeParser_Text::formatYield($line); continue; } $recipe->appendInstruction(RecipeParser_Text::formatAsOneLine($node->nodeValue)); } } } return $recipe; }
public function test_match_section_name_wrapped_equals() { $this->assertTrue(RecipeParser_Text::matchSectionName("==That==")); }
public static function parse($html, $url) { $recipe = new RecipeParser_Recipe(); libxml_use_internal_errors(true); $doc = new DOMDocument(); $html = mb_convert_encoding($html, 'HTML-ENTITIES', "UTF-8"); $doc->loadHTML('<?xml encoding="UTF-8">' . $html); $xpath = new DOMXPath($doc); $microdata = null; $nodes = $xpath->query('//*[contains(@itemtype, "//schema.org/Recipe") or contains(@itemtype, "//schema.org/recipe")]'); if ($nodes->length) { $microdata = $nodes->item(0); } // Parse elements if ($microdata) { // Title $nodes = $xpath->query('.//*[@itemprop="name"]', $microdata); if ($nodes->length) { $value = trim($nodes->item(0)->nodeValue); $recipe->title = RecipeParser_Text::formatTitle($value); } // Summary $nodes = $xpath->query('.//*[@itemprop="description"]', $microdata); if ($nodes->length) { $value = $nodes->item(0)->nodeValue; $value = RecipeParser_Text::formatAsParagraphs($value); $recipe->description = $value; } // Times $searches = array('prepTime' => 'prep', 'cookTime' => 'cook', 'totalTime' => 'total'); foreach ($searches as $itemprop => $time_key) { $nodes = $xpath->query('.//*[@itemprop="' . $itemprop . '"]', $microdata); if ($nodes->length) { if ($value = $nodes->item(0)->getAttribute('content')) { $value = RecipeParser_Text::iso8601ToMinutes($value); } else { if ($value = $nodes->item(0)->getAttribute('datetime')) { $value = RecipeParser_Text::iso8601ToMinutes($value); } else { $value = trim($nodes->item(0)->nodeValue); $value = RecipeParser_Times::toMinutes($value); } } if ($value) { $recipe->time[$time_key] = $value; } } } // Yield $nodes = $xpath->query('.//*[@itemprop="recipeYield"]', $microdata); if (!$nodes->length) { $nodes = $xpath->query('.//*[@itemprop="recipeyield"]', $microdata); } if ($nodes->length) { if ($nodes->item(0)->hasAttribute('content')) { $line = $nodes->item(0)->getAttribute('content'); } else { $line = $nodes->item(0)->nodeValue; } $recipe->yield = RecipeParser_Text::formatYield($line); } // Ingredients $nodes = $xpath->query('//*[@itemprop="ingredients"]'); foreach ($nodes as $node) { $value = $node->nodeValue; $value = RecipeParser_Text::formatAsOneLine($value); if (empty($value)) { continue; } if (strlen($value) > 150) { // probably a mistake, like a run-on of existing ingredients? continue; } if (RecipeParser_Text::matchSectionName($value)) { $value = RecipeParser_Text::formatSectionName($value); $recipe->addIngredientsSection($value); } else { $recipe->appendIngredient($value); } } // Instructions $found = false; // Look for markup that uses <li> tags for each instruction. if (!$found) { $nodes = $xpath->query('//*[@itemprop="recipeInstructions"]//li'); if ($nodes->length) { RecipeParser_Text::parseInstructionsFromNodes($nodes, $recipe); $found = true; } } // Look for instructions as direct descendents of "recipeInstructions". if (!$found) { $nodes = $xpath->query('//*[@itemprop="recipeInstructions"]/*'); if ($nodes->length) { RecipeParser_Text::parseInstructionsFromNodes($nodes, $recipe); $found = true; } } // Some sites will use an "instruction" class for each line. if (!$found) { $nodes = $xpath->query('.//*[@itemprop="recipeInstructions"]//*[contains(concat(" ", normalize-space(@class), " "), " instruction ")]'); if ($nodes->length) { RecipeParser_Text::parseInstructionsFromNodes($nodes, $recipe); $found = true; } } // Either multiple recipeInstructions nodes, or one node with a blob of text. if (!$found) { $nodes = $xpath->query('.//*[@itemprop="recipeInstructions"]'); if ($nodes->length > 1) { // Multiple nodes RecipeParser_Text::parseInstructionsFromNodes($nodes, $recipe); $found = true; } else { if ($nodes->length == 1) { // Blob $str = $nodes->item(0)->nodeValue; RecipeParser_Text::parseInstructionsFromBlob($str, $recipe); $found = true; } } } // Photo $photo_url = ""; if (!$photo_url) { // try to find open graph url $nodes = $xpath->query('//meta[@property="og:image"]'); if ($nodes->length) { $photo_url = $nodes->item(0)->getAttribute('content'); } } if (!$photo_url) { $nodes = $xpath->query('.//*[@itemprop="image"]', $microdata); if ($nodes->length) { $photo_url = $nodes->item(0)->getAttribute('src'); } } if (!$photo_url) { // for <img> as sub-node of class="photo" $nodes = $xpath->query('.//*[@itemprop="image"]//img', $microdata); if ($nodes->length) { $photo_url = $nodes->item(0)->getAttribute('src'); } } if ($photo_url) { $recipe->photo_url = RecipeParser_Text::formatPhotoUrl($photo_url, $url); } // Credits $line = ""; $nodes = $xpath->query('.//*[@itemprop="author"]', $microdata); if ($nodes->length) { $line = $nodes->item(0)->nodeValue; } $nodes = $xpath->query('.//*[@itemprop="publisher"]', $microdata); if ($nodes->length) { $line = $nodes->item(0)->nodeValue; } $recipe->credits = RecipeParser_Text::formatCredits($line); } return $recipe; }
public static function parse($html, $url) { $recipe = new RecipeParser_Recipe(); libxml_use_internal_errors(true); $doc = new DOMDocument(); $html = mb_convert_encoding($html, 'HTML-ENTITIES', "UTF-8"); $doc->loadHTML('<?xml encoding="UTF-8">' . $html); $xpath = new DOMXPath($doc); // Find the top-level node for Recipe microdata $microdata = null; $nodes = $xpath->query('//*[@itemtype="http://data-vocabulary.org/Recipe"]'); if ($nodes->length) { $microdata = $nodes->item(0); } // Parse elements if ($microdata) { // Title $nodes = $xpath->query('.//*[@itemprop="name"]', $microdata); if ($nodes->length) { $value = $nodes->item(0)->nodeValue; $value = RecipeParser_Text::formatTitle($value); $recipe->title = $value; } // Summary $nodes = $xpath->query('.//*[@itemprop="summary"]', $microdata); if ($nodes->length) { $value = trim($nodes->item(0)->nodeValue); $recipe->description = $value; } // Times $searches = array('prepTime' => 'prep', 'cookTime' => 'cook', 'totalTime' => 'total'); foreach ($searches as $itemprop => $time_key) { $nodes = $xpath->query('.//*[@itemprop="' . $itemprop . '"]', $microdata); if ($nodes->length) { if ($value = $nodes->item(0)->getAttribute('datetime')) { $value = RecipeParser_Text::iso8601ToMinutes($value); } else { if ($value = $nodes->item(0)->getAttribute('content')) { $value = RecipeParser_Text::iso8601ToMinutes($value); } else { $value = trim($nodes->item(0)->nodeValue); $value = RecipeParser_Times::toMinutes($value); } } if ($value) { $recipe->time[$time_key] = $value; } } } // Yield $line = ""; $nodes = $xpath->query('.//*[@itemprop="yield"]', $microdata); if ($nodes->length) { $line = trim($nodes->item(0)->nodeValue); } else { $nodes = $xpath->query('.//*[@itemprop="servingSize"]', $microdata); if ($nodes->length) { $line = trim($nodes->item(0)->nodeValue); } } if ($line) { $line = preg_replace('/\\s+/', ' ', $line); $recipe->yield = RecipeParser_Text::formatYield($line); } // Ingredients $nodes = null; // (data-vocabulary) if (!$nodes || !$nodes->length) { $nodes = $xpath->query('.//*[@itemprop="ingredient"]', $microdata); } if (!$nodes || !$nodes->length) { // non-standard $nodes = $xpath->query('.//*[@id="ingredients"]//li', $microdata); } if (!$nodes || !$nodes->length) { // non-standard $nodes = $xpath->query('.//*[@class="ingredients"]//li', $microdata); } foreach ($nodes as $node) { $value = $node->nodeValue; $value = RecipeParser_Text::formatAsOneLine($value); if (empty($value)) { continue; } if (RecipeParser_Text::matchSectionName($value)) { $value = RecipeParser_Text::formatSectionName($value); $recipe->addIngredientsSection($value); } else { $recipe->appendIngredient($value); } } // Instructions $found = false; // Look for markup that uses <li> tags for each instruction. if (!$found) { $nodes = $xpath->query('.//*[@itemprop="instructions"]//li', $microdata); if ($nodes->length) { RecipeParser_Text::parseInstructionsFromNodes($nodes, $recipe); $found = true; } } // Some sites will use an "instruction" class for each line. if (!$found) { $nodes = $xpath->query('.//*[@itemprop="instruction"]//*[contains(concat(" ", normalize-space(@class), " "), " instruction ")]', $microdata); if ($nodes->length) { RecipeParser_Text::parseInstructionsFromNodes($nodes, $recipe); $found = true; } } // Either multiple instrutions nodes, or one node with a blob of text. if (!$found) { $nodes = $xpath->query('.//*[@itemprop="instructions"]', $microdata); if ($nodes->length > 1) { // Multiple nodes RecipeParser_Text::parseInstructionsFromNodes($nodes, $recipe); $found = true; } else { if ($nodes->length == 1) { // Blob $str = $nodes->item(0)->nodeValue; RecipeParser_Text::parseInstructionsFromBlob($str, $recipe); $found = true; } } } // Photo $photo_url = ""; if (!$photo_url) { // try to find open graph url $nodes = $xpath->query('//meta[@property="og:image"]'); if ($nodes->length) { $photo_url = $nodes->item(0)->getAttribute('content'); } } if (!$photo_url) { $nodes = $xpath->query('.//*[@itemprop="photo"]', $microdata); if ($nodes->length) { if ($nodes->item(0)->hasAttribute('src')) { $photo_url = $nodes->item(0)->getAttribute('src'); } else { if ($nodes->item(0)->hasAttribute('content')) { $photo_url = $nodes->item(0)->getAttribute('content'); } } } } if (!$photo_url) { // for <img> as sub-node of class="photo" $nodes = $xpath->query('.//*[@itemprop="photo"]//img', $microdata); if ($nodes->length) { $photo_url = $nodes->item(0)->getAttribute('src'); } } if ($photo_url) { $recipe->photo_url = RecipeParser_Text::relativeToAbsolute($photo_url, $url); } // Credits $nodes = $xpath->query('.//*[@itemprop="author"]', $microdata); if ($nodes->length) { $line = $nodes->item(0)->nodeValue; $recipe->credits = RecipeParser_Text::formatCredits($line); } } return $recipe; }
public static function parse($html, $url) { // Turn off libxml errors to prevent mismatched tag warnings. libxml_use_internal_errors(true); $html = mb_convert_encoding($html, 'HTML-ENTITIES', "UTF-8"); $doc = new DOMDocument(); $doc->loadHTML('<?xml encoding="UTF-8">' . $html); $xpath = new DOMXPath($doc); $recipe = RecipeParser_Parser_MicrodataSchema::parse($html, $url); // OVERRIDES FOR ABOUT.COM // Title $nodes = $xpath->query('//*[@itemprop="headline name"]'); if ($nodes->length) { $value = trim($nodes->item(0)->nodeValue); $recipe->title = RecipeParser_Text::formatTitle($value); } // Credits $nodes = $xpath->query('//*[@itemprop="author"]//*[@itemprop="name"]'); if ($nodes->length) { $line = $nodes->item(0)->nodeValue; $recipe->credits = RecipeParser_Text::formatCredits($line . ", About.com"); } // Ingredients $recipe->resetIngredients(); $nodes = $xpath->query('//*[@itemprop="ingredients"]'); foreach ($nodes as $node) { $value = $node->nodeValue; $value = RecipeParser_Text::formatAsOneLine($value); if (RecipeParser_Text::matchSectionName($value) || $node->childNodes->item(0)->nodeName == "strong" || $node->childNodes->item(0)->nodeName == "b") { $value = RecipeParser_Text::formatSectionName($value); $recipe->addIngredientsSection($value); } else { $recipe->appendIngredient($value); } } // Instructions $recipe->resetInstructions(); $nodes = $xpath->query('//div[@itemprop="recipeInstructions"]'); foreach ($nodes as $node) { $text = trim($node->nodeValue); $lines = preg_split("/[\n\r]+/", $text); for ($i = count($lines) - 1; $i >= 0; $i--) { $lines[$i] = trim($lines[$i]); // Remove ends of lines that have the word "recipes" squashed up against // another word, which seems to happen with long lists of related // recipe links. // Remove lines that have the phrase "Xxxxx Recipes and More". // Remove lines that have the phrase "Xxxxx Recipes | Xxxxx". // Remove mentions of newsletters. $lines[$i] = preg_replace("/(.*)recipes\\w/i", "\$1", $lines[$i]); $lines[$i] = preg_replace("/(.*)More .* Recipes.*/", "\$1", $lines[$i]); $lines[$i] = preg_replace("/(.*)Recipes and More.*/", "\$1", $lines[$i]); $lines[$i] = preg_replace("/(.*)Recipes \\| .*/", "\$1", $lines[$i]); $lines[$i] = preg_replace("/(.*)Recipe Newsletter.*/", "\$1", $lines[$i]); // Look for a line in the instructions that looks like a yield. if (strpos($lines[$i], "Makes ") === 0) { $recipe->yield = substr($lines[$i], 6); $lines[$i] = ''; continue; } } foreach ($lines as $line) { $line = trim($line); if (empty($line)) { continue; } if (strtolower($line) == "preparation") { continue; } // Match section names that read something like "---For the cake: Raise the oven temperature..." if (preg_match("/^(?:-{2,})?For the (.+)\\: (.*)\$/i", $line, $m)) { $section = $m[1]; $section = RecipeParser_Text::formatSectionName($section); $recipe->addInstructionsSection($section); // Reset the value of $line, without the section name. $line = ucfirst($m[2]); } $recipe->appendInstruction($line); } } return $recipe; }