Chciałbym napisać scrapera, który pobierałby mi dane z jakieś strony. Obrazłem za cel Allegro. Najpierw chcę go napisać tak, by nie korzystał z Api, tylko parsował HTML.
-ShopFetcher
będzie odpowiadać za pobieranie danych i zwracanie aukcji. Nie będzie ona przechowywać żadnych informacji, więc jest oznaczona jako static
.
-ShopXPaths
odpowiada za wyrażenia xPath
do poszczególnych fragmentów podstrony. Pomyślałem, że logicznie będzie wydzielić te informacje do innej klasy (i przestrzeni nazw)
-Models
odpowiada za modele obiektów, które będę zwracał (np. aukcja, użytkownik)
-StringExtensions
zawiera rozszerzenia, które mogą się przydać do parsowania tekstu
Jeśli chodzi o kod, to wygląda to tak:
namespace ShopSharp
{
static class ShopFetcher
{
// Setup the configuration to support document loading
private static readonly IConfiguration DefultConfig = Configuration.Default.WithDefaultLoader();
public static async Task<Auction> GetAuctionAsync(string url)
{
// Asynchronously get the document in a new context using the configuration
var document = await BrowsingContext.New(DefultConfig).OpenAsync(url);
// Parse auction data
var title = document.QuerySelector(ShopXPaths.Title).TextContent.Trim();
var price = document.QuerySelector(ShopXPaths.Price).Attributes["data-price"].Value;
var quantity = document.QuerySelector(ShopXPaths.Quantity).TextContent.GetDigit();
// Create auction object using DTO Model
var auction = new Auction()
{
Topic = title,
Price = decimal.Parse(price, CultureInfo.InvariantCulture),
Quantity = quantity,
Url = url
};
return auction;
}
}
}
namespace ShopSharp.Helpers
{
static class ShopXPaths
{
public const string Title = "h1.title";
public const string Price = "div.price";
public const string Quantity = "span.no-wrap";
}
}
namespace ShopSharp.Models
{
class Auction
{
public string Url { get; set; }
public string Topic { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}
}
namespace ShopSharp.Extensions
{
static class StringExtensions
{
public static int GetDigit(this string str)
{
var m = Regex.Match(str, @"\d+");
if (m.Success)
{
var parsed = int.TryParse(m.Value, out var number);
if (parsed)
return number;
}
throw new Exception();
}
}
}
I przykładowe wykorzystanie:
private async void buttonScrape_ClickAsync(object sender, EventArgs e)
{
string url = textBoxUrl.Text;
if (string.IsNullOrEmpty(url))
return;
try
{
var auction = await ShopFetcher.GetAuctionAsync(url);
FillTextBoxWithAuction(auction);
}
catch (Exception exception)
{
MessageBox.Show(exception.Message, "Wystąpił błąd!");
}
}
private void FillTextBoxWithAuction(Auction auction)
{
var sb = new StringBuilder();
sb.Append("Tytuł: ").Append(auction.Topic).Append(Environment.NewLine);
richTextBoxResult.Text = sb.ToString();
}
Mam natomiast kilka pytań:
-
Czy metodę
GetDigit()
powinienem użyć tak jak zrobiłem, czy przy:Quantity = quantity.GetDigit()
, -
Czy metodę
decimal.Parse
powinienem użyć tak jak zrobiłem, czy przy tak jak zrobiłem zGetDigit()
, czyli na końcu metody. -
Jeśli aukcja nie ma np. ceny, to po dojechaniu na
var price = document.QuerySelector....
wyrzuci wyjątekobject reference not set to an instance of an object
. Czy w takim razie za każdym razem powinienem zrobić coś takiego:
string price;
try
{
price = document.QuerySelector(ShopXPaths.Price).Attributes["data-price"].Value;
}
catch (Exception)
{
throw new Exception("Price is missing");
}
czy może:
string price;
if (document.QuerySelector(ShopXPaths.Price).Attributes["data-price"].Value != null)
price = document.QuerySelector(ShopXPaths.Price).Attributes["data-price"].Value;
else
throw new Exception("Price is missing")
- w
buttonScrape_ClickAsync
najpierw sprawdzam, czy textbox nie jest pusty. Czy jest to ok? Może nie powinienem wprowadzać walidacji w obsłudze zdarzenia przycisku, a zamiast tego w metodzieGetAuctionAsync
. Wtedy wyglądałoby to tak:
public static async Task<Auction> GetAuctionAsync(string url)
{
if (string.IsNullOrEmpty(url))
throw new Exception("Wrong url")
// reszta kodu
}
a może walidacja tu i tu?
Macie może jeszcze jakieś wskazówki jak mógłbym polepszyć swój kod? Coś zrobiłem nie tak?